- 通常,程序总是根据运行时才知道的某些条件去创建新对象,需要在任意时刻和任意位置创建任意数量的对象,不能依靠创建命名的引用来持有对象,而数组具有固定的尺寸的限制显得过于受限。
- Java的集合类,又称容器,提供完善的方法来保存对象,其中基本的类型是List、Set、Queue和Map。
- Java容器类都可以自动地调整自己的尺寸。
- Java中容器类没有直接的关键字支持。
1 泛型和类型安全的容器
- JAVA SE5之前编译器允许你向容器中插入不正确的类型。
- ArrayList容器:add()插入对象,get()访问对象,size()返回对象数量。
- @SuppressWarnings(“unchecked”)注解表示不受检查的异常的警告信息应该被抑制。
- 如果一个类没有显式地声明继承自哪个类,那么它将自动地继承自Object。所以未指定预定义泛型时,会默认使用Object。
- 应用预定的泛型,在容器类名后紧跟尖括号,其中尖括号括起来的是类型参数(可以有多个),形如
holdingName<typeName>
。通过get()返回的对象也不会是默认值Object,而是指定的类型。 - 通过使用泛型,编译器会检查放置到容器中的对象类型,可以在编译期防止将错误类型的对象放置到容器中。
- 向上转型可以像作用于其他类型一样作用于泛型。
- 直接输出对象时,将调用Object的toString()方法。该方法将打印类名,后面跟随该对象的散列码的无符号十六制表示、(这个散列码通过hashCode()方法产生)。
2 基本概念
- Collection,一个独立元素的序列,一种存放一组对象的方式,元素服从一条或多条规则。
- List,按照插入的顺序保存元素。
- Set,不能有重复元素。
- Queue,按照排队规则来确定对象产生的顺序。
- Map,一组成对的“键值对”对象,又称映射表、字典,允许你使用键来查找值。
- 使用接口的目的在于如果要修改实现,只需要在创建处修改它。但某些类具有额外的功能,如果你要使用这些方法,就不能将它向上转型为更通用的接口。
- 所有的Collection都可以用foreach语法遍历。
3 添加一组元素
- Arrays.asList()方法接受一个数组或是一个用逗号分隔的元素列表,并将其转换为一个List对象。
- Collections.addAll()方法接受一个Collection对象,以及一个数组或是一个用逗号分割的列表,将元素添加到Collection中。
- Collection的构造器可以接受另一个Collection,用它来将自身初始化,因此可以使用Arrays.asList()来为这个构造器产生输入。Collections().addAll方法运行更快,是首选方式。
- Collection.addAll()成员方法只能接受另一个Collection对象作为参数,不够灵活。
- Arrays.asList()的输出,可以当作List,但是其底层表示的是数组,不能调整尺寸。
- Arrays.asList()由参数列表决定返回的List的目标类型。可以在Arrays.asList()中间插入一条“线索”,以告诉编译器对于产生的List类型的实际目标类型,称为显示类型参数说明。
4 容器的打印
- 打印数组必须用Arrays.asList(),但打印容器无需任何帮组。
- Collection打印出来的内容用放括号括住,每个元素由逗号隔开。Map则用大括号括住,键与值由等号联系(键左值右)。
- ArrayList和LinkedList都是List类型,区别在于执行某些类型的操作时的性能,而LinkedList包含的操作也更多。
- HashSet、TreeSet和LinkedHashSet都是Set类型,HashSet用相当复杂的方式来存储元素,提供最快的查找速度;TreeSet按照比较结果升序保存对象;LinkedHashSet按照被添加的顺序保存元素。
- 对于每个键,Map只接受存储一次。
- Map.put(key,value)方法将增加一个值,并将它与某个键关联起来。
- Map.get(key)方法将产生与这个键相关联的值。
- HashMap提供最快的查找速度;TreeMap按照比较结果的升序保存键;LinkedHashMap按照插入顺序保存键。
5 List
- List将元素维护在特定的序列中,可以在List的中间插入和移除元素。
- ArrayList,长于随机访问元素,在List的中间插入和移除元素时较慢。
- LinkedList,在List的中间插入和移除元素时较快,提供了优化的顺序访问,在随机访问方面较慢。
- List是一种可修改的序列,允许被创建后添加元素、移除元素或者自我调整尺寸。
- contains()方法确定某个对象是否在列表中。
- remove()方法移除最近的一个对象,移除失败返回false。
- indexOf()返回对象在List中所处位置的索引编号,不存在则返回-1。
- List进行remove()和indexOf()等很多方法时都会用到equals()方法(根类Object的一部分),List的行为根据equals()的行为而有所变化。
- List可通过add()方法在中间插入元素。
- sublist()方法从较大的列表中创建出一个片段,对返回的列表的修改都会反映到初始列表中。
- containsAll()方法确定一组列表中的所有对象是否在列表中,顺序并不重要。
- Collections.shuffle()将打乱列表的元素顺序。
- retainAll()方法是“交集”操作,保留同时在两个表中的元素。
- removeAll()方法移除在参数中的所有所有元素。
- set()方法修改指定索引处的元素。
- addAll()方法插入新的列表。
- isEmpty()判断List是否为空,clear()方法清空列表。
- toArray()方法将任意的Collection转换为一个数组。无参版本返回Object数组,传递目标类型数据,则产生指定类型的数据。
- Integer列表要remove指定对象,必须用Integer对象,或者Integer.valueOf()方法,不能直接用数字,否则编译器会认为使用索引移除对象。
- Integer和String对象,只要数值相同,equals()就会返回true。
6 迭代器
- 持有事物是容器最基本的工作。
- 要使用容器,必须对容器的确切类型编程。
- 迭代器是一个对象,它的工作是遍历并选择序列中的对象。能够将遍历序列的操作与序列底层的结构分离。
- Iterator是轻量级对象,只能单向移动。
- 容器的iterator()方法返回一个Iterator。
- next()获得序列中的下一个元素。
- hasNext()检查序列中是否还有元素。
- remove()将迭代器新近返回的元素删除,之前必须先调用next()。
6.1 ListIterator
- ListIterator是Iterator的子类型,只能用于List类的访问,可以双向移动。
- 可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引。
- 可以使用set()方法替换它访问过的最后一个元素。
- listIterator(n)方法创建指向列表索引为n的元素处的ListIterator。
- nextIndex()返回后一个元素的索引,nextPrevious()返回前一个元素的索引。
7 LinkedList
- LinkedList是List的子类型,还添加了可以使其用作栈、队列或双端队列的方法。
- 这些方法中有些彼此之间只是名称有些差异,或只存在些许差异。
- getFirst()和element()返回列表的第一个元素,列表为空,则抛出NoSuchElementException。peek()稍有差异,列表为空,返回null。
- removeFirst()和remove()移除并返回列表的第一个元素,为空抛出NoSucnElementException。poll()为空返回null。
- add()和addLast()将某个元素插入到列表尾部。
- addFirst()和offer()将某个元素插入到列表头部。
- removeLast()移除并返回列表的最后一个元素。
8 Stack
- 栈是后进先出(LIFO)的容器,LinkedList具有能够直接实现栈的所有功能的方法。
- 范型,类名之后的是一个参数化类型,其中的类型参数T,将在类被使用时将会被实际类型替换的参数。
- 如果只需要栈的行为,继承LinkedList就不合适了,应该使用组合。
- 当多个包的类命名冲突时,就需要完整指定包名。
9 Set
- Set最常用于测试归属性,基于对象的值来确定归属性。
- Set具有与Collection完全一样的接口。
- TreeSet将元素存储在红黑树中;HashSet使用的是散列函数;LinkedHashSet因为查询速度的原因也使用了散列,但是看起来它使用了链表来维护元素的插入顺序。
- 使用contains()测试Set的归属性。
- TreeSet排序是按字典序进行的(大小写被划分到不同的组中)。可以向TreeSet的构造器传入String.CASE_INSENTIVE_ORDER比较器,按字母序排序。
10 Map
- get()根据键返回值,如果键不在容器中,返回null。
- put()方法向容器插入键值对。
- containsKey()方法确定某个键是否在容器中。
- containsValue()方法确定某个值是否在容器中。
- Map与数组和其他的Collection一样,可以很容易地扩展到多维。
- keySet()方法产生由容器中的所有键组成的Set。
- entrySet()方法返回Entry对象列表,每个Entry对象存储一组键值对。
- getKey()方法返回Entry对象的键值。
- getValue()方法返回Entry对象的值值。
11 Queue
- 队列是先进先出(FIFO)的容器,常被当作一种可靠的将对象从程序的某个区域传输到另一个区域的途径。
- LinkedHashList实现了Queue接口,可以向上转型为Queue,窄化了对LinkedHashList的方法的访问权限。
- offer()将一个元素插入队尾,失败返回false。
- peek()和element()返回对头(不移除)。队列为空时,peek()返回null,element()抛出NoSuchElementException异常。
- poll()和remove()移除并返回对头。队列为空时,pool()返回null,remove()抛出NoSuchElementException异常。
11.1 PriorityQueue
- 先进先出队列声明下一个元素应该是等待时间最长的元素。
- 优先级队列声明下一个弹出元素是最需要的元素。
- PrioriryQueue默认的排序将使用对象在队列中的自然排序,可以通过提供Comparator来修改这个顺序。
- 重复是允许的,最小的值拥有最高的优先级(如果是String,空格也可以算作值,并且比字母的优先级高)。
- 如果想在PriorityQueue中使用自己的类,就必须包括额外的功能以产生自然排序,或者提供自己的Comparator。
12 Collection和Iterator
- Collection是描述所有序列容器的共性的根接口。
- AbstractCollection类提供了Collection的默认实现。
- Collection接口和Iterator都可以将读取元素与底层容器的特定实现解耦。
- Collection实现了Iterabl接口,对于容器类,使用foreach更方便。
- 如果实现Collection,就必须实现iterator()。
- 当要实现一个不是Collection的外部类时,去实现Collection接口非常麻烦,使用Iterator会方便很多。
- 生成Iterator是将队列与消费队列的方法连接在一起耦合度最小的方式。
13 Foreach与迭代器
- foreach语法用于数组和任何Collection对象。
- Iterable接口包含了一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。
- 任何实现了Iterable的类,都可以将它用于foreach语句中。
- iterator()方法可以返回实现Iterator的匿名内部类的实例。
- System.getenv()返回装填了系统环境变量配置的Map。
- 数组不是Iterable,也不存在从数组到Iterable的自动转换。
13.1 适配器方法惯用法
- 适配器方法,适用于当有一个接口需要另一个接口时。
- 定义返回Iterable接口的方法,在方法体内实现新的Iterable。
- Arrays.asList()产生的List对象会使用底层数组作为其物理实现。
14 总结
- 数组将数字与对象联系起来,保存类型明确的对象,可以保存基本类型。数组一旦生成,其容量就不能改变。
- Collection保存单一的元素,而Map保存相关联的键值对,都会自动调整其尺寸。容器不能持有基本类型,但自动包装机制可以解决这个问题。
- List建立数字索引与对象的关联,数组和List都是排好序的容器,List能够自动扩充容量。
- 如果要进行大量的随机访问,就使用ArrayList;如果要经常从表中间插入或删除元素,则使用LinkedList。
- 各种Queue和栈的行为,由LinkedList提供支持。
- Map是一种将对象与对象相关联的设计。HashMap用来快速访问;TreeMap保持“键”始终处于排序状态;LinkedHashSet保持元素插入的顺序,也通过散列提供了快速访问能力。
- Set不接受重复元素。HashSet提供最快的查询速度,TreeSet保存元素处于排序状态,LinkedHashSet以插入顺序保存元素。
- 新程序中不应该使用过时的Vector、Hashtable和Stack。
疑问和解答
1. 为什么Arrays.asList()不能进行add()、remove()操作?
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
- 其中返回ArrayList是定义在Arrays文件内的私有类。
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
- 该ArrayList中未重写add(),remove()方法,继承自AbstractList类。查看AbstractList中这两个方法的定义。
public boolean add(E e) {
add(size(), e);
return true;
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
- 可知Arrays.asList()返回的数组是个“只读”数组。
- 再看看Arrays中的ArrayList的构造器。
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
- Objects的requireNonNull()方法。
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
- 可知这个Arrays.asList()返回的列表的底层实现是传入的数组,为了不破坏原数组,故设计成“只读”。
- 接下来复原下Arrays.asList()的经典错误。
public class TestAsList
{
public static void main(String[] args)
{
int[] manyInts = { 1, 2, 3, 4, 5, 6 };
List list1 = Arrays.asList(1, 2, 3, 4, 5, 6);
System.out.println("list1.size(): " + list1.size());
List list2 = Arrays.asList(manyInts);
System.out.println("list2.size(): " + list2.size());
}
}
- 由于泛型只接受对象,直接传入基本类型数组,不会进行自动包装,而是将数组当成对象。
2. 先获取容器的迭代器,对容器进行add操作后,再使用之前获取的迭代器访问容器,编译器运行时报错的原因。
- 由于Iterator对象都在具体容器中才实现,所以只能查看已实现的容器ArrayList中的Itr。
- 在实现的Iterator对象中next()和remove()方法都在最前面进行检查checkForComodification()。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
int expectedModCount = modCount;
protected transient int modCount = 0;
- 当ArrayList增加或删除元素时,modCount增加,而exceperModCount不变,迭代器再进行操作就会抛出异常。
3. 为什么HashMap插入数字会自动从小到大排序,而且课本例题示例却没有?
public int hashCode() {
return Integer.hashCode(value);
}
4. 如何对Map容器内元素排序?
- 调用Map的entrySet(),用List列表接收,再用Collections进行排序。
5. 范型和泛型的区别?
- 范型(Paradigm)和泛型(Generic)主要是学术上的区别,好像没有很大的区别,没有找到很好的资料来解决该问题。