javaSE 基础笔记之集合框架
第九章 集合框架
学习目标:
² 掌握集合框架的基本概念
² 掌握 Collection 接口
² 掌握 Iterator 接口
² 掌握 Set 接口
² 掌握 List 接口
² 掌握 Map 接口
² 熟悉集合的排序
² 掌握泛型的概念
² 掌握泛型的编程应用
一:集合框架的基本概念
1:数学背景
在常见用法中,集合(collection)和数学上直观的集(set)的概念是相同的。集是
一个唯一项组,也就是说组中没有重复项。数学上集的概念比 Java 技术提前了一个世纪,
那时英国数学家 George Boole 按逻辑正式的定义了集的概念。大部分人在小学时通过我们
熟悉的维恩图引入的“集的交”和“集的并”学到过一些集的理论。
(1)集的一些现实的示例如下:
· 大写字母集“A”到“Z”
· 非负整数集{0, 1, 2 ...}
· 保留的 Java 编程语言关键字集 {'import', 'class', 'public', 'protected'...}
· 人集(friends, employees, clients, ...)
· 数据库查询返回记录集
· Container 的 Component 对象集
· 所有对(pair)集
· 空集{}
(2)集的基本属性如下:
· 集内只包含每项的一个实例
· 集可以是有限的,也可以是无限的
· 可以定义抽象概念
(3)映射是一种特别的集。它是一种对(pair)集,每个对表示一个元素到另一元素的单
向映射。一些映射示例有:
IP 地址到域名(DNS)的映射
关键字到数据库记录的映射
字典(词到含义的映射)
2 进制到 10 进制转换的映射
此外,因为映射也是集,所以它们可以是有限的,也可以是无限的。无限映射的一个示
例如 2 进制到 10 进制的转换。不幸的是, “集合框架”不支持无限映射 — 有时用数学函
数、公式或算法更好。但在有限映射能解决问题时, “集合框架”会给 Java 程序员提供一
个有用的 API。
2:基本概念
2.1:什么是集合
集合是包含多个对象的简单对象,所包含的对象称为元素。
集合里面可以包含任意多个对象,数量可以变化;同时对对象的类型也没有限制,也就
是说集合里面的所有对象的类型可以相同,也可以不同。
2.2:集合里面有什么
既然您已经具备了一些集的理论,您应该能够更轻松的理解“集合框架”。 “集合框架”
由一组用来操作对象的接口组成。不同接口描述不同类型的组。虽然您总要创建接口特定的
实现,但访问实际集合的方法应该限制在接口方法的使用上;因此,允许您更改基本的数据
结构而不必改变其它代码。
Java平台提供了一个全新的集合框架。“集合框架”主要由一组用来操作对象的接口组成。不同接口描述一组不同数据类型。
Java 集合框架
集合接口:6个接口(短虚线表示),表示不同集合类型,是集合框架的基础。
抽象类:5个抽象类(长虚线表示),对集合接口的部分实现。可扩展为自定义集合类。
实现类:8个实现类(实线表示),对接口的具体实现。
在很大程度上,一旦您理解了接口,您就理解了框架。虽然您总要创建接口特定的实现,但访问实际集合的方法应该限制在接口方法的使用上;因此,允许您更改基本的数据结构而不必改变其它代码。
· Collection 接口是一组允许重复的对象。
· Set 接口继承 Collection,但不允许重复,使用自己内部的一个排列机制。
· List 接口继承 Collection,允许重复,以元素安插的次序来放置元素,不会重新排列。
· Map接口是一组成对的键-值对象,即所持有的是key-value pairs。Map中不能有重复的key。拥有自己的内部排列机制。
· 容器中的元素类型都为Object。从容器取得元素时,必须把它转换成原来的类型。
集合框架接口层次结构如下图所示。
集合框架(结构图)
简化图:
有的人可能会认为 Map 会继承 Collection。在数学中,映射只是对(pair)的集合。
但是,在“集合框架”中,接口 Map 和 Collection 在层次结构没有任何亲缘关系,它
们是截然不同的。这种差别的原因与 Set 和 Map 在 Java 库中使用的方法有关。Map 的
典型应用是访问按关键字存储的值。它支持一系列集合操作的全部,但操作的是键-值对,
而不是单个独立的元素。因此 Map 需要支持 get() 和 put() 的基本操作,而 Set 不需要。
此外,还有返回 Map 对象的 Set 视图的方法:Set set = aMap.keySet();
让我们转到对框架实现的研究, 具体的集合类遵循命名约定,并将基本数据结构和框架
接口相结合。除了四个历史集合类外,Java 框架还引入了六个集合实现,如下表所示。关
于历史集合类如何转换、比如说,如何修改 Hashtable 并结合到框架中,请参阅历史集
合类。
这里没有 Collection 接口的实现。历史集合类,之所以这样命名是因为从 Java 类
库 1.0 发行版就开始沿用至今了。
如果从历史集合类转换到新的框架类,主要差异之一在于所有的操作都和新类不同步。
您可以往新类中添加同步的实现,但您不能把它从旧的类中除去。
2.3:集合框架中各接口的特点
用“集合框架”设计软件时,记住该框架四个基本接口的下列层次结构关系会有用处:
Collection 接口是一组允许重复的对象。
Set 接口继承 Collection,无序但不允许重复。
List 接口继承 Collection,有序但允许重复,并引入位置下标。
Map 接口既不继承 Set 也不继承 Collection,是键值对。
二:Collection 接口
1:Collection接口
Collection 接口用于表示任何对象或元素组。想要尽可能以常规方式处理一组元素
时,就使用这一接口。这里是以统一建模语言(Unified Modeling Language(UML))表示
法表示的 Collection 公有方法清单。
该接口支持如添加和除去等基本操作。设法除去一个元素时,如果这个元素存在,除去
的仅仅是集合中此元素的一个实例。
boolean add(Object element)
boolean remove(Object element)
Collection 接口还支持查询操作:
int size()
boolean isEmpty()
boolean contains(Object element)
Iterator iterator()
2:Iterator接口
Collection 接口的 iterator() 方法返回一个 Iterator。Iterator 和您可能
已经熟悉的 Enumeration 接口类似, 我们将在 Enumeration 接口中对 Enumeration 进行
讨论。 使用 Iterator 接口方法, 您可以从头至尾遍历集合,并安全的从底层 Collection
中除去元素:
remove() 方法可由底层集合有选择的支持。当底层集合调用并支持该方法时,最近
一次 next() 调用返回的元素就被除去。为演示这一点,用于常规 Collection 的
Iterator 接口代码如下:
1 Collection collection = ...; 2 3 Iterator iterator = collection.iterator(); 4 5 while (iterator.hasNext()) { 6 7 Object element = iterator.next(); 8 9 if (removalCheck(element)) { 10 11 iterator.remove(); 12 13 } 14 15 }
3:组操作
Collection 接口支持的其它操作,要么是作用于元素组的任务,要么是同时作用于
整个集合的任务。
boolean containsAll(Collection collection)
boolean addAll(Collection collection)
void clear()
void removeAll(Collection collection)
void retainAll(Collection collection)
containsAll() 方法允许您查找当前集合是否包含了另一个集合的所有元素, 即另一个集
合是否是当前集合的子集。其余方法是可选的,因为特定的集合可能不支持集合更改。
addAll() 方法确保另一个集合中的所有元素都被添加到当前的集合中,通常称为并。
clear() 方法从当前集合中除去所有元素。
removeAll() 方法类似于 clear() ,但只除去了元素的一个子集。
retainAll() 方法类似于 removeAll() 方法,不过可能感到它所做的与前面正好相反:它
从当前集合中除去不属于另一个集合的元素,即交。
三:Set 接口
1:Set接口
按照定义,Set 接口继承 Collection 接口,而且它不允许集合中存在重复项。所有
原始方法都是现成的, 没有引入新方法。 具体的 Set 实现类依赖添加的对象的 equals() 方
法来检查等同性。
2:HashSet 类和 TreeSet 类
“集合框架”支持 Set 接口两种普通的实现:HashSet 和 TreeSet。在更多情况下,
您会使用 HashSet 存储重复自由的集合。考虑到效率,添加到 HashSet 的对象需要采用
恰当分配散列码的方式来实现 hashCode() 方法。虽然大多数系统类覆盖了 Object 中
缺省的 hashCode() 实现,但创建您自己的要添加到 HashSet 的类时,别忘了覆盖
hashCode()。当您要从集合中以有序的方式抽取元素时,TreeSet 实现会有用处。为了
能顺利进行,添加到 TreeSet 的元素必须是可排序的。
“集合框架”添加对 Comparable 元素的支持,在排序的“可比较的接口”部分中
会详细介绍。我们暂且假定一棵树知道如何保持 java.lang 包装程序器类元素的有序状
态。一般说来,先把元素添加到 HashSet,再把集合转换为 TreeSet 来进行有序遍历会
更快。
为优化 HashSet 空间的使用,您可以调优初始容量和负载因子。TreeSet 不包含调
优选项,因为树总是平衡的,保证了插入、删除、查询的性能为 log(n)。
HashSet 和 TreeSet 都实现 Cloneable 接口。
3:集的使用示例
为演示具体 Set 类的使用,下面的程序创建了一个 HashSet,并往里添加了一组名
字,其中有个名字添加了两次。接着,程序把集中名字的列表打印出来,演示了重复的名字
没有出现。接着,程序把集作为 TreeSet 来处理,并显示有序的列表。
1 import java.util.*; 2 3 4 5 public class SetExample { 6 7 public static void main(String args[]) { 8 9 Set set = new HashSet(); 10 11 set.add("Bernadine"); 12 13 set.add(" Elizabeth"); 14 15 set.add("Gene"); 16 17 set.add("Elizabeth"); 18 19 set.add("Clara"); 20 21 System.out.println(set); 22 23 24 25 Set sortedSet = new TreeSet(set); 26 27 System.out.println(sortedSet); 28 29 } 30 31 }
运行程序产生了以下输出。请注意重复的条目只出现了一次,列表的第二次输出已按字母顺
序排序。
[Gene, Clara, Bernadine, Elizabeth]
[Bernadine, Clara, Elizabeth, Gene]
四:List 接口
1:List接口
List 接口继承了 Collection 接口以定义一个允许重复项的有序集合。该接口不但
能够对列表的一部分进行处理,还添加了面向位置的操作。
面向位置的操作包括插入某个元素或 Collection 的功能,还包括获取、除去或更改
元素的功能。在 List 中搜索元素可以从列表的头部或尾部开始,如果找到元素,还将报
告元素所在的位置。
void add(int index, Object element)
boolean addAll(int index, Collection collection)
Object get(int index)
int indexOf(Object element)
int lastIndexOf(Object element)
Object remove(int index)
Object set(int index, Object element)
List 接口不但以位置友好的方式遍历整个列表,还能处理集合的子集:
ListIterator listIterator()
ListIterator listIterator(int startIndex)
List subList(int fromIndex, int toIndex)
处理 subList() 时, 位于 fromIndex 的元素在子列表中, 而位于 toIndex 的元素则不是,
提醒这一点很重要。以下 for-loop 测试案例大致反映了这一点:
for (int i=fromIndex; i<toIndex; i++) {
// process element at position i
}
此外,我们还应该提醒的是 — 对子列表的更改(如 add()、remove() 和 set() 调用)
对底层 List 也有影响。
2:ListIterator 接口
ListIterator 接口继承 Iterator 接口以支持添加或更改底层集合中的元素, 还支
持双向访问。
以下源代码演示了列表中的反向循环。请注意 ListIterator 最初位于列表尾之后
(list.size()),因为第一个元素的下标是 0。
List list = ...;
ListIterator iterator = list.listIterator(list.size());
while (iterator.hasPrevious()) {
Object element = iterator.previous();
// Process element
}
正常情况下,不用 ListIterator 改变某次遍历集合元素的方向 — 向前或者向后。
虽然在技术上可能实现时, 但在 previous() 后立刻调用 next(), 返回的是同一个元素。
把调用 next() 和 previous() 的顺序颠倒一下,结果相同。
我们还需要稍微再解释一下 add() 操作。 添加一个元素会导致新元素立刻被添加到隐
式光标的前面。因此,添加元素后调用 previous() 会返回新元素,而调用 next() 则
不起作用,返回添加操作之前的下一个元素。
3:ArrayList 类和 LinkedList 类
在“集合框架”中有两种常规的 List 实现:ArrayList 和 LinkedList。使用两
种 List 实现的哪一种取决于您特定的需要。如果要支持随机访问,而不必在除尾部的任
何位置插入或除去元素,那么,ArrayList 提供了可选的集合。但如果,您要频繁的从列
表的中间位置添加和除去元素,而只要顺序的访问列表元素,那么,LinkedList 实现更
好。
ArrayList 和 LinkedList 都实现 Cloneable 接口。此外,LinkedList 添加了
一些处理列表两端元素的方法(下图只显示了新方法):
使用这些新方法,您就可以轻松的把 LinkedList 当作一个堆栈、队列或其它面向端
点的数据结构。
LinkedList queue = ...;
queue.addFirst(element);
Object object = queue.removeLast();
LinkedList stack = ...;
stack.addFirst(element);
Object object = stack.removeFirst();
Vector 类和 Stack 类是 List 接口的历史实现。
4:List 的使用示例
下面的程序演示了具体 List 类的使用。第一部分,创建一个由 ArrayList 支持的
List。 填充完列表以后, 特定条目就得到了。 示例的 LinkedList 部分把 LinkedList 当
作一个队列,从队列头部添加东西,从尾部除去。
1 import java.util.*; 2 3 4 5 public class ListExample { 6 7 public static void main(String args[]) { 8 9 List list = new ArrayList(); 10 11 12 13 list.add("Bernadine"); 14 15 list.add("Elizabeth"); 16 17 list.add("Gene"); 18 19 list.add("Elizabeth"); 20 21 list.add("Clara"); 22 23 System.out.println(list); 24 25 System.out.println("2: " + list.get(2)); 26 27 System.out.println("0: " + list.get(0)); 28 29 LinkedList queue = new LinkedList(); 30 31 queue.addFirst("Bernadine"); 32 33 queue.addFirst("Elizabeth"); 34 35 queue.addFirst("Gene"); 36 37 queue.addFirst("Elizabeth"); 38 39 queue.addFirst("Clara"); 40 41 System.out.println(queue); 42 43 queue.removeLast(); 44 45 queue.removeLast(); 46 47 System.out.println(queue); 48 49 } 50 51 }
运行程序产生了以下输出。请注意,与 Set 不同的是 List 允许重复。
[Bernadine, Elizabeth, Gene, Elizabeth, Clara]
2: Gene
0: Bernadine
[Clara, Elizabeth, Gene, Elizabeth, Bernadine]
[Clara, Elizabeth, Gene]
五:Map 接口
1:Map接口
Map 接口不是 Collection 接口的继承。而是从自己的用于维护键-值关联的接口层
次结构入手。按定义,该接口描述了从不重复的键到值的映射。
我们可以把这个接口方法分成三组操作:改变、查询和提供可选视图。
改变操作允许您从映射中添加和除去键-值对。键和值都可以为 null。但是,您不能
把 Map 作为一个键或值添加给自身。
Object put(Object key, Object value)
Object remove(Object key)
void putAll(Map mapping)
void clear()
查询操作允许您检查映射内容:
Object get(Object key)
boolean containsKey(Object key)
boolean containsValue(Object value)
int size()
boolean isEmpty()
最后一组方法允许您把键或值的组作为集合来处理。
public Set keySet()
public Collection values()
public Set entrySet()
因为映射中键的集合必须是唯一的, 您用 Set 支持。 因为映射中值的集合可能不唯一,
您用 Collection 支持。最后一个方法返回一个实现 Map.Entry 接口的元素 Set。
2:Map.Entry 接口
Map 的 entrySet() 方法返回一个实现 Map.Entry 接口的对象集合。集合中每个
对象都是底层 Map 中一个特定的键-值对。
通过这个集合迭代,您可以获得每一条目的键或值并对值进行更改。但是,如果底层
Map 在 Map.Entry 接口的 setValue() 方法外部被修改,此条目集就会变得无效,并
导致迭代器行为未定义。
3:HashMap 类和 TreeMap 类
“集合框架”提供两种常规的 Map 实现:HashMap 和 TreeMap。和所有的具体实现
一样,使用哪种实现取决于您的特定需要。在 Map 中插入、删除和定位元素,HashMap 是
最好的选择。但如果您要按顺序遍历键,那么 TreeMap 会更好。根据集合大小,先把元素
添加到 HashMap,再把这种映射转换成一个用于有序键遍历的 TreeMap 可能更快。使用
HashMap 要求添加的键类明确定义了 hashCode() 实现。有了 TreeMap 实现,添加到
映射的元素一定是可排序的。
HashMap 和 TreeMap 都实现 Cloneable 接口。
Hashtable 类和 Properties 类是 Map 接口的历史实现。
4:映射的使用示例
以下程序演示了具体 Map 类的使用。该程序对自命令行传递的词进行频率计数。
HashMap 起初用于数据存储。后来,映射被转换为 TreeMap 以显示有序的键列列表。
1 import java.util.*; 2 3 4 5 public class MapExample { 6 7 public static void main(String args[]) { 8 9 Map map = new HashMap(); 10 11 Integer ONE = new Integer(1); 12 13 for (int i=0, n=args.length; i<n; i++) { 14 15 String key = args[i]; 16 17 Integer frequency = (Integer)map.get(key); 18 19 if (frequency == null) { 20 21 frequency = ONE; 22 23 } else { 24 25 int value = frequency.intValue(); 26 27 frequency = new Integer(value + 1); 28 29 } 30 31 map.put(key, frequency); 32 33 } 34 35 System.out.println(map); 36 37 Map sortedMap = new TreeMap(map); 38 39 System.out.println(sortedMap); 40 41 } 42 43 }
用 Bill of Rights 的第三篇文章的文本运行程序产生下列输出,请注意有序输出看起
来多么有用!
无序输出:
{prescribed=1, a=1, time=2, any=1, no=1, shall=1, nor=1, peace=1, owner=1,
soldier=1, to=1, the=2, law=1, but=1, manner=1, without=1, house=1, in=4,
by=1, consent=1, war=1, quartered=1, be=2, of=3}
有序输出:
{a=1, any=1, be=2, but=1, by=1, consent=1, house=1, in=4, law=1, manner=1,
no=1, nor=1, of=3, owner=1, peace=1, prescribed=1, quartered=1, shall=1,
soldier=1, the=2, time=2, to=1, war=1, without=1}
六:集合排序
为了用“集合框架”的额外部分把排序支持添加到 Java SDK,版本 1.2,核心 Java 库
作了许多更改。像 String 和 Integer 类如今实现 Comparable 接口以提供自然排序
顺序。对于那些没有自然顺序的类、或者当您想要一个不同于自然顺序的顺序时,您可以实
现 Comparator 接口来定义您自己的。
为了利用排序功能, “集合框架”提供了两种使用该功能的接口:SortedSet 和
SortedMap。
1:Comparable 接口
在 java.lang 包中,Comparable 接口适用于一个类有自然顺序的时候。假定对象
集合是同一类型,该接口允许您把集合排序成自然顺序。
compareTo() 方法比较当前实例和作为参数传入的元素。如果排序过程中当前实例出
现在参数前,就返回某个负值。如果当前实例出现在参数后,则返回正值。否则,返回零。
这里不要求零返回值表示元素相等。零返回值只是表示两个对象排在同一个位置。
在 Java SDK,版本 1.2 中有十四个类实现 Comparable 接口。下表展示了它们的自
然排序。虽然一些类共享同一种自然排序,但只有相互可比的类才能排序。
String 的 compareTo() 方法的文档按词典的形式定义了排序。这意味着比较只是
在文本中被数字化了的字符串值之间,其中文本没有必要按所有语言的字母顺序排序。对于
语言环境特定的排序,通过 CollationKey 使用 Collator。
下面演示了通过 CollationKey 使用 Collator 进行语言环境特定的排序:
1 import java.text.*; 2 3 import java.util.*; 4 5 6 7 public class CollatorTest { 8 9 public static void main(String args[]) { 10 11 Collator collator = Collator.getInstance(); 12 13 CollationKey key1 = collator.getCollationKey("Tom"); 14 15 CollationKey key2 = collator.getCollationKey("tom"); 16 17 CollationKey key3 = collator.getCollationKey("thom"); 18 19 CollationKey key4 = collator.getCollationKey("Thom"); 20 21 CollationKey key5 = collator.getCollationKey("Thomas"); 22 23 24 25 Set set = new TreeSet(); 26 27 set.add(key1); 28 29 set.add(key2); 30 31 set.add(key3); 32 33 set.add(key4); 34 35 set.add(key5); 36 37 printCollection(set); 38 39 } 40 41 static private void printCollection( 42 43 Collection collection) { 44 45 boolean first = true; 46 47 Iterator iterator = collection.iterator(); 48 49 System.out.print("["); 50 51 while (iterator.hasNext()) { 52 53 if (first) { 54 55 first = false; 56 57 } else { 58 59 System.out.print(", "); 60 61 } 62 63 CollationKey key = (CollationKey)iterator.next(); 64 65 System.out.print(key.getSourceString()); 66 67 } 68 69 System.out.println(")"); 70 71 } 72 73 } 74 75 76 77
运行程序产生了以下输出。
[thom, Thom, Thomas, tom, Tom]
如果没有用 Collator,而是直接的存储名字,那么小写的名字会和大写的名字分开显示:
[Thom, Thomas, Tom, thom, tom]
创建您自己的类 Comparable 只是个实现 compareTo() 方法的问题。 通常就是依赖
几个数据成员的自然排序。您自己的类也应该覆盖 equals() 和 hashCode() 以确保两
个相等的对象返回同一个散列码。
2:Comparator 接口
若 一个 类 不能 用 于 实现 java.lang.Comparable , 您 可以 提 供 自己 的
java.util.Comparator 行为。如果您不喜欢缺省的 Comparable 行为,您照样可以提
供自己的 Comparator。
Comparator 的 compare() 方法的返回值和 Comparable 的 compareTo() 方法
的返回值相似。在此情况下,如果排序时第一个元素出现在第二个元素之前,则返回一个负
值。如果第一个元素出现在后,那么返回一个正值。否则,返回零。与 Comparable 相似,
零返回值不表示元素相等。 一个零返回值只是表示两个对象排在同一位置。 由 Comparator
用户决定如何处理。如果两个不相等的元素比较的结果为零,您首先应该确信那就是您要的
结果,然后记录行为。
为了演示,您会发现编写一个新的忽略大小写的 Comparator,代替使用 Collator
进行语言环境特定、忽略大小写的比较会更容易。这样的一种实现如下所示:
1 class CaseInsensitiveComparator implements Comparator { 2 3 public int compare(Object element1, Object element2) { 4 5 String lowerE1 = ((String)element1).toLowerCase(); 6 7 String lowerE2 = ((String)element2).toLowerCase(); 8 9 return lowerE1.compareTo(lowerE2); 10 11 } 12 13 }
因为每个类在某些地方都建立了 Object 子类, 所以这不是您实现 equals() 方法的
必要条件。实际上大多数情况下您不会去这样做。切记该 equals() 方法检查的是
Comparator 实现的等同性,不是处于比较状态下的对象。
Collections 类 有个 预 定义的 Comparator 用 于 重 用。 调 用
Collections.reverseOrder() 返回一个 Comparator,它对逆序实现 Comparable
接口的对象进行排序。
3:SortedSet 接口
“集合框架”提供了个特殊的 Set 接口:SortedSet,它保持元素的有序顺序。
该接口为集的子集和它的两端(即头和尾)提供了访问方法。当您处理列表的子集时,
更改子集会反映到源集。此外,更改源集也会反映在子集上。发生这种情况的原因在于子集
由两端的元素而不是下标元素指定。此外,如果 fromElement 是源集的一部分,它就是
子集的一部分。但如果 toElement 是源集的一部分,它却不是子集的一部分。如果您想
要一个特殊的高端元素 (to-element)在子集中, 您必须找到下一个元素。 对于一个 String
来说,下一个元素是个附带空字符的同一个字符串(string+"\0")。 ;
添加到 SortedSet 的元素必须实现 Comparable,否则您必须给它的实现类的构造
函数提供一个 Comparator:TreeSet(您可以自己实现接口。但是“集合框架”只提供
这样一个具体的实现类。)
为了演示,以下示例使用 Collections 类中逆序的 Comparator。
1 Comparator comparator = Collections.reverseOrder(); 2 3 Set reverseSet = new TreeSet(comparator); 4 5 reverseSet.add("Bernadine"); 6 7 reverseSet.add("Elizabeth"); 8 9 reverseSet.add("Gene"); 10 11 reverseSet.add("Elizabeth"); 12 13 reverseSet.add("Clara"); 14 15 System.out.println(reverseSet); 16 17
运行程序产生了以下输出。
[Gene, Elizabeth, Clara, Bernadine]
因为集必须包含唯一的项,如果添加元素时比较两个元素导致了零返回值(通过
Comparable 的 compareTo() 方法或 Comparator 的 compare() 方法)那么新元素
就没有添加进去。如果两个元素相等,那还好。但如果它们不相等的话,您接下来就应该修
改比较方法,让比较方法和 equals() 方法一致。
使用先前的 CaseInsensitiveComparator 演示这一问题,产生了一个三元素
集:thom、Thomas 和 Tom,而不是可能预期的五个元素。
1 Comparator comparator = new CaseInsensitiveComparator(); 2 3 Set set = new TreeSet(comparator); 4 5 set.add("Tom"); 6 7 8 9 set.add("tom"); 10 11 set.add("thom"); 12 13 set.add("Thom"); 14 15 set.add("Thomas"); 16 17
4:SortedMap 接口
“集合框架”提供了个特殊的Map 接口:SortedMap,它用来保持键的有序顺序。
此接口为映射的子集包括两个端点提供了访问方法。除了排序是作用于映射的键以外,
处理 SortedMap 和处理 SortedSet 一样。 “集合框架”提供的实现类是 TreeMap。
因为对于映射来说,每个键只能对应一个值,如果在添加一个键-值对时比较两个键产
生了零返回值(通过 Comparable 的 compareTo() 方法或通过 Comparator 的
compare() 方法) ,那么,原始键对应值被新的值替代。如果两个元素相等,那还好。但
如果不相等,那么您就应该修改比较方法,让比较方法和 equals() 的效果一致。
七:泛型
1:产生泛型的动机
可以在集合框架(Collection framework)中看到泛型的动机。例如,Map 类允许您向
一个 Map 添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型
(比如 String)的对象。
因为 Map.get() 被定义为返回 Object,所以一般必须将 Map.get() 的结果强制类型
转换为期望的类型,如下面的代码所示:
Map m = new HashMap();
m.put("key", "bag");
String s = (String) m.get("key");
要让程序通过编译,必须将 get() 的结果强制类型转换为 String,并且希望结果真的
是一个 String。但是有可能某人已经在该映射中保存了不是 String 的东西,这样的话,
上面的代码将会抛出 ClassCastException。
理想情况下,您可能会得出这样一个观点,即 m 是一个 Map,它将 String 键映射到
String 值。这可以让您消除代码中的强制类型转换,同时获得一个附加的类型检查层,该
检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。
那么上面的代码使用泛型后,修改如下:
Map<String,String> m = new HashMap<String,String>();
m.put("key","bag");
String s = m.get("key");
2:什么是泛型
泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持
创建可以按类型进行参数化的类。 可以把类型参数看作是使用参数化类型时指定的类型的一
个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
泛型为提高大型程序的类型安全和可维护性带来了很大的潜力。泛型与其他几个 Java
语言特性相互协作,包括增强的 for 循环(有时叫做 foreach 或者 for/in 循环) 、枚举
(enumeration)和自动装箱(autoboxing)。
在定义泛型类或声明泛型类的变量时, 使用尖括号来指定形式类型参数, 称为类型形参,
在调用时传递的实际类型成为类型实参。 类型形参与类型实参之间的关系类似于形式方法参
数与实际方法参数之间的关系,只是类型参数表示类型,而不是表示值,示例如下:
Map<String,String> m = new HashMap<String,String>();
m.put("key","bag");
String s = m.get("key");
注意,必须指定两次类型参数。一次是在声明变量 map 的类型时,另一次是在选择
HashMap 类的参数化以便可以实例化正确类型的一个实例时。
再看一个示例:
Collection<String> col = new ArrayList<String>();
col.add("Javass");
3:使用泛型的好处
(1)类型安全
泛型的主要目标是提高 Java 程序的类型安全。 通过知道使用泛型定义的变量的类型限
制,编译器可以在一个高得多的程度上验证类型假设。
Java 程序中的一种流行技术是定义这样的集合,即它的元素或键是公共类型的,比如
“String 列表”或者“String 到 String 的映射”。 通过在变量声明中捕获这一附加的类
型信息, 泛型允许编译器实施这些附加的类型约束。 类型错误现在就可以在编译时被捕获了,
而不是在运行时当作 ClassCastException 展示出来。 将类型检查从运行时挪到编译时有助
于您更容易找到错误,并可提高程序的可靠性。
(2)消除强制类型转换
泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并
且减少了出错机会。尽管减少强制类型转换可以降低使用泛型类的代码的罗嗦程度,但是声
明泛型变量会带来相应的罗嗦。比较下面两个代码例子。
该代码不使用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = (Integer) li.get(0);
该代码使用泛型:
List<Integer> li = new ArrayList<Integer>();
li.put(new Integer(3));
Integer i = li.get(0);
在简单的程序中使用一次泛型变量不会降低罗嗦程度。 但是对于多次使用泛型变量的大
型程序来说,则可以累积起来降低罗嗦程度。
(3)潜在的性能收益
泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型
的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编
译器这一事实,为未来版本的 JVM 的优化带来可能。
4:命名类型参数
在泛型中,类型参数推荐的命名约定是使用大写的单个字母。使用一个字母可以同现实
中那些具有描述性的,长的实际变量名有所区别。使用大写字母要同变量命名规则一致,并
且要区别于局部变量,方法参数,成员变量,而这些变量常常使用一个小写字母。集合类中,
比如java.util中常常使用类型变量 E代表“Element type”。T 和S 常常用来表示范型变
量名(好像使用i和j作为循环变量一样)。
对于常见的泛型模式,推荐的名称是:
• K —— 键,比如Map的键
• V —— 值,比如 List 和 Set 的内容,或者 Map 中的值
• E —— 异常类,或者集合元素类型
• T —— 泛型
注意:当一个变量被声明为泛型时,只能被实例变量和方法调用(还有内嵌类型)而不
能被静态变量和方法调用。原因很简单,参数化的泛型是一些实例。静态成员是被类的实例
和参数化的类所共享的,所以静态成员不应该有类型参数和他们关联。
5:泛型不是协变的
关于泛型的混淆,一个常见的来源就是假设它们像数组一样是协变的。如果 A 扩展 B,
那么 A 的数组也是 B 的数组,并且完全可以在需要 B[] 的地方使用 A[]: 如下:
Integer[] intArray = new Integer[10];
Number[] numberArray = intArray;
上面的代码是有效的, 因为一个 Integer 是 一个 Number,因而一个 Integer 数组是
一个 Number 数组。但是对于泛型来说则不然。下面的代码是无效的:
List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid
其实泛型不是协变的,List<Number>不是 List<Integer>的父类型。
最初,大多数 Java 程序员觉得这缺少协变很烦人,或者甚至是“坏的(broken)”,
但是之所以这样有一个很好的原因。如果可以将 List<Integer> 赋给 List<Number>,下面
的代码就会违背泛型应该提供的类型安全:
List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid
numberList.add(new Float(3.1415));
因为 intList 和 numberList 都是有别名的,如果允许的话,上面的代码就会让您将
不是 Integers 的东西放进 intList 中。但是,您有一个更加灵活的方式来定义泛型。
假设您具有该方法:
void printList(List l) {
for (Object o : l){
System.out.println(o);
}
}
编译通过,但是如果试图用 List<Integer> 调用它,则会得到警告。出现警告是因为,
您将泛型(List<Integer>)传递给一个只承诺将它当作 List(所谓的原始类型)的方法,
这将破坏使用泛型的类型安全。
如果试图编写像下面这样的方法,那么将会怎么样?
void printList(List<Object> l) {
for (Object o : l){
System.out.println(o);
}
}
它仍然不会通过编译,因为一个 List<Integer> 不是一个 List<Object>。这才真正烦
人 —— 现在您的泛型版本还没有普通的非泛型版本有用!
解决方案是使用类型通配符。
6:类型通配符
把上面的例子修改如下:
void printList(List<?> l) {
for (Object o : l){
System.out.println(o);
}
}
上面代码中的问号是一个类型通配符。它读作“问号”。List<?> 是任何泛型 List 的
父类型,所以可以将 List<Object>、List<Integer> 或 List<List<List<AnyObject>>> 传
递给 printList()。
引入了类型通配符,这让您可以声明 List<?> 类型的变量。您可以对这样的 List 做
什么呢?非常方便,可以从中检索元素,但是不能添加元素。原因不是编译器知道哪些方法
修改列表哪些方法不修改列表,而是(大多数)变化的方法比不变化的方法需要更多的类型
信息。下面的代码则工作得很好:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
System.out.println(lu.get(0));
为什么该代码能工作呢?对于 lu,编译器一点都不知道 List 的类型参数的值。但是
编译器比较聪明,它可以做一些类型推理。在本例中,它推断未知的类型参数必须扩展
Object。(这个特定的推理没有太大的跳跃,但是编译器可以作出一些非常令人佩服的类型
推理,所以它让您调用 List.get() 并推断返回类型为 Object。
另一方面,下面的代码不能工作:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.add(new Integer(43)); // error
在本例中,对于 lu,编译器不能对 List 的类型参数作出足够严密的推理,以确定将
Integer 传递给 List.add() 是类型安全的。所以编译器将不允许您这么做。
以免您仍然认为编译器知道哪些方法更改列表的内容哪些不更改列表内容, 请注意下面
的代码将能工作,因为它不依赖于编译器必须知道关于 lu 的类型参数的任何信息:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.clear();
7:泛型方法
通过在类的定义中添加一个形式类型参数列表, 可以将类泛型化。 方法也可以被泛型化,
不管它们定义在其中的类是不是泛型化的。
泛型类可在多个方法签名间实施类型约束。在 List<V> 中, 类型参数 V 出现在 get()、
add()、contains() 等方法的签名中。当创建一个 Map<K, V> 类型的变量时,您就在方法
之间宣称一个类型约束。您传递给 add() 的值将与 get() 返回的值的类型相同。
类似地,之所以声明泛型方法,一般是因为您想要在该方法的多个参数之间宣称一个类
型约束。例如,下面代码中的 ifThenElse() 方法,根据它的第一个参数的布尔值,它将返
回第二个或第三个参数:
public <T> T ifThenElse(boolean b, T first, T second) {
return b ? first : second;
}
注意,您可以调用 ifThenElse(),而不用显式地告诉编译器,您想要 T 的什么值。编译器不必显式地被告知 T 将具有什么值;它只知道这些值都必须相同。编译器允许您调用下面的代码,因为编译器可以使用类型推理来推断出,替代 T 的 String 满足所有的类型约束:
String s = ifThenElse(b, "a", "b");
类似地,您可以调用:
Integer i = ifThenElse(b, new Integer(1), new Integer(2));
但是,编译器不允许下面的代码,因为没有类型会满足所需的类型约束:
String s = ifThenElse(b, "pi", new Float(3.14));
为什么您选择使用泛型方法,而不是将类型 T 添加到类定义呢?(至少)有两种情况
应该这样做:
• 当泛型方法是静态的时,这种情况下不能使用类类型参数。
• 当 T 上的类型约束对于方法真正是局部的时,这意味着没有在相同类的另一个 方
法签名中使用相同 类型 T 的约束。通过使得泛型方法的类型参数对于方法是局部
的,可以简化封闭类型的签名。
8:有限制类型(extends新的含义)
在 Java 语言引入泛型之前,extends 关键字总是意味着创建一个新的继承自另一个类
或接口的类或接口。引入泛型之后,extends 关键字有了另一个含意。将 extends 用在类
型参数的定义中(Collection<T extends Number>)或者通配符类型参数中(Collection<?
extends Number>) ,用来表示有限制类型,限制前面的“T”或者“?”必须是 Number 类型
或者Number类型的子类型。
当使用 extends 来指示类型参数限制时,不需要子类-父类关系,只需要子类型-父类
型关系。 还要记住,有限制类型不需要是该限制的严格子类型;也可以是该限制。换句话说,
对 于 Collection<? extends Number> , 您 可以 赋 给它 Collection<Number> 、Collection<Integer>、Collection<Long>、Collection<Float> 等等。
9:类型与类
泛型的引入使得Java语言中的类型系统更加复杂。 以前, 该语言具有两种类型 —— 引
用类型和基本类型。对于引用类型,类型和类的概念基本上可以互换,术语子类型和子类也
可以互换。
随着泛型的引入, 类型和类之间的关系变得更加复杂。 List<Integer> 和 List<Object>是不同的类型,但是却是相同的类。尽管 Integer 扩展 Object,但是 List<Integer> 不是 List<Object>,并且不能赋给 List<Object> 或者强制转换成 List<Object>。
另一方面,现在有了一个新的古怪的类型叫做 List<?>,它是 List<Integer> 和List<Object> 的父类。并且有一个更加古怪的 List<? extends Number>。
类型层次的结构和形状也变得复杂得多。类型和类不再几乎是相同的东西了。
基本上可以这么说:一个类具有多种类型,而且数量是无限的。
练习实践课:
程序 1:
通讯录
需求:将朋友信息定义为一个对象,将对象加入List中并从List中显示。
目标:
1、 List基本用法;
2、 对象的封装。
程序:
1 //: Friend.java 2 3 package com.useful.java.part7; 4 5 6 7 public class Friend { 8 9 //名字 10 11 private String name; 12 13 //邮箱 14 15 private String email; 16 17 //手机号 18 19 private String mobile; 20 21 public String getName() { 22 23 return name; 24 25 } 26 27 28 29 public void setName(String name) { 30 31 this.name = name; 32 33 } 34 35 36 37 public void setEmail(String email) { 38 39 this.email = email; 40 41 } 42 43 44 45 public String getEmail() { 46 47 return email; 48 49 } 50 51 52 53 public void setMobile(String mobile) { 54 55 this.mobile = mobile; 56 57 } 58 59 60 61 public String getMobile() { 62 63 return mobile; 64 65 66 67 } 68 69 } 70 71 72 73 //: TestList.java 74 75 package com.useful.java.part7; 76 77 78 79 import java.util.*; 80 81 82 83 public class TestList { 84 85 //friendList是一个全局变量,用来存放所有的Friend对象 86 87 List friendList = new ArrayList(); 88 89 90 91 public TestList() { 92 93 } 94 95 96 97 public void addFriend(Friend friend){ 98 99 //调用add方法,往 list中添加对象 100 101 friendList.add(friend); 102 103 } 104 105 106 107 public int getFriendSize(){ 108 109 //size是list对象的一个方法 110 111 return friendList.size(); 112 113 } 114 115 116 117 public void insertFriend(String name,String email,String mobile){ 118 119 //新建一个Friend对象 120 121 Friend friend = new Friend(); 122 123 friend.setName(name); 124 125 friend.setEmail(email); 126 127 friend.setMobile(mobile); 128 129 this.addFriend(friend); 130 131 } 132 133 134 135 public void showAllFriend(){ 136 137 System.out.println("name " + "email " + "mobile"); 138 139 System.out.println("---------------------------------"); 140 141 Friend friend = new Friend(); 142 143 //循环显示,list也是从 0开始算起 144 145 for(int i = 0 ; i < this.getFriendSize(); i++){ 146 147 friend = (Friend)friendList.get(i); 148 149 System.out.print(friend.getName() + " "); 150 151 System.out.print(friend.getEmail() + " "); 152 153 System.out.println(friend.getMobile() + " "); 154 155 156 157 } 158 159 System.out.println("---------------------------------"); 160 161 System.out.println("size : " + this.getFriendSize()); 162 163 } 164 165 166 167 public static void main(String[] args) { 168 169 TestList testList1 = new TestList(); 170 171 //添加两条记录 172 173 testList1.insertFriend("holen","holen@263.net","13910118302"); 174 175 testList1.insertFriend("frank","frank@263.net","13800008888"); 176 177 testList1.showAllFriend(); 178 179 } 180 181 } 182 183
说明:
1、 List主要用于有顺序的对象集合;
2、 程序运行如下:
3、 Java 是面向对象的语言,在进行程序思维时,要注意增减有面向对象的思维方
式。
程序 2:
无序的对象
需求:将一些无序的对象置入一个对象集中。
目标:
1、 HashTable基本用法;
2、 “键值对”的意义。
程序:
1 //: TestHashTable.java 2 3 package com.useful.java.part7; 4 5 6 7 import java.util.Hashtable; 8 9 10 11 public class TestHashTable { 12 13 Hashtable hashTable = new Hashtable(); 14 15 String strObjectA = new String("this is ObjectA"); 16 17 String strObjectB = new String("This is ObjectB"); 18 19 Integer intObjectC = new Integer(2); 20 21 Integer intObjectD = new Integer(2345); 22 23 24 25 public TestHashTable() { 26 27 } 28 29 30 31 public void addObject(){ 32 33 hashTable.put("my1",strObjectA); 34 35 hashTable.put(intObjectC,strObjectB); 36 37 hashTable.put(intObjectD,strObjectA); 38 39 hashTable.put("cgcg",intObjectC); 40 41 hashTable.put(strObjectA,intObjectC); 42 43 } 44 45 46 47 public void printHashtable(){ 48 49 System.out.println(hashTable); 50 51 } 52 53 54 55 public static void main(String[] args) { 56 57 TestHashTable testHashTable1 = new TestHashTable(); 58 59 testHashTable1.addObject(); 60 61 testHashTable1.printHashtable(); 62 63 } 64 65 } 66 67 68 69
说明:
1、对于无序的对象集合,HashTable、HashMap将是不错的选择;
2、程序运行如下:
作业
1:定义一个类,类里面有一个属性col,类型是集合类型 Collection,实现下列方法:可以
向col里面添加数据、修改数据、查询数据、删除数据。也就是把这个 col当作一个数据存
储的容器,对其实现数据的增删改查的方法。
2:把上题的Collection改成使用Map实现