一.单元概述
Java为一组对象的处理提供了一套完整的,从接口到抽象类,再到实现类的体系结构,通常称作集合框架。本章我们将学习Java中的集合类,包括集合概述,Java中集合框架层次结构。学习集合框架的Collection接口,和它的子接口List和Set,以及它们的实现类各自的特点、适用的场景。还将学习一个提供了集合通用操作的类Collections,此外我们还会学习一个与集合密切相关的接口Map,以及它的实现类。最后我们将学习使用集合时常见的异常以及集合中泛型的使用。
二,教学重点与难点
重点:
(1) Java中的集合类
(2) Collection接口
(3) Set接口及实现类
(4) List接口及实现类
(5) 集合的遍历
(6) Collections类
(7) Map接口及实现类
(8) 相关实现类的比较
(9) 泛型的使用
难点:
(1) Set接口及实现类
(2) List接口及实现类
(3) 集合的遍历
(4) 相关实现类的比较
(5) 泛型的使用

2.1Java 中集合类

2.1.1 集合概述

前面我们学习了数组,可以使用数组组织多个相同数据类型的数据。Java数组的长度是固定的,在同一个数组中只能存放相同类型的数据。数组可以存放基本类型的数据,也可以存放引用类型的数据。

在创建Java数组时,必须明确指定数组的长度,数组一旦创建,其长度不能被改变。在许多应用场合,一组数据的数目不是固定的,比如一个单位的员工数目是变化的,有新的员工入职进来,也有老的员工跳槽离职。

为了使程序能方便地存储和操作数目不固定的一组数据,Java中提供了集合框架,所有Java集合类都位于java.util包中。Java中集合类是用来存放对象的,集合相当于一个容器,里面包容着一组对象,其中的每个对象作为集合的一个元素出现。与数组不同,Java集合中不能存放基本数据类型,而只能存放对象的引用。Java数组与集合的

区别主要有两点:

  • 数组是容器,它的长度是固定的,不会自动扩充,数组中既可以存放基本数据类型也能存放引用数据类型的引用。
  • 集合也是一种容器,它的长度是可变的,集合中只能存放引用数据类型的引用。

如图2-1所示,Java集合主要分为以下3种类型。
10.集合-高级篇 - 图1

2.1.2 Java中集合框架层次结构

Java集合框架为我们提供了处理一组对象的标准方式,这些标准在集合框架中被设计为一系列的接口。同时,集合框架还提供了这些接口的实现类。图2-2是Java集合框架的层次结构,图中虚线表示的是接口,实线表示的是类。

上图中的虚线框表示的是接口,接口具体包括:

  • Collection接口:是集合层次中的一个根接口,Collection 表示一组对象,这些对象也称为 Collection 的元素。一些Collection 允许有重复的元素,而另一些则不允许。一些Collection 是有序的,而另一些则是无序的,JDK 不提供此接口的任何直接实现,Collection接口是List接口和Set接口的父接口。
  • List接口:有序、可包含重复元素的Collection,通常被称为链表,此接口的用户可以对列表中每个元素的插入位置进行精确地控制,用户也可以根据元素的整数索引访问元素,并搜索链表中的元素。
  • Set接口:无序、不包含重复元素的Collection,通常被称为集合。
  • Map接口:组织映射数据,表示很多数据,每个数据都会包含两部分,一部分是数据,另一部分是键,每个数据称为键/值对(key/value)。
  • Iterator接口:对Collection 进行迭代的迭代器接口。

上图中的实线框表示是实际要使用的类,这些类比较多,也是我们需要重点掌握的,在后面的章节中会详细介绍。

2.2Collection接口

Collection接口用来存放一组称为元素的对象,一个Collection中可以放不同类型的数据,它是Set接口和List接口的父接口。其中元素是否有特定的顺序以及是否允许重复,取决于它的实现类。
(1) 子接口Set:无序的集合,不允许重复。
Set接口的实现类:HashSet 
(2) 子接口List:有序的集合,允许重复。
List接口的实现类:ArrayList、LinkedList
图2-3是Collection接口的类图,列出了其子接口及实现类,还有接口中的常用方法。
10.集合-高级篇 - 图2

2.2.1 Collection接口

最顶层接口就是Collection,表示一个集合。因为是接口,所以主要考虑它的方法,这个接口中定义的方法是所有实现该接口的类都应该实现的。因为Collection描述的是集合,所以它的方法都是与集合操作相关的方法。
(1)、第一类方法,向集合中添加对象的方法
可以添加一个,也可以添加多个,添加多个也就是把另外一个集合的元素添加进来,下面的两个方法是添加对象的方法:
public boolean add(Object o):向集合中添加参数指定的元素。
public boolean addAll(Collection c) :向集合中添加参数指定的所有元素。
(2)、第二类方法,从集合中删除元素的方法
可以删除一个,可以删除多个,还可以删除所有的元素,此外还有一个特殊的,删除某些元素之外的所有元素,所以对应的方法也有四个:
public boolean remove(Object o) :删除指定的某个元素。
public boolean removeAll(Collection c) :删除指定的多个元素。
public void clear():删除所有的元素。
public boolean retainAll(Collection c) :只保留指定集合中存在的元素,其他的都删除,相当于取两个集合的交集。
(3)、第三类方法,判断集合中元素的方法
public boolean isEmpty():用于判断集合是否是空的。
public boolean contains(Object o) :判断是否包含指定的元素。
public boolean containsAll(Collection c) :判断是否包含指定的多个元素。
public int size():用于获取集合中元素的个数。
(4)、第四类方法,与其它类型的对象进行转换的方法
public Iterator iterator():转换成迭代器,方便集合中元素的遍历。
public Object[] toArray():返回一个包含所有元素的对象数组,也是方便集合中元素的遍历。
通常在管理集合的过程中,使用集合本身提供的方法,但是遍历集合最好先转换成迭代器或者数组,这样访问比较方便,并且效率比较高。
(5)、第五类方法,比较通用的方法
public boolean equals(Object o) :判断是否与另外一个对象相同。
public int hashCode():返回集合的哈希码。

2.2.2 List接口及其实现类

List接口继承了Collection接口,用来包含一组有序有重复的对象。List中的每个元素都对应一个整数型的序号,记载其在容器中的位置,可以根据序号存取容器中的元素。List就是通常所说的链表,是一种特殊的集合,集合中的元素是有序的,所以多了一些与顺序相关的方法。这里只介绍增加的方法。
(1)、第一类方法,在指定的位置上添加元素
public void add(int index , Object o):第一个参数表示要添加的元素的位置,从0开始。
public boolean addAll(int index , Collection c) :第一个参数表示位置,如果不指定位置,默认在最后添加。
(2)、第二类方法,删除指定位置的元素
public Object remove(int index) :参数用于指定要删除的元素的位置。
(3)、第三类方法,获取某个元素或者获取某些元素
public Object get(int index) :获取指定位置的元素。
public List subList(int fromIndex,int toIndex) :获取从fromIndex到toIndex这些元素,包括fromIndex,不包括toIndex。
(4)、第四类方法,查找某个元素
public int indexOf(Object o) :查找元素在集合中第一次出现的位置,并返回这个位置,如果返回值为-1,表示没有找到这个元素。
public int lastIndexOf(Object o) :查找元素在集合中最后一次出现的位置。
(5)、第五类方法,修改元素的方法
public Object set(int index , Object o) :用第二个参数指定的元素替换第一个参数指定位置上的元素。
(6)、第六类方法,转换成有顺序的迭代器:
public ListIterator listIterator():把所有元素都转换成有顺序的迭代器。
public ListIterator listIterator(int index) :从index开始的所有元素进行转换。
List与Set相比,主要是增加了元素之间的顺序关系,并且允许元素重复。List有三种主要的实现类:

  • ArrayList 
  • LinkedList 
  • Vector

