集合概述
Java中的集合就像一个容器,专门用来存储Java对象(实际上是对象的引用,但习惯上称为对象),这些对象可以是任意的数据类型,并且长度可变。其中,这些集合类都位于java.util包中,在使用时一定要注意导包的问题,否则会出现异常。
集合按照其存储结构可以分为两大类,即单列集合Collection和双列集合Map,这两种集合的特点具体如下:
● Collection:单列集合的根接口,用于存储一系列符合某种规则的元素。
Collection集合有两个重要的子接口,分别是List和Set。其中,List集合的特点是元素有序、可重复;Set集合的特点是元素无序并且不可重复。List接口的主要实现类有ArrayList和LinkedList,Set接口的主要实现类有HashSet和TreeSet。
● Map:双列集合的根接口,用于存储具有键(Key)、值(Value)映射关系的元素。
Map集合中每个元素都包含一对键值,并且Key是唯一的,在使用Map集合时可以通过指定的Key找到对应的Value。例如,根据一个学生的学号就可以找到对应的学生。Map接口的主要实现类有HashMap和TreeMap。
从上面的描述可以看出Java中提供了丰富的集合类库,为了便于初学者进行系统地学习,接下来通过一张图来描述整个集合的核心继承体系,如图1所示。
图1 集合体系核心架构图
图1中列出了Java开发中常用的一些集合类,其中,虚线框里填写的都是接口类型,而实线框里填写的都是具体的实现类。
Collection接口
Collection是所有单列集合的根接口,因此在Collection中定义了单列集合(如List和Set)的一些通用方法,这些方法可用于操作所有的单列集合,如表1所示。
表1 Collection接口的主要方法
方法声明 | 功能描述 |
---|---|
boolean add(Object o) | 向集合中添加一个元素 |
boolean addAll(Collection c) | 将指定集合c中的所有元素添加到该集合中 |
void clear() | 删除该集合中的所有元素 |
boolean remove(Object o) | 删除该集合中指定的元素 |
boolean removeAll(Collection c) | 删除该集合中包含指定集合c中的所有元素 |
boolean isEmpty() | 判断该集合是否为空 |
boolean contains(Object o) | 判断该集合中是否包含某个元素 |
boolean containsAll(Collection c) | 判断该集合中是否包含指定集合c中的所有元素 |
Iterator iterator() | 返回在该集合的元素上进行迭代的迭代器(Iterator),用于遍历该集合所有元素 |
int size() | 获取该集合元素个数 |
Stream<E> stream() | 将集合源转换为有序元素的流对象(JDK 8新方法) |
表1中列举了单列集合根接口Collcetion中的一些主要方法,其中stream()方法是JDK 8新增的,用于对集合元素进行聚合操作,针对该方法,在后续小节将会详细讲解。
另外表1中列举的Collcetion集合的主要方法都来自Java API文档,初学者可以通过查询API文档来学习更多有关Collcetion集合方法的具体用法,此处列出这些方法,只是为了方便后面的学习。
List接口
List接口简介
List接口继承自Collection接口,是单列集合的一个重要分支,习惯性的会将实现了List接口的对象称为List集合。在List集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引(类似于数组中的元素角标)来访问集合中的指定元素。另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些操作集合的特有方法,如表1所示。
表1 List集合常用方法
方法声明 | 功能描述 |
---|---|
void add(int index,Object element) | 将元素element插入在List集合的指定索引位置 |
boolean addAll(int index,Collection c) | 将集合c包含的所有元素插入到List集合的指定索引位置 |
Object get(int index) | 返回集合索引index处的元素 |
Object remove(int index) | 删除index索引处的元素 |
Object set(int index, Object element) | 将索引index处元素替换成element元素,并将替换后的元素返回 |
int indexOf(Object o) | 返回对象o在List集合中首次出现的位置索引 |
int lastIndexOf(Object o) | 返回对象o在List集合中最后一次出现的位置索引 |
List subList(int fromIndex, int toIndex) | 返回从索引fromIndex(包括)到 toIndex(不包括)处所有元素集合组成的子集合 |
Object[] toArray() | 将集合元素转换为数组 |
default void sort(Comparator<? super E> c) | 根据指定的比较器规则对集合元素排序(JDK 8新方法) |
表1中列举了List集合中的常用方法,所有的List实现类都可以调用这些方法来对集合元素进行操作。其中sort(Comparator<? super E> c)方法是JDK 8新增的,用于对集合元素进行排序操作,该方法的参数是一个接口类型的比较器Comparator,可以通过前面讲解的Lambda表达式传入一个函数式接口作为参数,来指定集合元素的排序规则。
ArrayList集合
ArrayList是List接口的一个实现类,它是程序中最常见的一种集合。在ArrayList内部封装了一个长度可变的数组对象,当存入的元素超过数组长度时,ArrayList会在内存中分配一个更大的数组来存储这些元素,因此可以将ArrayList集合看作一个长度可变的数组。
正是由于ArrayList内部的数据存储结构是数组形式,在增加或删除指定位置的元素时,会创建新的数组,效率比较低,因此不适合做大量的增删操作。但是,这种数组结构允许程序通过索引的方式来访问元素,因此使用ArrayList集合在遍历和查找元素时显得非常高效。
ArrayList集合中大部分方法都是从接口Collection和List继承过来的,接下来通过一个案例来学习如何使用ArrayList集合的方法来存取元素,如文件1所示。
文件1 Example01.java
1 import java.util.ArrayList;
2 public class Example01 {
3 public static void main(String[] args) {
4 // 创建ArrayList集合
5 ArrayList list = new ArrayList();
6 // 向集合中添加元素
7 list.add("stu1");
8 list.add("stu2");
9 list.add("stu3");
10 list.add("stu4");
11 System.out.println("集合的长度:" + list.size());
12 System.out.println("第2个元素是:" + list.get(1));
13 }
14 }
运行结果如图1所示。
图1 运行结果
文件1中,首先通过“new ArrayList()”语句创建了一个空的ArrayList集合,接着调用add(Object o)方法向ArrayList集合中添加了4个元素,然后调用size()方法获取集合中元素个数,最后通过调用ArrayList的get(int index)方法取出指定索引位置的元素。
从图1可以看出,索引位置为1的元素是集合中的第二个元素,这说明集合和数组一样,索引的取值是从0开始的,最后一个索引是size-1,在访问元素时一定要注意索引不可超出此范围,否则会抛出角标越界异常IndexOutOfBoundsException。
注意:
1.在编译文件1时,会得到如图1所示的警告,意思是说在使用ArrayList集合时并没有显示的指定集合中存储什么类型的元素,会产生安全隐患,这涉及到泛型安全机制的问题。与泛型相关的知识将在后面的章节详细讲解,现在无需考虑。
图2 运行结果
2.在编写程序时,不要忘记使用类似于“import java.util.ArrayList;”语句导包,否则程序将会编译失败,显示类找不到,如图6-4所示。要解决此问题,只需单击图6-4所示错误小窗口中的第一行“Import’ArrayList’(java.util)”链接即可,这样Eclipse就会自动导入ArrayList的包。另外在后面的案例中可能会大量的用到集合类,除了可以使用上述方式导入指定集合类所在的包外,为了方便,程序中还可以统一使用import java.util.;来进行导包,其中为通配符,整个语句的意思是将java.util包中的内容都导入进来。
图3 编译错误
LinkedList集合
ArrayList集合在查询元素时速度很快,但在增删元素时效率较低,为了克服这种局限性,可以使用List接口的另一个实现类LinkedList。LinkedList集合内部包含有两个Node类型的first和last属性维护一个双向循环链表,在链表中的每一个元素都使用引用的方式来记住它的前一个元素和后一个元素,从而可以将所有的元素彼此连接起来。当插入一个新元素时,只需要修改元素之间的这种引用关系即可,删除一个节点也是如此。正因为这样的存储结构,所以LinkedList集合对于元素的增删操作表现出很高的效率,LinkedList集合添加元素和删除元素的过程如图1所示。
图1 双向循环链表结构图
在图1中,通过两张图描述了LinkedList集合新增元素和删除元素的过程。其中,左图为新增一个元素,图中的元素1和元素2在集合中彼此为前后关系,在它们之间新增一个元素时,只需要让元素1记住它后面的元素是新元素,让元素2记住它前面的元素为新元素就可以了。右图为删除元素,要想删除元素1和元素2之间的元素3,只需要让元素1与元素2变成前后关系就可以了。
LinkedList集合除了从接口Collection和List中继承并实现了集合操作方法外,还专门针对元素的增删操作定义了一些特有的方法,如表1所示。
表1 LinkedList中的特有方法
方法声明 | 功能描述 |
---|---|
void add(int index, E element) | 在此列表中指定的位置插入指定的元素。 |
void addFirst(Object o) | 将指定元素插入集合的开头 |
void addLast(Object o) | 将指定元素添加到集合的结尾 |
Object getFirst() | 返回集合的第一个元素 |
Object getLast() | 返回集合的最后一个元素 |
Object removeFirst() | 移除并返回集合的第一个元素 |
Object removeLast() | 移除并返回集合的最后一个元素 |
boolean offer(Object o) | 将指定元素添加到集合的结尾 |
boolean offerFirst(Object o) | 将指定元素添加到集合的开头 |
boolean offerLast(Object o) | 将指定元素添加到集合的结尾 |
Object peek() | 获取集合的第一个元素 |
Object peekFirst() | 获取集合的第一个元素 |
Object peekLast() | 获取集合的最后一个元素 |
Object poll() | 移除并返回集合的第一个元素 |
Object pollFirst() | 移除并返回集合的第一个元素 |
Object pollLast() | 移除并返回集合的最后一个元素 |
void push(Object o) | 将指定元素添加到集合的开头 |
Object pop() | 移除并返回集合的第一个元素 |
表1中,列出的方法主要针对集合中的元素进行增加、删除和获取操作,接下来通过一个案例来学习LinkedList中常用方法的使用,如文件1所示。
文件1 Example02.java
1 import java.util.LinkedList;
2 public class Example02 {
3 public static void main(String[] args) {
4 // 创建LinkedList集合
5 LinkedList link = new LinkedList();
6 // 1、添加元素
7 link.add("stu1");
8 link.add("stu2");
9 System.out.println(link); // 输出集合中的元素
10 link.offer("offer"); // 向集合尾部追加元素
11 link.push("push"); // 向集合头部添加元素
12 System.out.println(link); // 输出集合中的元素
13 // 2、获取元素
14 Object object = link.peek();//获取集合第一个元素
15 System.out.println(object); // 输出集合中的元素
16 // 3、删除元素
17 link.removeFirst(); // 删除集合第一个元素
18 link.pollLast(); // 删除集合最后一个元素
19 System.out.println(link);
20 }
21 }
运行结果如图2所示。
图2 运行结果
文件1中,首先创建了一个LinkedList集合,接着分别使用add()、offer()、push()方法向集合中插入元素,然后使用peek()方法获取了集合的第一个元素,最后分别使用removeFirst()、pollLast()方法将集合中指定位置的元素移除,这样就完成了元素的增、查、删操作。由此可见,使用LinkedList对元素进行增删操作是非常便捷的。
Collection集合遍历
Iterator遍历集合
Iterator接口是Java集合框架中的一员,但它与Collection、Map接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。
接下来通过一个案例来学习如何使用Iterator迭代集合中的元素,如文件1所示。
文件1 Example03.java
1 import java.util.ArrayList;
2 import java.util.Iterator;
3 public class Example03 {
4 public static void main(String[] args) {
5 // 创建ArrayList集合
6 ArrayList list = new ArrayList();
7 // 向该集合中添加字符串
8 list.add("data_1");
9 list.add("data_2");
10 list.add("data_3");
11 // 获取Iterator对象
12 Iterator iterator = list.iterator();
13 // 判断集合中是否存在下一个元素
14 while (iterator.hasNext()) {
15 Object obj = iterator.next(); // 取出ArrayList集合中的元素
16 System.out.println(obj);
17 }
18 }
19 }
运行结果如图1所示。
图1 运行结果
文件1演示的是Iterator遍历集合的整个过程。当遍历元素时,首先通过调用ArrayList集合的iterator()方法获得迭代器对象,然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。需要注意的是,在通过next()方法获取元素时,必须保证要获取的元素存在,否则,会抛出NoSuchElementException异常。
Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了让初学者能更好地理解迭代器的工作原理,接下来通过一个图例来演示Iterator对象迭代元素的过程,如图2所示。
图2 遍历元素过程图
在图2中,在调用Iterator的next()方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的next()方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next()方法时,迭代器的索引会指向第二个元素并将该元素返回,以此类推,直到hasNext()方法返回false,表示到达了集合的末尾终止对元素的遍历。
foreach遍历集合
虽然Iterator可以用来遍历集合中的元素,但写法上比较繁琐,为了简化书写,从JDK 5开始,提供了foreach循环。foreach循环是一种更加简洁的for循环,也称增强for循环。foreach循环用于遍历数组或集合中的元素,其具体语法格式如下:
for(容器中元素类型 临时变量 :容器变量) {
// 执行语句
}
从上面的格式可以看出,与for循环相比,foreach循环不需要获得容器的长度,也不需要根据索引访问容器中的元素,但它会自动遍历容器中的每个元素。接下来通过一个案例对foreach循环进行详细讲解,如文件1所示。
文件1 Example04.Java
1 import java.util.ArrayList;
2 public class Example04 {
3 public static void main(String[] args) {
4 ArrayList list = new ArrayList();
5 list.add("data_1");
6 list.add("data_2");
7 list.add("data_3");
8 // 使用foreach循环遍历集合
9 for (Object obj : list) {
10 System.out.println(obj); // 取出并打印集合中的元素
11 }
12 }
13 }
运行结果如图1所示。
图1 运行结果
通过文件1可以看出,foreach循环遍历集合的语法非常简洁,没有循环条件,也没有迭代语句,所有这些工作都交给JVM去执行了。foreach循环的次数是由容器中元素的个数决定的,每次循环时,foreach中都通过变量将当前循环的元素记住,从而将集合中的元素分别打印出来。
脚下留心:
1.foreach循环虽然书写起来很简洁,但在使用时也存在一定的局限性。当使用foreach循环遍历集合和数组时,只能访问集合中的元素,不能对其中的元素进行修改,接下来以一个String类型的数组为例来进行演示,如文件2所示。
文件2 Example05.java
1 public class Example05 {
2 static String[] strs = { "aaa", "bbb", "ccc" };
3 public static void main(String[] args) {
4 // 1、foreach循环遍历数组
5 for (String str : strs) {
6 str = "ddd";
7 }
8 System.out.println("foreach循环修改后的数组:" + strs[0] + ","
9 + strs[1] + "," + strs[2]);
10 // 2、for循环遍历数组
11 for (int i = 0; i < strs.length; i++) {
12 strs[i] = "ddd";
13 }
14 System.out.println("普通for循环修改后的数组:" + strs[0] + ","
15 + strs[1] + "," + strs[2]);
16 }
17 }
运行结果如图2所示。
图2 运行结果
在文件2中,分别使用foreach循环和普通for循环去修改数组中的元素。从运行结果可以看出foreach循环并不能修改数组中元素的值。其原因是第6行代码中的str = "ddd"只是将临时变量str指向了一个新的字符串,这和数组中的元素没有一点关系。而在普通for循环中,是可以通过索引的方式来引用数组中的元素并将其值进行修改的。
2.在使用Iterator迭代器对集合中的元素进行迭代时,如果调用了集合对象的remove()方法去删除元素,会出现异常。接下来通过一个案例来演示这种异常,如文件3所示。
文件3 Example06.java
1 import java.util.ArrayList;
2 import java.util.Iterator;
3 public class Example06 {
4 public static void main(String[] args) {
5 ArrayList list = new ArrayList();
6 list.add("Jack");
7 list.add("Annie");
8 list.add("Rose");
9 list.add("Tom");
10 Iterator it = list.iterator(); // 获得Iterator对象
11 while (it.hasNext()) { // 判断该集合是否有下一个元素
12 Object obj = it.next(); // 获取该集合中的元素
13 if ("Annie".equals(obj)) { // 判断该集合中的元素是否为Annie
14 list.remove(obj); // 删除该集合中的元素
15 }
16 }
17 System.out.println(list);
18 }
19 }
运行结果如图3所示。
图3 运行结果
文件3在运行时出现了并发修改异常ConcurrentModificationException。这个异常是迭代器对象抛出的,出现异常的原因是集合中删除了元素会导致迭代器预期的迭代次数发生改变,导致迭代器的结果不准确。
为了解决上述问题,可以采用两种方式,具体如下:
第一种方式:从业务逻辑上讲只想将元素Annie删除,至于后面还有多少元素我们并不关心,所以只需找到该元素后跳出循环不再迭代即可,也就是在第14行代码下面增加一个break语句,代码如下:
if ("Annie".equals(obj)) {