1. 集合类概述
1.1 集合类的意义
通常我们在编写Java代码的时候,会创建一些对象,通过调用对象的方法和数据(成员变量)来实现一些功能。在有些情况下,我们需要创建大量的的对象,为了提高代码的阅读性,并且不至于出现逻辑上的混乱,我们就需要一个用于存储对象的容器,方便对这些对象的管理和调用,这时集合类也就应运而生了。集合类实例对象的最大功能,就是用于存储其他对象(准确的说是其他对象的引用)。
1.2 集合类相对数组的优势
可能有朋友会说,数组也可以存储对象。但是,数组相对集合类有以下几点不足:
(1) 数组是不可变长度容器,集合是可变长度容器。这一点是集合类相对数组最大的优势,因为通常情况下,一开始我们并不知道运行某段代码具体需要创建多少个对象,而数组的固定长度,大大限制了代码的灵活性。
(2) 数组只能存储某一类对象,而集合可以存储多种类型的对象;
(3) 数组的功能比较单一,无法实现较为复杂的功能;而集合类分为好几种,每一种的功能侧重点不同,并且每一种都对外提供了丰富的方法,通过这些方法的组合,可以实现非常复杂的操作。
基于上述几点优势,集合的使用范围更为广泛,但是数组也可以帮助我们实现一些简单的功能的。
2. 集合类的体系——集合框架
上面说到集合类按照功能侧重点的不同分为好几种,将功能相似的集合类向上抽取出几个父类,再将这些父类的共性抽取出来,定义一个根父类,我们就将这一体系称为集合框架。而在这一体系中,不同集合类之间的区别就是他们存储对象的方式不同,这种方式称为数据结构,也就是说不同集合类的底层数据结构不同,而不同的数据结构虽然都可以用于存储数据,但是各有各的特点。下图表示的就是集合框架中常用的一些类(接口)及其继承关系。
上图中圆角方框表示一个接口或者类,虚线方框表示接口,实线方框表示可以创建对象的类,向上的箭头表示继承或者实现的关系,而粗黑框表示的类是我们要中点掌握的类。我们看到处于最顶层位置的Collection就是根父类(实际是接口,这里仅为表达,下同),该接口中定义了集合框架中所有类及接口的共性特征。Collection接口下又按照主要功能的不同派生出很多子接口,而呈现在上图中的List和Set分支是其中最常用的。在List和Set分支下面有更多的实现子类,而这些实线子类正是我们日后常用的用于存储对象容器类。
3. 集合框架顶层父类——Collection
3.1 Collection接口概述
我们曾经在讨论面向对象第二大特征继承的时候说到,通过根父类去快速了解一个继承体系,而创建子类实例对象去实现具体的功能。这是因为,通常顶层父类就是用于对这一大类事物的描述和功能的定义,也就是说是一个抽象类(或者接口,我们马上就要说到),无法创建实例对象,而子类会具体实现父类的方法。
那么对于集合框架来说,顶层根父类称为Collection,单词本意就是收集、集合的意思,该类在Java标准类库的位置为java.util(util意思是工具)。通过API文档可以了解到,Collection是一个接口,也就是说该接口的所有方法均是抽象的,并且没有对外提供公有构造方法。下面我们就通过API文档来学习该接口的方法,也是其实现子类的方法。这里我们需要提一句,既然,集合框架中的大部分类是容器类,那么这些类的大部分方法我们同样可以分为增(添加)、删(删除)、改(替换)、查(获取)四大类,这与我们前面介绍的StringBuffer类和StringBuilder是类似的。
3.2 Collection接口方法简介
添加:
boolean add(E e):将指定E类对象添加至容器中,使该对象成为该容器中的元素。大家可能会对E表示的类类型感到疑惑。Collection接口API文档中,Collecion接口名后有一个“<E>”的声明,这个声明涉及到泛型的知识,需要我们后面介绍,目前大家只需要把E理解为Object类即可。
booleanaddAll(Collection<?extends E> c):向该容器中添加指定容器对象c中的所有元素。
删除:
void clear():清除该容器中的所有对象。
booleanremove(Objecto):移除容器中的指定元素。
booleanremoveAll(Collection<?>c):移除该容器中同样包含在容器对象c中的元素。
判断:
booleancontains(Object o):判断指定对象是否包含在该容器中。
booleancontainsAll(Collection<?>c):判断该容器中是否包含指定容器总所有元素。
booleanisEmpty():判断集合中是否包含任意元素。
booleanretainAll(Collection<?>c):仅保留该容器中哪些同样包含在指定容器对象中的元素。可以简单理解为取两个容器的交集。
其他:
int size():获取该容器对象的元素的数量。实际上isEmpty方法就是判断size方法的返回值是否为0。
Object[] toArray():将该容器中的对象元素存储到一个数组中,并返回该数组。
获取:
Iterator<E>iterator():该方法专门用于从容器中获取数据,后面会重点介绍。
这里我们要说明两点:
(a) 所有从Object类继承来的方法这里不再介绍。
(b) 虽然,实际开发时从来不使用Collection接口,但是了解了该接口的方法,也就大体了解了集合框架中类的共性内容,有助于我们可以快速掌握其他实现子类。
3.3 Collection实现类——ArrayList方法代码演示
介绍完父接口的共性方法,下面我们通过一个实现子类——ArrayList——来演示这些方法的使用。首先演示,除获取元素以外的其他基本方法,而获取元素方法因其操作的特殊性,将单独开辟一节来进行介绍。
这里我们需要事先声明一点,虽然我们介绍的是ArrayList这个类的一些方法,但是这并不代表我们专门在讲解该类的特点,只不过是因为Collection作为接口,无法创建实例对象,因此从众多Collection实现子类中任意挑选出一个类,来介绍所有集合类的共性方法,而ArrayList类及其他集合类的各自的特性,比如底层的数据结构,将在后面一一讲解。
添加元素:
代码1:
//我们要使用的集合类在Java标准类库中的位置为java.util,所以一定记得导包
import java.util.*;
classCollectionDemo
{
public static void main(String[] args)
{
//创建一个ArrayList集合容器对象,该类是Collection父接口的实现子类
ArrayList al = new ArrayList();
//1.添加元素
al.add("HelloWorld!");//添加一个字符串对象
al.add(newInteger(50));//添加一个Integer对象
al.add(newObject());//添加一个Object对象
//2.获取该集合的长度
System.out.println("size= "+al.size());
//3.顺序打印集合中的元素对象
System.out.println(al);
}
}
运行结果为:
size = 3
[Hello World!, 50, java.lang.Object@175078b]
在编译上述代码1时,会有如下提示:
注: CollectionDemo.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked重新编译。
该提示的内容同样涉及到泛型的知识,我们将在后面的部分中介绍,这里大家只要理解为上面代码有安全隐患即可,编译器希望我们能够解决这个安全隐患,如果没有解决就是给出如上提示,但是允许编译通过。
针对上述代码1,我们要进行两点说明:
第一点,我们向ArrayList容器中添加了三种不同类型的对象,并且通过编译成功运行。那么这也就是为什么我们可以将add方法的参数列表中的E类型理解为Object的原因了。无论我们向add方法传递什么类型的对象,都可以通过多态的方式,当做Object对象接收。
第二点,我们提出一个这样的问题:当通过add方法向ArrayList容器对象中存储其他对象时,容器中真正存储的是对象本身还是对象的引用,或者说是对象的地址值呢?大家可以回想一下我们曾经在《面向对象9:对象的初始化&方法的调用》中讲到过,对象在创建的时候,会在堆内存中开辟一块空间用于存储对象的特有数据,换句话说,每个对象“实体”都是互相独立地存储在堆内存中,容器对象也不例外,因此容器内真正存储的并非是元素对象实体,而是元素对象的地址值。试想,如果真的为了在实际意义上将元素对象“实体”存储到容器中,而在堆内存中移动对象所在的地址,是非常麻烦的。
第三点,集合与数组不同,如果直接打印集合对象,将会自动顺序打印容器内的元素。String对象和Integer对象会直接打印对象的内容,而Object对象还是通过toString方法打印对象的哈希值。
删除元素:
代码2:
import java.util.*;
class CollectionDemo2
{
public static void main(String[] args)
{
ArrayList al = new ArrayList();
String str = "Hello World!";
Integer in = new Integer(100);
Object obj = new Object();
al.add(str);
al.add(in);
al.add(obj);
printInfo(al);
//删除单个元素
al.remove(str);
printInfo(al);
//清空集合
al.clear();
printInfo(al);
}
//将打印集合长度、集合中的元素等动作封装为一个方法
public static void printInfo(ArrayList al)
{
System.out.println(al);
System.out.println("size= "+al.size());
System.out.println();
}
}
运行结果为:
[Hello World!, 100, java.lang.Object@1db9742]
size = 3
[100, java.lang.Object@1db9742]
size = 2
[]
size = 0
从上述运行结果的第三部分,我们可以看出,通过clear方法将集合清空以后,集合内就不再包含任何元素了。
判断:
代码3:
import java.util.*;
class CollectionDemo3
{
public static void main(String[] args)
{
ArrayList al = new ArrayList();
String str = "Hello World!";
Integer in = new Integer(100);
Object obj = new Object();
al.add(str);
al.add(in);
al.add(obj);
//判断Integer对象是否存在于集合中
System.out.println("Integer对象是否存在:"+al.contains(in));
//判断集合是否为空
System.out.println("集合是否为空:"+al.isEmpty());
}
}
运行结果为:
Integer对象是否存在:true
集合是否为空:false
取交集:
代码4:
import java.util.*;
class CollectionDemo4
{
public static void main(String[] args)
{
ArrayList al1 = new ArrayList();
al1.add("Element1");
al1.add("Element2");
al1.add("Element3");
ArrayList al2 = new ArrayList();
al2.add("Element2");
al2.add("Element3");
al2.add("Element4");
//仅保留al1集合中同样也包含在al2中的元素,简单那说就是取交集
al1.retainAll(al2);
System.out.println(al1);
System.out.println(al2);
}
}
运行结果为:
[Element2, Element3]
[Element2, Element3, Element4]
从上述运行结果来看,在调用retain方法以后,集合al1中仅保留了同样包含在al2中的元素Element2和Element3。如果两个集合中没有相同的元素,那么al1中将不包含任何元素。
除了上述提到的几个方法以外,诸如addAll、containsAll、removeAll等方法不再作具体演示,使用方法与操作单个元素类似,只不过传递的参数是集合对象,希望大家自行尝试。
3.4 获取集合中元素的通用方法
(1) 获取集合中的元素
在前面的内容中,我们介绍了向集合中添加元素、删除元素、判断元素、获取集合的长度、打印集合中元素等方法,而唯独没有介绍如何获取集合中的元素。这是因为获取集合中的元素需要一个专门的“工具”,下面就向大家介绍如何通过这个“工具”获取集合中的元素。
Collection接口API文档方法摘要这一栏中有如下这么一个方法:
Iterator<E>iterator():返回在此collection的元素上进行迭代的迭代器。该方法的返回值类型为Iterator<E>,这就是迭代器,用于获取集合中元素的“工具”。
那么我们接着去查阅Iterator的API文档可知,这是一个接口,方法摘要中有三个方法:
booleanhasNext():如果仍有元素可以迭代,则返回true。该接口中定义了一个指针,按照某个顺序获取到一个元素以后,指针就指向下一个元素,那么该方法就会判断下一个元素是否存在。
E next():按照某个顺序,返回集合中的下一个元素,同时指针指向下一个元素。
void remove():从该迭代器指向的集合对象中,移除该迭代器返回的最后一个元素。
下面我们将ArrayList和Iterator接口结合起来演示如何获取集合中的元素。
代码演示,
代码5:
import java.util.*;
class CollectionDemo5
{
public static void main(String[] args)
{
ArrayList al = new ArrayList();
//为了掩饰方便这里我们仅添加字符串对象元素
al.add("Strng1");
al.add("Strng2");
al.add("Strng3");
//通过iterator方法获取到该ArrayList集合对象的迭代器对象
Iterator it = al.iterator();
/*
开启一个while循环
将hasNext返回值作为循环结束条件
循环获取集合中的元素,直到没有元素可取
*/
while(it.hasNext())
{
//通过next方法获取到集合中的下一个元素,并打印
System.out.println(it.next());
}
}
}
运行结果为:
Strng1
Strng2
Strng3
仅从运行结果来看,似乎与直接打印集合对象的结果没有什么区别,都可以将集合中的元素按顺序打印在控制台。但是,通过迭代器的方式,我们可以获取集合中的元素对象,并操作这些对象,这是最大区别。
这里需要大家注意的是:利用迭代器,并通过循环遍历集合中元素的过程中,每次循环只能调用一次next方法获取一个元素,相反,如果在一次循环中,重复调用next,可能会发生NoSuchElementException异常,例如下面的代码,
代码6:
import java.util.*;
class CollectionDemo6
{
public static void main(String[] args)
{
ArrayList al = new ArrayList();
al.add("String1");
al.add("String2");
al.add("String3");
al.add("String4");
al.add("String5");
Iteratorit = al.iterator();
while(it.hasNext())
{
//一次获取两个元素
System.out.println(it.next()+"......"+it.next());
}
}
}
运行结果为:
String1......String2
String3......String4
Exception in thread "main"java.util.NoSuchElementException
atjava.util.ArrayList$Itr.next(ArrayList.java:839)
at test13.ArrayListDemo.main(ArrayListDemo.java:21)
上述代码在元素数量为偶数时是可以正常运行的,但是如果元素数量为奇数,最后一次循环获取第二个元素时,就会抛出NoSuchElementException异常,这是因为最后一次判断hasNext方法时,“String4”后面确实有“String5”元素,循环可以正常执行,但是第一次调用next方法获取完“String5”以后,再次调用next方法就会出现问题——后面没有元素可取了。所以大家要注意,在无法明确元素数量时,要尽可能保证,判断一次hasNext,就调用一次next获取一个元素。
此外,对上述代码进行如下改进,可以进一步优化内存,
代码7:
//仅呈现迭代器部分代码
for(Iterator it = al.iterator();it.hasNext(); )
{
System.out.println(it.next());
}
在第一个分号前初始化一个迭代器对象,第二个分号前定义循环结束条件,第二个分号后代码缺省。这样书写代码不仅简化了书写,而且有效控制了迭代器对象(Iterator对象)的生命周期——for循环一结束迭代器对象也将消失,不再占用内存,起到内存优化的作用,建议大家使用。
(2) 迭代器
对于迭代器,大家可能会对“迭代”两个字感到疑惑,但目前不必去深究这个称呼的由来,大家只要知道迭代器是用于从容器中获取元素的工具即可。
那么为什么要煞费苦心的通过一个对象去完成取出元素的动作,而不是通过一个方法呢?这是因为,集合类有多种多样,正像上文提到的那样,这些集合类之间最大的区别就在于,底层应用的数据结构不同,那么对应的获取元素的动作也会有区别,有些集合获取元素的方式可能非常复杂,每次从头定义一些方法去实现这个动作是非常麻烦的,因此Java语言工程师们,就定义了Iterator这样一个接口,并在每种集合类内部定义一个实现Iterator接口的内部类,专门用于从该种集合中获取元素,虽然都是Iterator接口的实现子类,但是底层实现的原理随不同的集合类而不同,而这种不同就体现在next方法、hasNext方法以及remove方法的实现原理上。因为每个集合类的迭代器都实现了Iterator接口,所以无论哪种集合类对象通过iterator方法返回的迭代器对象,都可以通过Iterator类型变量接收,非常的方便。
另外,如果有朋友对为什么要将迭代器类定义为内部类有疑惑的话,其实可以这样理解:既然元素都是存储在容器的内部,那么存在于该容器内部,并专为该容器设计的工具,将会更为方便而恰当地获取到元素。希望下面这个图可以帮助大家理解,迭代器的定义原理。
虚线框表示某种集合类,而其内部的实现框表示迭代器类,向上的箭头表示与外部接口Iterator的实现关系。通过这样的设计,即使日后新定义了一个集合类,只要使其内部的迭代器类实现统一的规则——Iterator接口,并复写其中的三个方法,就可以使用相同的代码,从不同的集合中取出元素。
在这里请允许我引用毕老师的经典比喻,因为这个比喻太过经典,因此,我必须再次强调。很多朋友都去过电玩城,电玩城里面一般都会有这样一种游戏:投币抓玩偶。玩偶都是存放在一个透明塑料容器中,就相当于一个集合;里面的玩偶就相当于一个个元素;而安装在容器内用于抓玩偶的夹子就相当于迭代器。玩家通过容器对外提供的夹子,来抓取玩偶。
有兴趣的朋友可以去查阅AbstractList类的源代码(该类是ArrayList类的父类,ArrayList类的iterator方法就是从这个类继承而来)。源代码中定义了一个名为Itr的内部类,该类实现了Iterator接口,并复写了next、hasNext以及remove方法。而在AbstractList类iterator方法中返回了Itr对象,也是迭代器对象。