1 . ArrayList实现类
ArrayList是数据结构中数组的Java实现,ArrayList是一种动态数组,它是java.util包中的一个类。原则上所有的对象都可以加入到ArrayList里,但通常为了使用方便,一般可以通过泛型()限定加入到ArrayList中的元素类型以保证加入的都是相同类型的元素。
该类的构造方法有3种:
ArrayList():构造一个初始容量为10的空的链表;
ArrayList(Collection<? extends E> c):使用1个已经存在的集合构造一个链表,集合中的元素在新的链表中的顺序由集合的iterator()方法决定。
ArrayList(int initialCapacity) :构造一个由参数指定初始化空间大小的链表。
下面的代码分别展示了3种用法:
ArrayList list1 = new ArrayList();
list1.add(“user1”);
list1.add(“user2”);
ArrayList list2 = new ArrayList(list1);
ArrayList list3 = new ArrayList(8);
其中list2使用list1中的元素进行初始化。注意在使用ArrayList的时候应该是指定元素的类型。这里使用了泛型,泛型的使用我们将会在2.6中做详细介绍。
ArrayList其它的主要方法还包括:
(1)、向ArrayList中添加对象
可以在最后添加,也可以在指定的位置添加对象。可以添加一个,可以添加多个,添加多个也就是把另外一个集合的元素添加进来。
public void add(int index,Object o):第一个参数表示要添加的元素的位置,从0开始。
public boolean addAll(int index,Collection c) :第一个参数表示位置,如果不指定位置,默认在最后添加。
public boolean add(Object o) :在链表的最后添加参数指定的元素。
public boolean addAll(Collection c) :在链表最后添加参数指定的所有元素。
下面的代码展示了这些方法的应用:
list1.add(“user3”);
list1.addAll(list2);
list1.add(0,”user0”);
运行后集合中的元素为:[user0, user1, user2, user3, user1, user2]
(2)、删除特定的元素
可以删除一个,可以删除多个,还可以删除所有的元素,此外还有一个特殊的,删除某些元素之外的所有元素,所以对应的方法也有四个:
public boolean remove(Object o):删除指定的某个元素。
public boolean removeAll(Collection c) :删除指定的多个元素。
public void clear():删除所有的元素。
public boolean retainAll(Collection c) :只保留指定集合中存在的元素,其他的都删除,相当于取两个集合的交集。
public Object remove(int index) :参数用于指定要删除的元素的位置。
下面的代码删除了user1:
list1.remove(“user1”);
注意:这里只删除了第一个出现的user1。
(3)、获取某个元素或者获取某些元素
可以获取某个位置的单个元素,也可以获取多个元素。
public Object get(int index):获取指定位置的元素。
public List subList(int fromIndex,int toIndex) :获取从fromIndex到toIndex这些元素,包括fromIndex,不包括toIndex。
要获取第三个元素可以使用下面的代码:
String str = list1.get(2);
结果是:user3
当前集合中的元素为:user0, user2, user3, user1, user2。
(4)、查找某个元素
可以根据位置查找集合中的对象,也可以判断结合中是有对象,以及是否是空的,元素的个数等。
public int indexOf(Object o):查找元素在集合中第一次出现的位置,并返回这个位置,如果返回值为-1,表示没有找到这个元素。
public int lastIndexOf(Object o) :查找元素在集合中最后一次出现的位置。
public boolean isEmpty():用于判断集合是否是空的。
public boolean contains(Object o) :判断是否包含指定的元素。
public boolean containsAll(Collection c) :判断是否包含指定的多个元素。
public int size(),用于获取集合中元素的个数。
下面的代码用于查找user1第一次出现和最后一次次出现的位置。
System.out.println(list1.indexOf(“user2”));
System.out.println(list1.lastIndexOf(“user2”));
得到的结果:
1
4
当前集合中的元素为:user0, user2, user3, user1, user2。
(5)、修改元素的方法
public Object set(int index,Object o):用第二个参数指定的元素替换第一个参数指定位置上的元素。
下面的代码把第二个元素修改user4:
list1.set(1,”user4”);
集合中原来的元素:user0, user2, user3, user1, user2。
修改后的元素为:user0, user4, user3, user1, user2。
(6)、转换成其它对象
public ListIterator listIterator():把所有元素都转换成有顺序的迭代器。
public ListIterator listIterator(int index) :从index开始的所有元素进行转换。
public Iterator iterator():转换成迭代器,方便集合中元素的遍历。
public Object[] toArray():转换成数组,也是方便集合中元素的遍历。
(7)、ArrayList的遍历
可以采用下面的3种方法进行遍历。
方法1:
for(int i=0;i System.out.println(list1.get(i));
}
方法2:
Object o[] = list1.toArray();
for(int i=0;i String temp = (String)o[i];
System.out.println(temp);
}
方法3:
Iterator i = list1.iterator();
while(i.hasNext()){
String temp = i.next();
System.out.println(temp);
}
通常在管理集合的过程中使用集合本身提供的方法,但是遍历集合最好先转换成迭代器或者数组,这样访问比较方便,并且效率比较高。
例**2-1】ArrayList的使用。
import  java.util.ArrayList;
import  java.util.Iterator;
import  java.util.List;
public class ListDemo {
public  static  void main(String args[]) {
List li = new ArrayList();
li.add(“one”);
li.add(“two”);
li.add(3);
li.add(new Float(4.0F));
li.add(“two”);
li.add(new Integer(3));
System.out.println(li);
}
}
程序运行结果:
[one, two, 3, 4.0, two, 3]
程序分析:ArrayList是个动态数组,本例用来存放动态添加字符串。
2.LinkedList实现类**
LinkedList是数据结构中链表的Java实现,相对于ArrayList来说,LinkedList最主要的功能增强是可以在List的头部和尾部添加、删除、取得元素,直接提供了这些方法的实现。所以它可以非常方便的实现我们数据结构中的常见的Stack(栈)、queue(队列)等。LinkedList类的构造方法如下:
LinkedList 变量名 = new LinkedList() ;
LinkedList 变量名 = new LinkedList(Collection c) ;
表2-2列出了LinkedList的常用方法。
表2-1 LinkedList的常用方法

方法 含义
void addFirst(Object o) 将给定元素插入此列表的开头
void addLast(Object o) 将给定元素追加到此列表的结尾
Object getFirst() 返回此列表的第一个元素
Object getLast() 返回此列表的最后一个元素
Object removeFirst() 移除并返回此列表的第一个元素
Object removeLast() 移除并返回此列表的最后一个元素
  1. public class LinkedListTest {
  2. public static void main(String[] args) {
  3. LinkedList<String> myList = new LinkedList<String>();
  4. myList.add("1");
  5. myList.add("2");
  6. myList.add("3");
  7. myList.add("4");
  8. myList.add("5");
  9. System.out.println("链表的第一个元素是 : " +  myList.getFirst());
  10. System.out.println("链表的最后一个元素是 : " +  myList.getLast());
  11. }
  12. }
  13. 程序运行结果:
  14. 链表的第一个元素是 : 1
  15. 链表的最后一个元素是 : 5

程序分析
LinkedList类是双向列表,列表中的每个节点都包含了对前一个和后一个元素的引用,本例获取链表的第一个元素和最后一个元素使用getFirst()与getLast()方法。

2.2.3 Iterator接口

Iterator对象称作迭代器,用来方便的实现对容器内的元素进行遍历操作。所有实现了Collection接口的集合类都有一个iterator()方法,该方法返回一个实现了Iterator接口的对象。Iterator对象实现了统一的一个用来遍历Collection中对象的方法。Iterator是为遍历而设计,能够从集合中取出元素和删除元素,但是没有添加元素的功能。Iterator的功能上比较简单,使用时,只能单向移动。Iterator接口及其子接口的主要方法如图2-4所示。
10.集合-高级篇 - 图3

  1. Iterator接口的定义如下:
  2. package java.util;
  3. public interface Iterator {
  4.   boolean hasNext();
  5.   Object next();
  6.   void remove();
  7. }

3个方法的作用如下:
hasNext():判断迭代器中是否有下一个元素,如果有,返回true,否则返回false。
next():用于得到下一个元素,返回值是Object类型,需要强制转换成自己需要的类型。
remove():用于删除元素。
关于迭代器的使用,通常是在得到它之后,对它进行遍历。
ListIterator接口是Iterator接口的子接口,相对于Iterator,它提供了更多方法。

  1. 【例2-5 使用ListIterator遍历元素
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import java.util.ListIterator;
  5. public class ListIteratorExercise {
  6. public static void main(String args[]) {
  7. List list = new ArrayList();
  8. // 为List赋值
  9. for (int i = 0; i < 6; i++) {
  10. list.add(new Integer(i));
  11. }
  12. //遍历输出每一个元素
  13. ListIterator lit = list.listIterator();
  14. while (lit.hasNext()) {
  15. System.out.print(lit.next() + " ");
  16. }
  17. System.out.println();
  18. ListIterator lit3s = list.listIterator();
  19. //逆序输出每个元素的值
  20. while (lit.hasPrevious()) {
  21. System.out.print(lit.previous() + " ");
  22. }
  23. }
  24. }
  25. 程序运行结果:
  26. 0 1 2 3 4 5 
  27. 5 4 3 2 1 0
  28. 程序分析:本例使用istIterator迭代器输出ArrayList中的元素。
  29. 【例2-6】使用ArrayList添加和删除元素,然后使用ListIterator遍历元素。
  30. import java.util.ArrayList;
  31. import java.util.ListIterator;
  32. public class ListIteratorDemo {
  33. public static void main(String args[]) {
  34. List li = new ArrayList();
  35. for (int i = 1; i < 6; i++) {
  36. li.add(new Integer(i));
  37. }
  38. ListIterator lit = li.listIterator();
  39. //在1和2之间插入6
  40. System.out.println(lit.nextIndex());
  41. lit.next();
  42. lit.add(new Integer(6));
  43. System.out.println(li);
  44. //删除6
  45. lit.previous();
  46. lit.remove();
  47. System.out.println(li);
  48. //将元素3替换成字符'A'
  49. lit.next();
  50. lit.next();
  51. lit.set(new Character('A'));
  52. System.out.println(li);
  53. }
  54. }
  55. 程序运行结果:
  56. 0
  57. [1, 6, 2, 3, 4, 5]
  58. [1, 2, 3, 4, 5]
  59. [1, 2, A, 4, 5]
  60. 程序分析:本例使用add()和remove()方法添加和删除元素,然后使用ListIterator遍历元素。

