一. 容器和泛型的引入
不管是什么语言,在开发程序的过程中和数据打交道是必不可少的,为了更好地容纳这些数据,我们在之前的学习中就引入了数组的概念,这确实是容纳数据的好办法,但是单单数组是完全不能满足我们的需求的,因为我们对数据远远不止只有容纳的要求,更重要的是要管理数据。诚然,对数组中的元素也可以进行管理,不过需要自己敲代码,如果功能仅限于此,那么这就不是Java了。作为更强大的语言,Java自然要帮你写好各种管理数据的代码,你只需要傻傻地调用就可以了,Java把这些容纳、管理数据的代码都封装成一些类,就成为了今天我们要学的——容器,也叫集合(Collection)。
容器有几点注意事项:
- Java的容器装的元素只能是一个对象。不过有包装类的存在,也不用担心基本数据类型装不进容器。
- 容器不止是像它字面上的意思——装东西,容器的强大之处是不单单要存储数据,还要帮你处理数据、管理数据。我们可以把容器理解成一个超级强化版的数据结构。
以下是Java容器类的主要框架:
现在我们还是来探讨一下Java那些写源码的人是怎么将任何类型的对象都可以存到一个容器的呢?这个问题在包装类的时候就已经想过了,Object[]数组实现(多态性)。好,现在又有一个新问题:我把乱七八糟的不同类型的对象全部放到一个容器里,那当我拿的时候还得挨个判断是不是我要的类型的数据再拿,这不废劲吗?的确,按照我们的生活经验,我们用容器(瓶子、管子啥的)装东西的时候,也要按照种类的不同分开装,然后为了我们自己更方便的知道指定的容器里装的是啥,我们会在容器的最外面贴上标签来提示自己。哈哈,Java也不傻,也有常识,既然都封装好了容器,自然要做的完美,也认为一个容器装的对象类型不能太杂了,也定义了一个类似标签的东东——泛型,来限制一下。
用专业的话来阐述一遍:泛型可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可读性和安全性。随便看看吧。泛型是在JDK1.5开始增加的。
二. 容器中泛型的使用
我们打开eclipse,不妨来看看Collection接口的部分源码,这些容器类都打包在了java.util包中:
好,这就是我们目前需要看的源码范围。咦,这接口名后面咋有个 '< E >'东西?这就是泛型!泛型定义的语句和语法全体现在上面了。这个泛型的定义呀,好比这个方法中的形参,这个<>中的这个字符E就好比形参的名字,这里的字符可以是任何标识符,不过一般采用T,E,V这3个字母,咱们也就别搞特殊了。那么怎么将一个确定的对象类型给传过去呢?具体语句是这样的:Collection<String> 对象名= new 具体实现类名<String>();
,在JDK1.7开始后面<>里的内容可以省略。这样,这个接口中 < E >就相当于< String >了,也就是说,这个容器对象只能存储String类的对象。这个过程是不是和方法中形参和实参特别相似?!
这里还有几点要注意:第一,既然容器只能装对象,那<>里必须是引用数据类型,不能是基本数据类型。第二,我们只是强烈建议使用泛型。事实上,泛型的使用不是强制的,不使用编译器也不会报错! 这点可不能与形参对应了。
package cn.zjb.test;
import java.util.ArrayList;
import java.util.Collection;
/**
* 测试集合类使用
* @author 张坚波
*
*/
public class TestCollection {
public static void main(String[] args) {
Collection c= new ArrayList(); //没加泛型
System.out.println(c.isEmpty()); //这个方法顾名思义,判断容器是否为空
}
}
运行结果如下:
这个程序只是为了强调一下泛型不是必加不可的,没有其他任何意义。
三. Collection接口
现在,有了前面知识的铺垫,我们就开始从封装好的容器“类”继承树体系的根节点开始以深度优先为原则进行学习。
首先来看看Collection接口的常用方法:
最后一个先不管,来测试一下:
package cn.zjb.test;
import java.util.ArrayList;
import java.util.Collection;
/**
* 测试集合类使用
* @author 张坚波
*
*/
public class TestCollection {
public static void main(String[] args) {
Collection<String> c= new ArrayList<>();
c.add("张");
c.add("坚");
c.add("波");
System.out.println(c);//[张, 坚, 波]
System.out.println(c.size()); //3
System.out.println(c.isEmpty()); //false
System.out.println(c.contains("zhang")); //false
System.out.println(c.contains("张")); //true
c.remove("张");
Object[] obj=c.toArray(); //Object的toString()方法打印的是哈希地址值
for(Object temp:obj)
System.out.print(temp+" "); //坚 波
System.out.println(""); //换行
Collection<String> c2= new ArrayList<>();
c2.add("张");
c2.addAll(c);
System.out.println(c2); //[张, 坚, 波]
System.out.println(c2.containsAll(c)); //true
c2.retainAll(c);
System.out.println(c2);// [坚,波]
c2.removeAll(c);
System.out.println(c2); //[] ,元素为空
}
}
四. List接口
java.util.List接口是有序、可重复的容器。它继承于Collection接口,所以Collection接口的方法它也有。 除了Collection接口中的方法,List多了一些跟顺序(索引)有关的方法,如图:
测试一下:
package cn.zjb.test;
import java.util.List;
import java.util.ArrayList;
/**
* 测试List接口
* @author 张坚波
*
*/
public class TestList {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("张");
list.add("坚");
// list.add(3, "波"); //错误的,必须要在原有的数组长度内容中添加或紧接着添加
list.add(2, "波");
System.out.println(list); //[张, 坚, 波]
list.set(1, "波");
System.out.println(list); //[张, 波, 波]
System.out.println(list.get(0)); //张
System.out.println(list.indexOf("波")); //1
System.out.println(list.lastIndexOf("波")); //2
list.remove(1);
System.out.println(list); //[张, 波]
}
}
List接口常用的实现类有3个:ArrayList、LinkedList和Vector。咱们依次学习一下。
五. ArrayList容器
顾名思义,ArrayList底层用于存储数据的方式肯定是: 数组。ArrayList的特点:查询效率高,增删效率低,线程不安全。我们一般使用它。
我们知道,数组长度是有限的,而ArrayList是可以存放任意数量的对象,长度不受限制,那么它是怎么实现的呢?本质上就是通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。 ArrayList的Object数组初始化长度为10,如果我们存储满了这个数组,需要存储第11个对象,就会定义新的长度更大的数组(容量是加上原来容量右移一位的结果,即一半),并将原数组内容和新的元素一起加入到新数组中,源码如下:
为了更好地掌握和使用ArrayList,我们来自己编写一下代码:
package cn.zjb.practice;
import java.util.Arrays;
/**
* MyArrayList类是用来对数据进行存储和管理的容器类,存储数据的结构为数组。
* <p>该类没有实现任何接口,并且是Object的直接子类。
* @author 张坚波
*
*/
public class MyArrayList <E> {
/**
* MyArrayList容器用来储存数据元素的结构。
*/
private static Object[] container;
/**
* MyArrayList容器默认的大小。
*/
private static final int DEFAULT_CAPACITY=5;
/**
* 检查是否需要扩容的变量
*/
private static int currentLength;
/**
* 当前container数组的长度
*/
private static int length;
/**
* 用于创建一个容量为默认大小的数组作为数据存储结构。
*/
public MyArrayList() {
container=new Object[DEFAULT_CAPACITY];
currentLength=DEFAULT_CAPACITY;
}
/**
* 用于创建一个容量为自定义大小的数组作为数据存储结构。
* @参数 initialCapacity是自定义长度
* (目前不考虑参数传递是否会出问题,所有的方法都这样,简化点)
*/
public MyArrayList(int initialCapacity) {
container=new Object[initialCapacity];
currentLength=initialCapacity;
}
/**
* 该方法用于添加元素
* @参数 e是泛型E的具体对象
*/
public void add(E e) {
if(length==currentLength-1) {
expend();
}
container[length++]=e;
}
/**
* 该方法用于删除指定索引的元素
* @参数 index表示索引位置
*/
public void remove(int index) {
if(index<=length-1) {
System.arraycopy(container, index+1, container, index, length-index+1);
length--;
}else
throw new RuntimeException();
}
/**
*该方法用于找出对应泛型元素的索引位置
*@参数 e表示要找的泛型对象
*@返回值 对应泛型的索引位置,如果没有返回-1
*/
public int indexOf(E e) {
for(int i=0;i<length;i++)
if(container[i].equals(e))
return i;
return -1;
}
/**
* 该方法用于找容器的大小
* @返回值 底层数组的长度
*/
public int size() {
return length;
}
/**
* 该方法用于自动扩容
*/
public void expend() {
container=Arrays.copyOf(container, currentLength*2);
currentLength=currentLength*2;
}
/**
*
* @参数 obj是container数组
* @返回值 将泛型E具体值以特殊格式返回
*/
public static String toString(Object[] obj) {
if(length==0)
return "[]";
else {
StringBuilder sb=new StringBuilder();
sb.append('[');
for(int i=0;i<length;i++)
sb.append(container[i]+",");
sb.setCharAt(sb.length()-1, ']');
return sb.toString();
}
}
/**
* 重写Object类的toString()方法,打印容器的内容
* @返回值 返回static String toString(Object[] obj)的返回值
*/
@Override
public String toString() {
return toString(container);
}
}
package cn.zjb.practice;
/**
* practice包中主方法所在的类
* @author 张坚波
*
*/
public class MainClass {
public static void main(String[] args) {
MyArrayList<String> a=new MyArrayList<>();
for(int i=0;i<10;i++) //实现了自动扩容
a.add("zhang");
System.out.println(a); //[zhang,zhang,zhang,zhang,zhang,zhang,zhang,zhang,zhang,zhang]
a.remove(2);
System.out.println(a); //[zhang,zhang,zhang,zhang,zhang,zhang,zhang,zhang,zhang]
System.out.println(a.indexOf("zhang")); //0
System.out.println(a.size()); //9
}
}
简化版的,不能再简化了,哈哈,就是找找感觉。