2.2.4增强型for循环

for-each循环是对for循环的增强,主要用来对集合类型对象的遍历,是JDK5.0之后才支持的,但是有一定的约束。
for-each循环的基本结构如下:
for (数据类型 变量名:数组名)  循环体语句
其中,变量的数据类型必须与数组的数据类型一致,变量按照从头到尾的顺序,每次迭代接收数组中的一个元素,循环不断重复,直到获得了数组中的所有元素为止。
循环体中的代码主要是对标识变量的操作。
如果使用I表示元素的类型,标识符变量使用i,表达式是exp,要执行的语句为statements,for-each循环的格式如下:
for (I i:exp){
   statements;
}
如果使用for循环结构,代码如下:
for(I i=exp.iterator();exp.hasNext();i=i.next()){
}

  1. 【例2-7】分别使用for循环为数组赋值,并分别使用for循环和for-each循环输出数组元素的值。
  2. public class ForEachTest {
  3. public static void main(String[] args) {
  4. int a[] = new int[5];
  5. //通过for循环为数组元素赋值
  6. for(int i=0;i<5;i++){
  7. a[i] = 2*i;
  8. }
  9. //通过for循环输出数组元素的值
  10. System.out.println("使用for循环输出数组元素的值");
  11. for(int i=0;i<5;i++){
  12. System.out.println(a[i]);
  13. }
  14. //通过for-each循环输出数组元素的值
  15. System.out.println("使用for-each循环输出数组元素的值");
  16. for(int i:a){
  17. System.out.println(i);
  18. }
  19. }
  20. }
  21. 程序运行结果:
  22. 使用for循环输出数组元素的值
  23. 0
  24. 2
  25. 4
  26. 6
  27. 8
  28. 使用for-each循环输出数组元素的值
  29. 0
  30. 2
  31. 4
  32. 6
  33. 8
  34. 【注意】for-each循环中的循环变量表示集合或者数组中的元素,而不是索引号。
  1. 【例2-8】使用for-each循环遍历ArrayList对象。
  2. import java.util.ArrayList;
  3. public class ForEachTest2 {
  4. public static void main(String[] args){
  5. //创建ArrayList对象,并赋值
  6.   List<String> users = new ArrayList<String>();
  7. for(int i=0;i<5;i++){
  8. users.add("user"+(i+1));
  9. }
  10. //使用for-each循环遍历ArrayList对象
  11. for(String user:users){
  12. System.out.println(user);
  13. }
  14. }
  15. }
  16. 程序运行结果:
  17. user1
  18. user2
  19. user3
  20. user4
  21. user5
  22. 程序分析:本例中for-each用来循环遍历ArrayList对象。
  23. 【例2-9】使用for-each与迭代器循环遍历ArrayList
  24. public class EnhanceFor {
  25. public static void main(String args[]){
  26. //遍历集合中的每一个元素
  27. List li = new ArrayList();
  28. for(int i=0;i<5;i++){
  29. li.add(i);
  30. }
  31. for(Object num:li){
  32. System.out.print(num);
  33. }
  34. System.out.println();
  35. for(Iterator it=li.iterator();it.hasNext();){
  36. Integer num = (Integer)it.next();
  37. System.out.print(num);
  38. }
  39. //遍历数组中的每个元素
  40. int[] arr = new int[5];
  41. for(int i=0;i<arr.length;i++){
  42. arr[i] = i+5;
  43. }
  44. System.out.println();
  45. for(int n:arr){
  46. System.out.print(n);
  47. }
  48. System.out.println();
  49. for(int i=0;i<arr.length;i++){
  50. int n = arr[i];
  51. System.out.print(n+" ");
  52. }
  53. }
  54. }
  55. 程序运行结果:
  56. 0 1 2 3 4 
  57. 0 1 2 3 4 
  58. 5 6 7 8 9 
  59. 5 6 7 8 9
  60. 程序分析:程序使用for-each和迭代器来遍历集合中的元素。在遍历集合时通常会使用增强的for循环,其用法如下:
  61. 1.首先增强for循环和iterator遍历的效果是一样的,也就说增强for循环的内部也就是调用iterator实现的,但是增强for循环有些缺点,例如不能在增强循环里动态的删除集合内容。不能获取下标等。
  62. 2.ArrayList由于使用数组实现,因此下标明确,最好使用普通循环。
  63. 3.而对于LinkedList由于获取一个元素,要从头开始向后找,因此建议使用增强for循环,也就是iterator
  64. 【例2-10】先把信息("0"、 "1"、 "2"、 "3"、 "4"5个字符串)存储到Vector对象中,然后通过迭代器对这个Vector对象进行遍历。
  65. import java.util.Vector;
  66. import java.util.Iterator;
  67. public class IteratorTest {
  68. public static void main(String[] args) {
  69. //定义一个Vector,通过循环向Vector添加5个元素
  70. Vector v = new Vector();
  71. for(int i=0;i<5;i++){
  72. v.add(String.valueOf(i));
  73. }
  74. //把Vector对象转换成迭代器对象
  75. Iterator i = v.iterator();
  76. //通过迭代器对象对Vector元素进行遍历
  77. while(i.hasNext()){
  78. String s = (String)i.next();
  79. System.out.print(s);
  80. }
  81. }
  82. }
  83. 程序运行结果:
  84. 0 1 2 3 4
  85. 程序分析:本程序先向Vector中添加5个元素,然后通过迭代器后进行遍历。

2.2.5 Set接口及其实现类

Set接口是Collection接口的子接口,表示集合,对实现该接口的对象有个要求,就是集合中的元素不允许重复,该接口与Collection接口基本一致,方法与Collection完全相同。Set接口的实现类有两个:

  • HashSet — HashSet的特性在于其内部对象的散列存取,即采用哈希技术。散列存储,又称hash存储,是一种力图将数据元素的存储位置与关键码之间建立确定对应关系的查找技术。散列法存储的基本思想是:由节点的关键码值决定节点的存储地址。散列技术除了可以用于查找外,还可以用于存储。
  • TreeSet — TreeSet存入的顺序跟存储的顺序不同,但是存储是按照排序存储的。

下面分别来介绍下这两个实现类的用法。
(1)、HashSet类
HashSet是实现Set接口的一个类,具有以下的特点:

  • 不能保证元素的排列顺序,顺序有可能发生变化。
  • HashSet不是同步的,如果多个线程同时访问一个Set,只要有一个线程修改Set中的值,就必须进行同步处理,通常通过同步封装这个Set的对象来完成同步,如果不存在这样的对象,可以使用Collections.synchronizedSet()方法完成。
  • Set s = Collections.synchronizedSet(new HashSet(…));
  • 元素值可以是null。

主要方法如下:
(1)、构造方法
提供了4个构造方法:
public HashSet();
public HashSet(Collection<? extends E> c);
public HashSet(int initialCapacity);
public HashSet(int initialCapacity,float loadFactor);
第1个方法创建初始化大小为16,加载因子为0.75的默认实例,第2个方法是以已经存在的集合对象中的元素为基础创建新的HashSet实例,第3个方法根据指定的初始化空间大小的实例,第4个方法在创建实例的时候不仅指出了初始化大小空间,同时也指出了加载因子。
下面的例子,分别采用了4种方法来创建HashSet对象:

  1. HashSet set1 = new HashSet();
  2. set1.add("元素1");
  3. set1.add("元素2");
  4. HashSet set2 = new HashSet(set1);
  5. HashSet set3 = new HashSet(10);
  6. HashSet set4 = new HashSet(10,0.8f);

(2)添加元素的方法

  1. 可以添加一个,可以添加多个,添加多个也就是把另外一个集合的元素添加进来。下面的两个方法是添加元素的方法:
  2. public boolean add(Object o):向集合中添加参数指定的元素。
  3. public boolean addAll(Collection c) :向集合中添加参数指定的所有元素。
  4. 例如:
  5. set3.add("元素5");
  6. set3.add("元素6");
  7. set1.addAll(set3);
  8. set1.add("元素3");
  9. 添加之后,set1中的元素为:元素2, 元素5, 元素3, 元素1, 元素6

(3)删除元的方法

  1. 可以删除一个,可以删除多个,还可以删除所有的元素,此外还有一个特殊的,删除某些元素之外的所有元素,所以对应的方法也有4个:
  2. public boolean remove(Object o):删除指定的某个元素。
  3. public boolean removeAll(Collection c) :删除指定的多个元素。
  4. public void clear():删除所有的元素。
  5. public boolean retainAll(Collection c) :只保留指定集合中存在的元素,其他的都删除,相当于取两个集合的交集。
  6. 下面的代码展示了具体用法:
  7. set1.remove("元素3");
  8. set1.remove(set2);
  9. 第一个方法删除了元素3,第二个方法删除set2中的元素,包括元素1和元素2,删除之后set1中剩下元素5和元素6

(4)、查找元素的方法
HashSet提供了判断元素是否存在的方法,方法定义如下:
public boolean contains(Object o):如果包含则返回true,否则返回false。
(5)、判断集合是否为空
方法定义如下:
public boolean isEmpty( ):如果集合为空返回true,否则返回false。
(6)、遍历集合的方法
HashSet提供了两种遍历集合的方法:
public Iterator iterator():转换成迭代器,方便集合中元素的遍历。
public Object[] toArray():转换成数组,也是方便集合中元素的遍历。
通常在管理集合的过程中使用集合本身提供的方法,但是遍历集合最好先转换成迭代器或者数组,这样访问比较方便,并且效率比较高。
下面是对HashSet进行遍历的两种方式。
方式1:得到迭代器对象
Iterator i = set1.iterator();
while(i.hasNext()){
String temp = (String)i.next();
System.out.println(temp);
}
方式2:转换成数组
Object o[] = hs.toArray();
for(int i=0;i < o.length;i++){
System.out.println((String)o[i]);
}

  1. 【例2-11】使用迭代器遍历HashSet
  2. import java.util.HashSet;
  3. import java.util.Iterator;
  4. public class IteractorDemo {
  5. public static void main(String args[]) {
  6. Set set = new HashSet();
  7. for (int i = 1; i < 6; i++) {
  8. set.add(new Integer(i));
  9. }
  10.    //获得迭代器对象
  11. Iterator it = set.iterator();
  12.    //循环遍历
  13. while(it.hasNext()){
  14. Integer j = (Integer)it.next();
  15. System.out.println(j);
  16. }
  17. System.out.println(set);
  18. }
  19. }
  20. 程序运行结果:
  21. 1
  22. 2
  23. 3
  24. 4
  25. 5
  26. [1, 2, 3, 4, 5]
  27. 程序分析:该例首先向集合中添加5个元素,之后获取该集合的迭代器对象,然后通过迭代器遍历集合,进行输出。
  1. 【例2-12HashSet使用示例。
  2. import java.util.ArrayList;
  3. import java.util.HashSet;
  4. import java.util.Iterator;
  5. public class HashSetTest {
  6. public static void main(String[] args) {
  7. HashSetTest test = new HashSetTest();
  8. test.testHashSet();
  9. }
  10. public void testHashSet(){
  11. //实例化HashSet对象
  12. HashSet hs = new HashSet();
  13. System.out.println("添加第一个元素");
  14. hs.add(new String("第一个元素"));
  15. System.out.println("创建一个ArrayList对象,添加两个元素");
  16. ArrayList list = new ArrayList();
  17. list.add("第二个元素");
  18. list.add("第三个元素");
  19. System.out.println("把ArrayList对象添加到HashSet中");
  20. hs.addAll(list);
  21. System.out.println("在HashSet中添加一个元素");
  22. hs.add("第四个元素");
  23. System.out.println("添加一个null元素"); 
  24. hs.add(null);
  25. System.out.println("\n通过转换成数组遍历的结果:");
  26. System.out.println("HashSet中的数据如下:");
  27. this.show2(hs);
  28. System.out.println("\n通过得到Iterator遍历的结果:");
  29. hs.remove("第一个元素");
  30. System.out.println("删除\"第一个元素\"之后:");
  31. this.show1(hs);
  32. System.out.println("HashSet中元素的个数为:"+hs.size());
  33. if(hs.isEmpty()){
  34. System.out.println("HashSet是空的");
  35. }
  36. else{
  37. System.out.println("HashSet不是空的");
  38. }
  39. System.out.println("清空所有的元素:");
  40. hs.clear();
  41. if(hs.isEmpty()){
  42. System.out.println("HashSet是空的");
  43. }
  44. else{
  45. System.out.println("HashSet不是空的");
  46. }
  47. }
  48. public void show1(HashSet hs){
  49. Iterator i = hs.iterator();
  50. while(i.hasNext()){
  51. String temp = (String)i.next();
  52. System.out.println(temp);
  53. }
  54. }
  55. /*
  56.  * 转换成数组,遍历并输出HashSet中的元素
  57.  */
  58. public void show2(HashSet hs){
  59. Object o[] = hs.toArray();
  60. for(int i=0;i<o.length;i++){
  61. System.out.println((String)o[i]);
  62. }
  63. }
  64. }
  65. 程序运行结果:
  66. 添加第一个元素
  67. 创建一个ArrayList对象,添加两个元素
  68. ArrayList对象添加到HashSet
  69. HashSet中添加一个元素
  70. 添加一个null元素
  71. 通过转换成数组遍历的结果:
  72. HashSet中的数据如下:
  73. null
  74. 第四个元素
  75. 第一个元素
  76. 第二个元素
  77. 第三个元素
  78. 通过得到Iterator遍历的结果:
  79. 删除"第一个元素"之后:
  80. null
  81. 第四个元素
  82. 第二个元素
  83. 第三个元素
  84. HashSet中元素的个数为:4
  85. HashSet不是空的
  86. 清空所有的元素:
  87. HashSet是空的

(2)TreeSet类
前面介绍的Set接口中的元素是没有顺序的,SortedSet继承了Set接口,但是SortedSet中的元素是按照升序排列的。排列的顺序既可以按照元素的自然顺序,也可以按照创建SortedSort时指定的Comparator对象。所有插入SortedSort中的元素必须实现Comparator,实现了该接口的类只有TreeSet类。[s1]
主要方法如下:
①一类方法,得到相关的Comparator对象:

  • public Comparator comparator(),返回相关的comparator对象,如果按照自然排序,返回null。

②二类方法,获取子集的方法:

  • public SortedSort subSet(Object fromElement,Object toElement),获取从fromElement到toElement的元素,包含fromElement,不包含toElement。
  • public SortedSet headSet(Object toElement),获取从开头到toElement的所有元素,不包含toElement。
  • public SortedSet tailSet(Object fromElement),获取从fromElement开始到结束的所有元素,包含fromElement。

③三类方法,获取元素的方法:

  • public Object first(),获取第一个元素。
  • public Object last(),获取最后一个元素。

    1. 【例2-13HashSetTreeSet使用练习
    2. import java.util.HashSet;
    3. import java.util.Set;
    4. import java.util.TreeSet;
    5. public class SetDemo {
    6. public static void main(String args[]) {
    7. /*
    8.  * HashSet
    9.  * 元素是无序的,不允许重复的
    10.  * 按照HashCode()来存储
    11.  */
    12. HashSet s = new HashSet();
    13. s.add("one");
    14. s.add("two");
    15. s.add(3);//JDK1.5后的新特性,不用创建包装类就可以直接传值
    16. s.add(new Float(4.0F));
    17. s.add("two");
    18. System.out.println(s);
    19. /*
    20.  * TreeSet
    21.  * 元素是无序的,不允许重复
    22.  * 按照排序树来存储,且要求各元素间的类型是可比较的
    23.  */
    24. TreeSet tree = new TreeSet();
    25. tree.add("one");
    26. tree.add("pwo");
    27. tree.add(new Integer(3).toString());
    28. tree.add(new Float(4.0F).toString());
    29. tree.add("pwo");
    30. tree.add(new Integer(3).toString());
    31. System.out.print(tree);
    32. TreeSet tree1 = new TreeSet();
    33. tree1.add("de");
    34. tree1.add("a");
    35. tree1.add("ab");
    36. tree1.add("c");
    37. System.out.print(tree1);
    38. }
    39. }
    40. 程序运行结果:
    41. [4.0, 3, one, two]
    42. [3, 4.0, one, pwo]
    43. [a, ab, c, de]
    44. 程序分析:TreeSet元素是无序的,不允许重复,按照排序树来存储,且要求各元素间的类型是可比较的。
    1. 【例2-14HashSetTreeSet比较
    2. import java.util.*;
    3. public class Test {
    4. public static void main(String[] args) {
    5. TreeSet hs = new TreeSet();
    6. hs.add("zs");
    7. hs.add("ls");
    8. hs.add("ww");
    9. hs.add("zs");
    10. for (Object i : hs)
    11. System.out.println(i);
    12. System.out.println("-------------");
    13. HashSet hs1 = new HashSet();
    14. hs1.add(new Student(1, "zs"));
    15. hs1.add(new Student(2, "ls"));
    16. hs1.add(new Student(3, "ww"));
    17. hs1.add(new Student(1, "zs"));
    18. for (Object i : hs1)
    19. System.out.println(i);
    20. TreeSet ts = new TreeSet(new SortedByName());
    21. ts.add(new Student(1, "zs"));
    22. ts.add(new Student(2, "ls"));
    23. ts.add(new Student(3, "ww"));
    24. ts.add(new Student(1, "zs"));
    25. for (Object i : ts)
    26. System.out.println(i);
    27. }
    28. }
    29. class SortedByName implements Comparator<Student> {
    30. public int compare(Student o1, Student o2) {
    31. return o1.name.compareTo(o2.name);
    32. }
    33. }
    34. class Student implements Comparable<Student> {
    35. int id;
    36. String name;
    37. public Student(int id, String name) {
    38. this.id = id;
    39. this.name = name;
    40. }
    41. public String toString() {
    42. return id + "-" + name;
    43. }
    44. public int hashCode() {
    45. return id;
    46. }
    47. public boolean equals(Object o) {
    48. Student o1 = (Student) o;
    49. return o1.id == this.id && o1.name.equals(this.name);
    50. }
    51. public int compareTo(Student o) {
    52. return this.id - o.id;
    53. }
    54. }

    程序分析:本程序演示HashSet确认两元素相等:先看hashCode 再看equalsTreeSet如何排序:原则上按照泛型限定的元素的排序实现,如果想改变这种排序则需重新定义一个类实现comparator。同时实例化TreeSet时需以实现comparator接口的类作为实际参数。
    TreeSet和Hashset两个类的区别如下。
    1. HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key。
    2. Map的key和Set都有一个共同的特性就是集合的唯一性。TreeMap更是多了一个排序的功能。
    3. hashCode和equal()是HashMap用的, 因为无需排序所以只需要关注定位和唯一性即可。
       a. hashCode是用来计算hash值的,hash值是用来确定hash表索引的。
       b. hash表中的一个索引处存放的是一张链表, 所以还要通过equal()方法循环比较链上的每一个对象才可以真正定位到键值对应的Entry。
       c. put时,如果hash表中没定位到,就在链表前加一个Entry,如果定位到了,则更换Entry中的value,并返回旧value。
    4. 由于TreeMap需要排序,所以需要一个Comparator为键值进行大小比较。当然也是用Comparator定位的。
       a. Comparator可以在创建TreeMap时指定。
       b. 如果创建时没有确定,那么就会使用key.compareTo()方法,这就要求key必须实现Comparable接口。
       c. TreeMap是使用Tree数据结构实现的,所以使用compare接口就可以完成定位了。

    2.3 Map接口及其实现类

    Map接口同样是包含多个元素的集合,Map内存储的是“键/值对”[s1] 这样以成对的对象组(可以把一组对象当成一个元素),通过“键”对象来查询“值”对象。Map是不同于Collection的另外一种集合接口。Map的每个元素包括两个部分:键(Key)和值(Value)。同一个Map对象中不允许使用相同的键,但是允许使用相同的值。所以Map接口隐含的有3个集合:键的集合、值的集合和映射的集合。[s2]
    Map和List有一些相同之处,List中的元素是用位置确定的,元素虽然可以相同,但是位置不能相同,也就是不会出现某个位置两个元素的情况,而Map中的元素是通过键来确定的,如果把List中的位置信息看成键的话,List也可以是一种特殊的Map。
    与Collection接口相比,Map接口中主要增加了通过键进行操作的方法,就像List中增加了通过位置进行操作的方法一样。
    ①**一类方法,添加元素的方法:
    § public Object put(Object key,Object value),第一个参数指定键,第二个参数指定值,如果键存在,则用新值覆盖原来的值,如果不存在添加该元素。
    § public void putAll(Map m),添加所有参数指定的映射。
    **二类方法,获取元素的方法:
    § public Object get(Object key),获取指定键所对应的值,如果不存在,返回null。
    ③**三类方法,删除元素的方法:
    § public Object remove(Object key),根据指定的键删除元素,如果不存在该元素,返回null。
    **四类方法,与键集合、值集合和映射集合相关的操作:
    § public Set entrySet(),获取映射的集合。
    § public Collection values(),获取值的集合。
    § public Set keySet(),返回所有键名的集合。
    这3个方法的返回值不一样,因为Map中的值是允许重复的,而键是不允许重复的,当然映射也不会重复。Set不允许重复,而Collection允许重复。
    ⑤**五类方法,判断是否存在指定KeyValue的方法:
    § public boolean containsValue(Object value),判断是否存在值为value的映射。
    § public boolean containsKey(Ojbect key),判断是否存在键为key的映射。
    Map接口有三个实现:
    § Hashtable — 主要用于存储一些映射关系。
    § HashMap — key/value对是按照Hash算法存储的。
    § TreeMap — key/value对是排序(按key排序)存储的。
    1.Hashtable**类
    实现了Map接口,是同步的哈希表,不允许类型为null的键名和键值。哈希表主要用于存储一些映射关系。这个类比较特殊,与Collection中的其他类不太一样,首先它是同步的,另外它是继承自java.util.Dictionary类。
    一个典型的应用就是在连接数据库的时候,需要提供各种参数,包括主机、端口、数据库ID、用户名、口令等,可以把这些信息先存储在哈希表中,然后作为参数使用。
    2.HashMap**
    是基于Hash表的Map接口实现。该类提供了所有的可选的映射操作,允许null值和null键。HashMap类和Hashtable基本相同,只是HashMap不同步,并且允许null键和null值。这个类不能保证元素的顺序,特别是顺序有可能随着时间变化。
    HashMap使用了泛型,泛型的具体内容将会在2.6节讲解,读者可参看2.6节阅读本例。对于Map类型的集合,在定义对象的时候同时要指定Key的类型和Value的类型,下面的例子展示了用法:
    HashMap user = 
    new** HashMap();
    user.put(“name”,”zhangsan”);
    user.put(“sex”,”男”);
    user.put(“id”,135);
    user.put(“age”,21);
    HashMap对象的遍历。假设map是HashMap的对象,对map进行遍历可以使用下面两种方式:
    第一种:得到元素的集合,然后进行运算,元素类型是Map.Entry。
    //得到元素集合,然后转换成数组
    Object[] o = map.entrySet().toArray();
    Map.Entry x  ;
    // 对数组进行遍历
    for(int i=0; i < map.size(); i++){
    // 取出数组的每一个元素
    x = (Map.Entry)o[i];
    // 获取该元素的key
    Object key = x.getKey();
    //获取该元素的值
    Object value = x.getValue();
    }
    第二种:先得到所有元素的Key的集合,然后根据key得到每个key对应的value。
    //先得到key的集合,然后转换成数组
    Object[] o = map.keySet().toArray();
    //对数组进行遍历
    for(int i=0; i < o.length; i++){
    //根据key得到具体的value。
    Object value = map.get(o[i]);
    }

    1. 【例2-16HashMap使用练习。
    2. public class HashMapDemo {
    3. public static void main(String args[]) {
    4. HashMap hm = new HashMap();
    5. hm.put("tom", 20);
    6. hm.put("john", 21);
    7. hm.put("jack", 20);
    8. hm.put("jones", 19);
    9. hm.put("rose", 19);
    10. hm.put("sun", 23);
    11. hm.put("tom",25);
    12. // 直接通过key值来取值
    13. String name = "tom";
    14. int age = (Integer) hm.get("tom");
    15. System.out.println(name + "的年龄是" + age);
    16. System.out.println();
    17. // 通过Iterator迭代出key值,再通过key值取出内容
    18. Set keys = hm.keySet();
    19. //获得key的集合
    20. Iterator it = keys.iterator();
    21. //遍历key的集合,取得每个key值
    22. while(it.hasNext()){
    23. String key = (String)it.next();
    24. System.out.println(key+":");
    25. //通过每个key值,找到value
    26. int age1 = (Integer)hm.get(key);
    27. System.out.println(age1);
    28. }
    29. }
    30. }
    31. 程序运行结果:
    32. tom的年龄是25
    33. rose:
    34. 19
    35. jones:
    36. 19
    37. jack:
    38. 20
    39. john:
    40. 21
    41. tom:
    42. 25
    43. sun:
    44. 23
    45. 程序分析:HashMap中存放的value,可以直接通过key值来取值,也可通过Iterator迭代出key值,再通过key值取出内容
    1. 【例2-17HashMapHashSet的使用。
    2. public class AccountCustomer {
    3. public static void main(String args[]) {
    4. Map ac = new HashMap();
    5. Set cus1 = new HashSet();
    6. cus1.add("SY000005");
    7. cus1.add("SY000015");
    8. ac.put("210103198802022273", cus1);
    9. HashSet cus2 = new HashSet();
    10. cus2.add("DL000123");
    11. cus2.add("DL000321");
    12. ac.put("210103196802022284", cus2);
    13. HashSet cus3 = new HashSet();
    14. cus3.add("SH000012");
    15. ac.put("205103196802022284", cus3);
    16. Iterator it = ac.keySet().iterator();
    17. while (it.hasNext()) {
    18. String customer = (String)it.next();
    19. HashSe account = (HashSet)ac.get(customer);
    20. System.out.print("身份号码是" + customer + "的用户的帐户:");
    21. Iterator it2 = account.iterator();
    22. while(it2.hasNext()){
    23. String num = (String)it2.next();
    24. System.out.print(num+" ");
    25. }
    26. /*转换成数组进行遍历
    27. Object[] acc = account.toArray();
    28. System.out.print("身份号码是" + customer + "的用户的帐户为");
    29. for (int i = 0; i < acc.length; i++) {
    30. System.out.print(acc[i] + " ");
    31. }
    32. */
    33. System.out.println();
    34. }
    35. }
    36. }
    37. 程序运行结果:
    38. 身份号码是210103196802022284的用户的帐户:DL000123 DL000321 
    39. 身份号码是205103196802022284的用户的帐户:SH000012 
    40. 身份号码是210103198802022273的用户的帐户:SY000005 SY000015

    程序分析:程序中使用HashMap来存储账号信息,每个账号的键是用来唯一表示一个客户身份的身份证号,值是HashSet类型,用来存储客户的账号,客户在银行中可以开设多个账号。程序首先使用ac.keySet().iterator()来获取所有账号信息的key,然后根据key取每个身份证对应的值,其类型为HashSet。遍历HashSet时,可以转换成数组遍历,也可以转换成迭代器进行遍历。
    3.HashMap**TreeMap的比较**
    HashMap与TreeMap区别如下:
    § HashMap基于哈希表实现。
    § TreeMap基于树实现。
    § HashMap可以通过调优初始容量和负载因子,优化HashMap空间的使用。
    § TreeMap没有调优选项,因为该树总处于平衡状态
    § HashMap性能优于TreeMap。

    1. 【例2-18HashMapTreeMap的使用,本例使用了泛型,关于泛型的详细讲解,请参考2.6节。
    2. import java.util.Map;
    3. class Test {
    4. public static void main(String args[]) throws Exception{
    5. Map<Emp, Integer> m1= new HashMap<Emp, Integer>();
    6. Map<Emp, Integer> m2 = new TreeMap<Emp, Integer>();
    7. Emp emp1 = new Emp("张三");
    8. Emp emp2 = new Emp("李四");
    9. Emp emp3 = new Emp("王五");
    10. Emp emp4 = new Emp("小张");
    11. Emp emp5 = new Emp("小李");
    12. Emp emp6 = new Emp("小王");
    13. //Emp emp7 = new Emp("小小");
    14. m1.put(emp1, emp1.getID());
    15. m1.put(emp2, emp2.getID());
    16. m1.put(emp3, emp3.getID());
    17. m1.put(emp4, emp4.getID());
    18. m1.put(emp5, emp5.getID());
    19. m1.put(emp6, emp6.getID());
    20. //m1.put(emp7, emp7.getID());
    21. //HashMap 遍历方法
    22. Iterator ilter1 = m1.entrySet().iterator();
    23. while (ilter1.hasNext()) {
    24.  Map.Entry entry = (Map.Entry)ilter1.next(); 
    25.  Emp key = (Emp)entry.getKey();
    26.  int value = (Integer)entry.getValue();
    27.  System.out.println("Key : "+key.getName() + " Id : "+value);
    28. }
    29. System.out.println("*****************************");
    30. //TreeMap遍历方法
    31. m2.put(emp1, emp1.getID());
    32. m2.put(emp2, emp2.getID());
    33. m2.put(emp3, emp3.getID());
    34. m2.put(emp4, emp4.getID());
    35. m2.put(emp5, emp5.getID());
    36. m2.put(emp6, emp6.getID());
    37. Iterator  ilter2= m2.entrySet().iterator();
    38. while (ilter2.hasNext()) {
    39.  Map.Entry entry = (Map.Entry)ilter2.next(); 
    40.  Emp key = (Emp)entry.getKey();
    41.  int value = (Integer)entry.getValue();
    42.  System.out.println("Key : "+key.getName() + " Id : "+value);
    43. }
    44. }
    45. }
    46. class Emp implements Comparable<Emp>{
    47. public Emp() {
    48. this.id = 0;
    49. }
    50. public Emp(String name){
    51. this.name = name;
    52. this.id = ++emp.EmpId ;
    53. }
    54. public String getName(){
    55. return this.name;
    56. }
    57. public int getID(){
    58. return this.id;
    59. }
    60. public int compareTo(Emp o) {
    61. return id-o.getID();
    62. }
    63. private static int empId = 0;
    64. private int id;
    65. private String name;
    66. }

    程序分析:程序中定义了一个员工类,包含员工的编号、姓名,且实现了Comparable接口,测试类中分别用HashMap与TreeMap两个类创建了存储员工信息的对象。
    Map m1= new HashMap();//hashmap
    Map m2 = new TreeMap();//treemap
    以上两条语句在创建员工对象时限制了其传入的对象类型。
    4.HashMap与HashTable的比较
    HashMap与HashTable区别如下:
    § Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现。
    § Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的。
    § HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
    5.如何选择集合类
    实际应用中,集合类的选择主要依据如下:

  • Set内存放的元素不允许重复,List存放的元素有一定的顺序。

  • Map的应用主要在利用键/值对进行快速查询。
  • ArrayList和LinkedList的区别在于随机查询性能上ArrayList要好,但LinkedList的中间元素的插入与删除性能好。
  • HashSet和TreeSet的区别在于集合内元素是否排序。

    2.4 集合中的异常

    在操作Java集合时,常常会遇到各种异常,其异常类型及说明如表2-3所示。
    表2-3 操作集合时常见异常及说明
异常类型 说明
ClassCastException 从集合中取得元素对象在进行类型转换的时候类型不匹配。
UnsupportedOperationException 当底层实现没有实现上层的相关方法的时候由Collection抛出该异常。Collection接口(或其他集合超类)中的一些函数在java doc中是标明”可有可无(Optional)”的函数,这些函数在底层的一些具体实现中,有的实现了,有的没有实现,当我们调用底层实现集合类的这些没有实现的方法时就会抛出该异常。
ConcurrentModificationException ConcurrentModificationException 当采用Iterator遍历集合时,如果此时集合中的元素被改变则Iterator遍历抛出此异常。
IndexOutOfBoundsException 集合中元素引用的索引值超出界限(<0或>size())。
NoSuchElementException LinkedList中getLast,getFirst等方法取元素的时候List为空。
  1. 【例2-19】集合操作中出现的java.lang.ClassCastException
  2. import java.util.ArrayList;
  3. import java.util.Iterator;
  4. import java.util.List;
  5. public class CollectionException {
  6. public static void main(String args[]) {
  7. List list = new ArrayList();
  8. list.add(new Integer(5));
  9. list.add("aaaaa");
  10. Iterator elements = list.iterator();
  11. while (elements.hasNext()) {
  12. Integer i = (Integer) elements.next();
  13. }
  14. }
  15. }
  16. 程序运行结果:
  17. Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integerat collection.CollectionException.main(CollectionException.java:14)
  18. 程序分析:集合里添加了个整型数据,一个字符串类型数据,在取出时把字符串转换成整型会出现类型转换异常。
  1. 【例2-20ConcurrentModificationException异常
  2. 在集合操作的时候,我们通常会使用foreach增强for循环遍历集合,比如我们想输出集合中的每个元素,我们可以使用for循环遍历。代码如下:
  3. //初始化一个集合
  4. static List<String> list = new ArrayList<String>();
  5. list.add("1");
  6. list.add("2");
  7. list.add("3");
  8. list.add("4");
  9. //遍历集合
  10. for (String l : list) {
  11. System.out.println(l);
  12. }
  13. 但是当我们在使用增强for循环的时候,如果我们想在循环中删除一个或者多个元素的时候,直接调集合的remove()方法是不行的,会报异常:
  14. Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(Unknown Source)at java.util.ArrayList$Itr.next(Unknown Source)at collection.Exe.main(Exe.java:22)
  15. for (String l : list) {
  16. if (l.equals("1")) {
  17. list.remove(l);
  18. }
  19. }

究其原因,是因为我们在使用集合的remove()操作的时候,java底层会去执行一个modCount++操作,修改modCount这个变量的值。同时我们在执行增强for循环的时候,本质上是在使用集合的Iterator,而这个Iterator也会维护这个modCount的变量,此外它还维护一个expectedModCount变量。也就是说集合本身会维护modCount变量,Iterator会维护modCount和expectedModCount变量。但是每次在Iterator执行操作的时候,都会先检查modCount变量和expectedModCount的值是否一致,源代码如下:
final void checkForComodification() {
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}
在检查modCount和expectedModCount的值是否还相同,如果不相同,那么就报异常。在上面for循环的操作中,我们使用for循环遍历,但是在循环内部使用集合自己的remove()方法,这个方法把modCount的值给修改,接下来再遍历的时候,使用到了Iterator的方法,这时再检查那两个变量的时候,因为只有modCount被改了,所以他们不一致了,自然要抛异常。有两个办法可以避免这个异常。
第一,不要使用集合Iterator相关的方法,不涉及到expectedModCount变量即可,只涉及到modCount变量即可。说具体点就是我们不要使用任何和Iterator相关的东西,也就是说不要使用争强for循环,当然这样就不会执行检查两个变量是否相同了。代码体现如下:
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(“4”)) {
//只使用到集合自己的remove()方法,不会涉及到
list.remove(list.get(i));
expectedModCount
}
}
第二,每次操作的时候,不要只修改modCount变量,同时修改expectedModCount变量,使之永远保持一致即可。也就是说不要使用集合自己remove()方法,使用Iterator提供的方法。代码体现如下:
Iterator itList = list.iterator();
while (itList.hasNext()) {
if (itList.next().equals(“1”)) {
//使用Iterator的remove()方法,这样modCount和expectedModCount永远是一致的
itList.remove();
}
}
第三,我们还有一个折中的办法。首先使用iterator()方法得到所有需要被删除的元素,然后再使用集合的removeAll()方法一次性将要删除的元素都删除,在removeAll()方法中,我们也不会涉及到检查modCount和expectedModCount是否一致的问题。这里只列举了List集合的例子,对于Set集合和Map集合是相同的。

2.5 泛型的使用

1.泛型概述

泛型(Generics)是自JDK5.0开始引入的一种Java语言新特性,其实质是将原本确定不变的数据类型参数化,作为对原有Java类型体系的扩充,使用泛型可以提高Java应用程序的类型安全、可维护性和可靠性。
泛型是指参数化类型的能力。可以定义带泛型类型的类或方法,随后编译器会用具体的类型来替换他。泛型就是添加了一个类型参数,你可以在用泛型类或者泛型方法的时候确定这个泛型为一个确定的类型。在以前的java版本中是没有泛型的只能用根类Object来表示泛型,但是这样的话就不能表示一个确定的类型,因为object是所有类的父类,所以它表示所有类型。
泛型允许编译器实施由开发者设定的附加类型约束,将类型检查从运行时挪到编译时进行,这样类型错误可以在编译时暴露出来,而不是在运行时才发作,这非常有助于早期错误检查,提高程序的可靠性,同时还可以减少强制类型转换的编码量。
java中加入了泛型以后,所有的集合框架都重新写了,使它们支持泛型。这样你就可以这样写:
ArrayList al=new ArrayList();
表示一个String型的Arraylist,但是泛型有一个问题就是它不支持基本类型作为类型参数。Java中的集合可以添加任何类型的对象,所有的元素均当作Object类型来处理,当从集合中取出元素时,必须进行强制类型转换。而使用泛型可以在创建集合时指定其允许保存的元素类型,然后由编译器负责添加元素的类型合法性检查,而从集合中取出元素时,也不需要强制类型转换了。
泛型思想早在C++语言的模板(Templates)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会被转嫁到程序运行期中。
泛型技术在C#和Java中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,List与List就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList与ArrayList就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类来说尤其有用。泛型程序设计(Generic Programming)意味着编写的代码可以被很多不同类型的对象所重用。
在JDK1.5之前,Java泛型程序设计是用继承来实现的。因为Object类是所有类的基类,所以只需要维持一个Object类型的引用即可。就比如ArrayList只维护一个Object类型的数组:
public class ArrayList{ //JDK1.5之前的
public Object get(int i){……} 
public void add(Object o){……}
……
private Object[] elementData;
}
这样会有两个问题:
1、没有错误检查,可以向数组列表中添加类的对象
2、在取元素的时候,需要进行强制类型转换
这样,很容易发生错误,比如:
/*jdk1.5之前的写法,容易出问题/  
ArrayList arrayList1=new ArrayList();  
arrayList1.add(1);  
arrayList1.add(1L);  
arrayList1.add(“asa”);  
int i=(Integer) arrayList1.get(1);
这里的第一个元素是一个长整型,而你以为是整形,所以在强转的时候发生了错误。所以在JDK1.5之后,加入了泛型来解决类似的问题。例如在ArrayList中使用泛型: 
ArrayList arrayList2=new ArrayList();  //限定数组列表中的类型  
// arrayList2.add(1); //因为限定了类型,所以不能添加整形  
// arrayList2.add(1L);//因为限定了类型,所以不能添加整长形  
arrayList2.add(“asa”);//只能添加字符串  
String str=arrayList2.get(0);//因为知道取出来的值的类型,所以不需要进行强制类型转换  
还要明白的是,泛型特性是向前兼容的。尽管 JDK 5.0 的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类(比如 HashMap 和 ArrayList)的现有代码可以继续不加修改地在 JDK 1.5 中工作。当然,没有利用泛型的现有代码将不会赢得泛型的类型安全的好处。
在学习泛型之前,简单介绍下泛型的一些基本术语,以ArrayList和ArrayList做简要介绍:

  • 整个称为ArrayList泛型类型。
  • ArrayList中的 E称为类型变量或者类型参数。
  • 整个ArrayList称为参数化的类型。
  • ArrayList中的integer称为类型参数的实例或者实际类型参数。
  • ArrayList中的念为typeof  Integer。
  • ArrayList称为原始类型。

    2.泛型的使用

    泛型经常被称为参数化类型,它能够像方法一样接受不同类型的参数。定义方式为:
    ArrayList 变量名;//E是变量类型
    ArrayList arr; 
    arr = new ArrayList(); 
    HashMap hm = new HashMap();
    泛型的参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。下面看看具体是如何定义的。

    (1)泛型类的定义和使用

    一个泛型类(generic class)就是具有一个或多个类型变量的类。定义一个泛型类十分简单,只需要在类名后面加上<>,再在里面加上类型参数:

    1. class Pair<T> {
    2. private T value;
    3. public Pair(T value) {
    4. this.value=value;  
    5. }  
    6. public T getValue() {
    7. return value;
    8. }
    9. public void setValue(T value) {
    10. this.value = value;
    11. }
    12. }

    现在我们就可以使用这个泛型类了:

    1. public static void main(String[] args) throws ClassNotFoundException {  
    2.   Pair<String> pair=new Pair<String>("Hello");  
    3.   String str=pair.getValue();  
    4.   System.out.println(str);  
    5.   pair.setValue("World");  
    6.   str=pair.getValue();  
    7.   System.out.println(str);  
    8. }

    Pair类引入了一个类型变量T,用尖括号<>括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义Pair类,其中第一个域和第二个域使用不同的类型:
    public class Pair{……}
    注意:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示关键字与值的类型。(需要时还可以用临近的字母U和S)表示“任意类型”。

    (2)泛型接口的定义和使用

    定义泛型接口和泛型类差不多,看下面简单的例子:

    1. interface Show{  
    2.   void show(T t,U u);  
    3. }
    4. class ShowTest implements Show{  
    5.   @Override  
    6.   public void show(String str,Date date) {  
    7.     System.out.println(str);  
    8.     System.out.println(date);  
    9.   }  
    10. }
    11. interface Show{
    12. void show(T t,U u);
    13. }
    14. class ShowTest implements Show{
    15. @Override
    16. public void show(String str,Date date) {
    17. System.out.println(str);
    18. System.out.println(date);
    19. }
    20. }

    测试一下:
    public static void main(String[] args) throws ClassNotFoundException {
    ShowTest showTest=new ShowTest();
    showTest.show(“Hello”,new Date());
    }
    public static void main(String[] args) throws ClassNotFoundException {
    ShowTest showTest=new ShowTest();
    showTest.show(“Hello”,new Date());

    (3)泛型方法的定义和使用

    泛型类在多个方法签名间实施类型约束。在 List中,类型参数 V 出现在 get()、add()、contains() 等方法的签名中。当创建一个 Map类型的变量时,您就在方法之间宣称一个类型约束。您传递给 add() 的值将与 get() 返回的值的类型相同。
    类似地,之所以声明泛型方法,一般是因为想要在该方法的多个参数之间宣称一个类型约束。
    举个简单的例子:
    public static void main(String[] args) throws ClassNotFoundException {
    String str=get(“Hello”, “World”);
    System.out.println(str);
    }
    public static  T get(T t, U u) {
    if (u != null)
    return t;
    else
    return null;
    }  

    (4)泛型变量的类型限定

    在上面,我们简单的学习了泛型类、泛型接口和泛型方法。我们都是直接使用这样的形式来完成泛型类型的声明。
    有的时候,类、接口或方法需要对类型变量加以约束。看下面的例子:
    有这样一个简单的泛型方法:
    public static  T get(T t1,T t2) {  
       if(t1.compareTo(t2)>=0);//编译错误  
       return t1;  
     }  
    因为,在编译之前,也就是我们还在定义这个泛型方法的时候,我们并不知道这个泛型类型T,到底是什么类型,所以,只能默认T为原始类型Object。所以它只能调用来自于Object的那几个方法,而不能调用compareTo()方法。
    可我的本意就是要比较t1和t2,怎么办呢?这个时候,就要使用类型限定,对类型变量T设置限定(bound)来做到这一点。
    我们知道,所有实现Comparable接口的方法,都会有compareTo()方法。所以,可以对做如下限定:

    1. public static T get(T t1,T t2){
    2. if(t1.compareTo(t2)>=0);
    3. return t1;
    4. }

      
    类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过要注意下面几点:
    ①不管该限定是类还是接口,统一都使用关键字 extends
    ②可以使用&符号给出多个限定,比如
    public static  T get(T t1,T t2)  
    public static  T get(T t1,T t2)
    ③ 如果限定既有接口也有类,那么类必须只有一个,并且放在首位置
    public static  T get(T t1,T t2)  
    程序中使用泛型的主要好处如下:

  • 在对象放入集合前,为其作个限制

  • 在获取集合中的对象时,不用进行造型的操作
  • 当有不同类型的对象添加到集合中的时候,编译时就能检查出错
    1. //【例2-21】HashSet集合中使用泛型。
    2. public class GenericDemo {
    3. public static void main(String args[]) {
    4. HashSet<Integer> set = new HashSet<Integer>();
    5. for (int i = 1; i < 6; i++) {
    6. set.add(new Integer(i));
    7. }
    8. Iterator<Integer> it = set.iterator();
    9. while (it.hasNext()) {
    10. System.out.println(it.next());
    11. }
    12. }
    13. }
    14. 程序运行结果:
    15. 1
    16. 2
    17. 3
    18. 4
    19. 5
  1. //【例2-22】使用泛型方法计算任意多个数值类型的数据的和。
  2. import java.util.ArrayList;
  3. public class GenericDemo1{
  4. public static double getSum(ArrayList<? extends Number> list){
  5. double sum = 0;
  6. for(Number n:list){
  7. sum += n.doubleValue();
  8. }
  9. return sum;
  10. }
  11. public static void main(String[] args) {
  12. ArrayList<Integer> a = new ArrayList<Integer>(); 
  13. a.add(3);
  14. a.add(5);
  15. a.add(7);
  16. System.out.println(getSum(a));
  17. }
  18. }
  19. //程序运行结果:15.0

程序分析:与泛型类的情况类似,方法也可以泛型化,且无论其所属的类是否为泛型类。其做法是在某一个方法中使用类型参数。泛型机制允许开发者对类型参数指定附加的约束。可以使用extends关键字来指明类型参数必须符合这种继承上的限制。

  1. 【例2-24】使用泛型方法计算任意多个数值类型的数据的平均值。
  2. public class GenericDemo3 {
  3. public static double getAverage(ArrayList<? extends Number> list) {
  4. double total = 0.0;
  5. for (Number number : list)
  6. total += number.doubleValue();
  7. return total / list.size();
  8. }
  9. public static void main(String[] args) {
  10. ArrayList<Integer> integerList = new ArrayList<Integer>();
  11. integerList.add(100);
  12. integerList.add(200);
  13. System.out.println(integerList);
  14. //integerList.add(300);
  15. System.out.println(getAverage(integerList));
  16. ArrayList<Double> doubleList = new ArrayList<Double>();
  17. doubleList.add(10.0);
  18. doubleList.add(20.0);
  19. System.out.println(getAverage(doubleList));
  20. ArrayList<String> StringList = new ArrayList<String>();
  21. //System.out.println(getAverage(StringList)); 
  22. }
  23. }
  24. 程序运行结果:
  25. [100, 200]
  26. 150.0
  27. 15.0
  28. 程序分析:在泛型类和泛型方法中,都可以使用类型参数。若施加的类型约束只想作用于一个方法的多个参数之间,而不涉及到类中的其他方法时,则应将之定义为泛型方法,否则,将之定义为泛型类。
  1. 【例2-25】泛型中通配符?的使用。
  2. import java.util.ArrayList;
  3. public class GenericDemo4 {
  4. public static void printList(ArrayList<?> list) {
  5. for (Object element : list) {
  6. System.out.println(element);
  7. }
  8. }
  9. }
  10. 程序分析:Java5.0新特性(增强for循环),它可以遍历集合中的元素,这里需要注意的地方是: element的类型必须是Object的,原因是:这里使用了通配符"?",它可以代表任意类型的数据,所以必须用Object类型的变量来接收。

2.6课后作业

1、请使用LinkedList来模拟一个队列(先进先出的特性):
1.1 拥有放入对象的方法void put(Object o) 
1.2 取出对象的方法Object get() 
1.3 判断队列当中是否为空的方法boolean isEmpty();并且,编写测试代码,验证你的队列是否正确。
2、假设顺序列表ArrayList中存储的元素是整型数字1~5,遍历每个元素,将每个元素顺序输出。
3、在一个列表中存储以下元素:apple,grape,banana,pear 
3.1 返回集合中的最大的和最小的元素
3.2 将集合进行排序,并将排序后的结果打印在控制台上
4、编写一个程序,创建一个 HashMap对象,用于存储银行储户的信息(其中储户的主要信息有储户的ID,姓名和余额)。另外,计算并显示其中某个储户的当前余额。
5、从控制台输入若干个单词(输入回车结束)放入集合中,将这些单词排序后(忽略大小写)打印出来。
6 [ 单选题 ]
Java 中的集合类包括 ArrayList、LinkedList、HashMap 等类,下列关于集合类描述错误的是( C )
A: ArrayList 和 LinkedList 均实现了 List 接口
B ArrayList 的访问速度比 LinkedList 快
C 添加和删除元素时,ArrayList 的表现更佳
D HashMap 实现 Map 接口,它允许任何类型的键和值对象,并允许将 null 用作键或值
7.简答题:
简单叙述一下 Arraylist扩容 与 hashmap 扩容机制 ?[

](https://null_688_6639.gitee.io/javase/01JavaSE/%E7%AC%AC11%E7%AB%A0%E6%96%87%E4%BB%B6%E4%B8%8E%E6%B5%81-%E9%AB%98%E7%BA%A7%E7%AF%87.html)