文章目录
- 9.1 Java集合框架
- 9.1.1 集合接口与实现分离
- 9.1.3 迭代器
- 9.1.4 泛型实用方法
- 9.2 集合框架中的接口
- 9.3.1 链表
- 9.3.2 数组列表
- 9.3.3散列集
- 9.3.4 树集
- 9.3.5 队列与双端队列
- 9.3.6 优先队列
- 9.4 映射
- 9.4.1 基本映射操作
- 9.4.2 更新映射条目
- 9.4.3 映射视图
- 9.4.4 弱散列映射
- 9.4.5 链接散列集与映射
- 9.4.6 枚举集与映射
- 9.4.7 标识散列映射
- 9.5 视图与包装器
- 9.5.1 小集合
- 9.5.2 子范围
- 9.5.3 不可修改的视图
- 9.5.4 同步视图
- 9.5.5 检查型视图
- 9.5.6 关于可选操作的说明
- 9.6算法
- 9.6.1 为什么使用泛型算法
- 9.6.2 排序与混排
- 9.6.3 二分查找
- 9.6.4 简单算法
- 9.6.5批操作
9.1 Java集合框架
9.1.1 集合接口与实现分离
- 举例:队列接口,可以有循环数组和链表两种实现。
- 可以用接口指向两种实现中的任意一种。这样也可以轻松从CircularArrayQueue实现换成
LinkedListQueue实现,而不需要修改后续代码如expressLane.add。
Queue\<Customer> expressLane = new CircularArrayQueue<>(100);
Queue\<Customer> expressLane = new LinkedListQueue<>(); //换实现
expressLane.add(new Customer("Harry"));
...
-
根据实际情况选实现:循环数组要比链表更高效,但循环数组是一个有界集合,即容量有限。优先选择循环数组,如果程序中要收集的对象数量没有上限,就最好使用链表来实现。
-
以Abstract开头的类,例如,AbstractQueue。这些类是为类库实现者而设计的。如果想要实现自己的队列类,扩展AbstractQueue类要比实现Queue接口中的所有方法轻松得多。
9.1.2 Collection接口
public interface Collection<E> extends Iterable<E> {
//两个基本方法
…
boolean add(E e);
Iterator iterator();
…
}
9.1.3 迭代器
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
- “foreach”循环可以处理任何实现了Iterable接口的对象,这个接口只包含一个抽象
方法。编译器简单地将“foreach”循环转换为带有迭代器的循环。
public interface Iterable<E>
{
Iterator<E> iterator();
}
- Collection接口扩展了Iterable接口。因此,对于标准类库中的任何集合都可以使用“for
each”循环。 - 也可以不写循环,而是调用forEachRemaining方法并提供一个lambda表达式。
iterator.forEachRemaining(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
- 迭代器iterator不保证访问顺序,除非集合类型可以保证元素顺序。
如果迭代处理一个ArrayList,迭代器将从索引0开始,每迭代一次,索引值加1。不过,如果访问HashSet中的元素,会按照一种基本上随机的顺序获得元素。
- Java的迭代器只能通过next移动。可以想象Java迭代器位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
- Iterator接口的remove方法将会删除上次调用next方法时返回的元素。例如,可以如下删除一个字符串集合中的第一个元素:
Iterator<String> it = coll.iterator();
it.next(); // skip over the first element:“Amy”
it.remove(); // now remove “Amy”
- next方法和remove方法调用之间存在依赖性。如果调用remove之前没有调
用next,将是不合法的。
9.1.4 泛型实用方法
- 当然,如果实现Collection接口的每一个类都要提供如此多的例行方法,这将是一
件很烦人的事情。为了能够让实现者更容易地实现这个接口,Java类库提供了一个类
AbstractCollection,它保持基础方法size和iterator仍为抽象方法,但是为实现者实现了其他例行方法。例如:
- 这种Abstract类做法有些过时了。这些方法最好是Collection接口的默认方法。
子类可以重写接口的默认方法和static方法。
- default boolean removeIf(Predicate<? super E> filter)方法
coll.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length()==3;
}
});
9.2 集合框架中的接口
- 集合有两个基本接口: Collection和Map。
- List是一个有序集合。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。后面这种方法称为随机访问。与之不同,使用迭代器访问时,必须顺序地访问元素。
虽然LinkedList也有get(int i)方法通过索引访问。但链表不支持快速随机访
问,get(i)必须从经过i-1个元素,所以最好使用迭代器来遍历。
为了避免对链表完成随机访问操作,Java 1.4引入了 一个标记接口RandomAccess。这个接口不包含任何方法,不过可以用它来测试一个特定的集合是否支持高效的随机访问:
if (coll instanceof RandomAccess)
{
//use random access algorithm
}
else
{
// use sequential access algorithm
}
- ListIterator接口是Iterator的一个子接口。它定义了一个方法用于在迭代器位置前面增加一个元素
void add(E element)
。ListIterator接口还可以用hasPrevious逆向访问集合。 - Set接口不允许重复元素。要适当地定义集的equals方法:只要两个集包含同样的元素就认为它们是相等的,而不要求这些元素有同样的顺序。hashCode方法的定义要保证包含相同元素的两个集会得到相同的散列码。
- SortedSet和SortedMap接口会提供用于排序的比较器对象。
- Java6引入了接口NavigableSet和NavigableMap,其中包含一些用于搜索和遍历有序集和映射的方法。(理想情况下,这些方法本应直接包含在SortedSet和SortedMap接口中。)TreeSet和TreeMap类实现了这些接口。
9.3 具体集合
9.3.1 链表
- Java所有链表实际上都是双向链表。
- 链表与泛型集合区别:链表是有序的,添加的元素位置很重要。由于迭代器描述了集合中的位置,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。例如,下一节将要讨论的集(set)数据类型中,元素是完全无序的。因此,Iterator接口中没有add方法。实际上,集合类库提供了一个子接口ListIterator,其中包含add方法:
interface ListIterator<E> extends Iterator<E>
{
void add(E element);
…
} - add方法在迭代器位置之前添加一个新对象。可以多次调用add方法,依次添加到迭代器当前位置之前。
不能连续调用两次remove。必须同时next。
- 当用一个刚由listiterator方法返回并指向链表表头的迭代器调用add操作时,新添加的元素将变成列表的新表头。
- 当迭代器越过链表的最后一个元素时(即hasNext返回false),添加的元素将成为列表的新表尾。
- 在调用next之后,remove方法确实与Backspace键一样会删除迭代器左侧的元素。但是,如果调用了 previous,就会将右侧的元素删除。
- set方法用一个新元素替换调用next或previous方法返回的上一个元素。
- 如果一个迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的某个方法修改了,就会抛出一个ConcurrentModificationException异常。
- 为了避免发生并发修改异常,请遵循这样一个简单的规则:可以根据需要为一个集合关联多个迭代器,前提是这些迭代器只能读取集合。或者,只关联一个能同时读写的迭代器。
- 有一种简单的方法可以检测到并发修改。集合可以跟踪更改操作(诸如添加或删除元素) 的次数。每个迭代器都会为它负责的更改操作维护一个单独的更改操作数。在每个迭代器方法的开始处,迭代器会检查它自己的更改操作数是否与集合的更改操作数相等。如果不一致,就抛出一个 Concurrent ModificationException 异常。
- 虽然LinkedList也有get(int i)方法通过索引访问。但链表不支持快速随机访
问,get(i)必须从经过i-1个元素,所以最好使用迭代器来遍历。 - 列表迭代器接口可以告诉你当前位置的索引。nextindex方法返回下一次调用next方法时所返回元素的整数索引;previouslndex方法返回下一次调用previous方法时所返回元素的整数索引。这两个方法的效率非常高,因为有一个迭代器保持着当前位置的计数值。
9.3.2 数组列表
- 需要动态数组时,Vector与ArrayL选择:Vector类的所有方法都是同步的,但同步操作需要大量时间。不需要同步时使用ArrayList , 需要同步时用Vector。
9.3.3散列集
- 快速查找元素的数据结构。其缺点是无法控制元素出现的次序。
- 自己的类自己实现hashCode方法,与equals方法兼容。
- 最重要的问题是要能够快速地计算出散列码。
- 散列表用链表数组实现。每个列表被称为桶。散列码与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。
例如,如果某个对象的散列码为76 268,并且有128个桶,那么这个对象应该保存在第108号桶中(因为76 268%128的余数是108)。
- 有时候会遇到桶已经被填充的情况。这种现象被称为散列冲突。
在Java8中,桶满时会从链表变为平衡二叉树。
- 通常,将桶数设置为预计元素个数的7 5 % 〜 1 5 0 %。标准类库使用的桶数是2的幂,默认值为16。
- 装填因子默认值为0.75,说明表中已经填满了 75%以上,就会自动再散列,新表的桶数是原来的两倍。
- HashSet类实现了基于散列表的集。只有不关心集合中元素的顺序时才应该使用HashSet。
- HashSet迭代器将依次访问所有的桶。访问元素是无序的随机的。
9.3.4 树集
- TreeSet类比散列集有所改进。树集是一个有序集合。可以以任意顺序将元素插人到集合中。在对集合进行遍历时,值将自动地按照排序后的顺序呈现。
- 排序是用红黑树实现的。
- 将元素添加到树中要比散列表中慢。但查找元素只需要log2 N。
- 要使用树集,元素必须实现Comparable接口,或者构造集时提供Comparator比较器。
- 只有在需要元素有序的时候,才用树集,否则用散列表实现快速查找某元素。
- 树集的排序顺序必须是全序。也就是说,任意两个元素都必须是可比的,并且只有在两个元素相等时结果才为0。
- 对于某些数据来说,对其进行排序要比给出一个散列函数更加困难。散列函数只需要将对象适当地打乱存放,而比较函数必须精确地区分各个对象。
矩形确实有一种排序方式(按照坐标的词典顺序排序),但这很牵强,而且计算很烦琐。相反地,Rectangle类已经定义了散列函数,它直接对坐标进行散列。
9.3.5 队列与双端队列
- Deque接口,ArrayDeque和LinkedList类实现了这个接口。
9.3.6 优先队列
- 优先级队列用堆实现。remove方法获得当前优先队列中最小的元素。
- 优先队列既可以保存实现了 Comparable接口的类对象,也可以保存构造器中提供的Comparator对象。
9.4 映射
9.4.1 基本映射操作
- HashMap和TreeMap。这两个类都实现了 Map接口。
- 散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。与键关联的值不进行散列或比较。
- 散列稍微快一些,如果不需要按照有序的顺序访问键,最好选择散列映射。
- 没有出现在映射中的键,可以使用一个好的默认值: getOrDefault。
- 键必须是唯一的。如果对同一个键调用两次put方法,第二个值就会取代第一个值。实际上,put将返回与这个键参数关联的上一个值。
9.4.2 更新映射条目
键第一次出现:counts.put(word, counts.get(word) + 1);
会报NullPointerException异常。
- 用getOrDefault方法:
counts.put(word, counts.getOrDefault(word, 0) + 1);
- 用putIfAbsent方法。只有当键原本不存在(或者映射到null)时才会放入一个值。
counts.putIfAbsent(word, 0);
counts.put(word, counts.get(word) + 1); // now we know that get will succeed
- 比2更好的是,用merge方法。
counts.merge(word, 1, Integer::sum);
如果键原先不存在,将把word与1关联,否则使用Integer: :sum函数组合原值和1。
9.4.3 映射视图
- 映射视图是实现了 Collection接口或某个子接口的对象。集合框架不认为映射本身是一个集合。不过,可以得到映射的视图。
- 有3种视图:键集、值集合(可重复)以及键/值对集。
- 需要说明的是,keyset不是HashSet或TreeSet,而是实现了 Set接口的另外某个类的对象。Set接口扩展了 Collection接口。因此,可以像使用任何集合一样使用keySet。
- 以枚举一个映射的所有键。
- 如果想同时查看键和值,可以通过枚举映射条目来避免查找值。
- 还可以用map.forEach方法。
- KeySet调用迭代器的remove方法,实际上会从映射中删除这个键和与它关联的值。但不能调用add方法。EntrySet也只能remove,不能add。
9.4.4 弱散列映射
- WeakHashMap类是为了解决:如果程序中的任何部分不会再用这个 键,垃圾回收器却无法从映射中删除这个键/值对。
- 垃圾回收器会跟踪活动的对象。只要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收。
- 不能用垃圾处理器回收,只能由程序负责从长期存活的映射表中删除那些无用的值;或者,使用WeakHashMap与垃圾回收器协同工作一起删除键/值对。
- 工作原理:WeakHashMap使用弱引用(weak references)保存键。正常情况下,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器也会将其回收,但会将引用这个对象的弱引用放人一个队列。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进人队列意味着这个键不再被他人使用,并且已经回收。于是WeakHashMap将删除相关联的映射条目。
就是说,如果发现WeakHashMap中的对象,只有WeakHashMap引用他,垃圾回收器就回收,还会通过队列告诉WeakHashMap这个对象被回收了。WeakHashMap就会删除这个对象。
9.4.5 链接散列集与映射
- LinkedHashSet和LinkedHashMap类会记住插人元素项的顺序。这样就可以避免散列表中的项看起来顺序是随机的。在表中插人元素项时,就会并人到双向链表中。
- 链接散列映射可以使用访问顺序而不是插人顺序来迭代处理映射条目。每次调用 get或put时,受到影响的项将从当前的位置删除,并放到项链表的尾部(只影响项在链表中 的位置,而散列表的桶不会受影响。映射条目总是在键散列码对应的桶中)
LinkedHashMap<K, V>(initialcapacity, loadFactor, true)
。 - 访问顺序对于实现缓存的“最近最少使用”原则十分重要。可以得到表的一个迭代器,并删除它枚举的前几个元素。这些项是近期最少使用的几个元素。甚至可以让这一过程自动化。构造LinkedHashMap的一个子类,然后覆盖下面这个方法:
protected boolean removeEldestEntry(Map.Entry<K, V> eldest)
9.4.6 枚举集与映射
-
EnumSet是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1。
-
EnumSet类没有公共的构造器。要使用静态工厂方法构造这个集:
-
EnumMap是一个键类型为枚举类型的映射。它可以直接且高效地实现为一个值数组。需要
在构造器中指定键类型:
var personlnCharge = new EnumMap<Weekday, Employee>(Weekday.class);
9.4.7 标识散列映射
类IdentityHashMap有特殊的用途。在这个类中,键的散列值不是用hashCode函数计算的, 而是用System.identityHashCode方法计算的。这是Object.hashCode根据对象的内存地址计算散列码时所使用的方法。而且,在对两个对象进行比较时,IdentityHashMap类使用**=**,而不使用 equals。也就是说,不同的键对象即使内容相同,也被视为不同的对象。在实现对象遍历算法 (如对象串行化)时,这个类非常有用,可以用来跟踪哪些对象已经遍历过。
9.5 视图与包装器
映射类keySet方法,初看起来,好像这个方法创建了一个新集,并填入映射中的所有键,然后返回这个集。但是,情况并非如此。实际上,keyset方法返回一个实现了 Set接口的类对象,由这个类的方法操纵原映射。这种集合称为视图。
9.5.1 小集合
- Java 9引人了一些静态方法,可以生成给定元素的集或列表,以及给定键/值对的映射。
List<String> names = List.of("Peter", "Paul", "Mary");
Set<Integer> numbers = Set.of(2, 3, 5);
Map<String, Integer> scores = Map.of("Peter", 2, "Paul", 3, "Mary", 5);
-
List和Set接口有11个方法,分别有0到10个参数,另外还有一个参数个数可变的of方法。提供这种特定性是为了提高效率。
-
对于Map接口,则无法提供一个参数可变的版本,因为参数类型会在键和值类型之间交替。不过它有一个静态方法ofEntries,能接受任意多个Map.Entry<K,V>对象(可以用静态方法entry创建这些对象)。
-
这些集合对象是不可修改的。如果试图改变它们的内容,会导致一UnsupportedOperationException 异常。如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器:
-
CoUections. nCopies (n, anObj ect)会返回一个实现了 List接口的不可变的对象。
List<String> settings=Collections.nCopies(10,"DEFAULT");
这样存储开销很小。对象只存储一次。 -
of方法是Java9新引入的。之前有一个静态方法Arrays.asList,它会返回一个可更改但是大小不可变的列表。
-
Java没有Pair类,有些程序员会使用Map. Entry作为对组(pair),但这种做法并不好。在Java 9之前,这会很麻烦,你必须使用new AbstractMap.SimplelmmutableEntry<>(first, second)构造对象。不过现在可以调用 Map. entry (first, second)。
9.5.2 子范围
- subList方法获子范围的视图。对子范围应用任何操作,而且操作会自动反映到整个列表。
- 对有序集合SortedSet和SortedMap。可以使用排序顺序而不是元素位置建立子范围。分别用sortedSet和SortedMap函数。
SortedSet<E> subSet(E from, E to) //大于等于from且小于to的所有元素构成的子集
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)
9.5.3 不可修改的视图
- Collections类可以生成集合的不可修改视图。
- 假设想要让你的某些代码查看(但不能修改)一个集合的内容,就可以进行以下操作:
LinkedList<String> staff = new LinkedList<>();
lookAt(Collections.unmodifiableList(staff)); //lookAt函数里写只能查看staff的代码
- 不可修改的视图并不是集合本身不可更改。依旧可以用staff对集合进行修改。
- 由于视图只是包装了接口而不是具体的集合对象,所以只能访问接口中定义的方法。
9.5.4 同步视图
类库的设计者使用视图机制来确保常规集合是线程安全的,而没有实现线程安全的集合类。例如,Collections类的静态synchronizedMap方法可以将任何一个映射转换成有同步访问方法的Map: Collections.synchronizedMap(new HashMap<String, Integer>());
9.5.5 检查型视图
- “检查型”视图用来对泛型类型可能出现的问题提供调试支持。
- 看下面的错误,在运行add时检测不到,直到get方法强转时才出现。
ArrayList<String> strings = new ArrayList<>();
ArrayList rawList=strings;
rawList.add("123");
rawList.add(new Date());
System.out.println(rawList); //Object的ArrayList存放了一个String和一个Date
System.out.println(rawList.get(1));
String str= (String) rawList.get(1); //强转类型转换异常,把Date强转成String
3. 检查型视图可以探测这类问题。下面定义了一个安全列表。这个视图的add方法将检查插人的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个ClassCastException。
List<String> safeStrings = Collections.checkedList(strings, String.class);
检查型视图受限于虚拟机可以完成的运行时检查。例如,对于ArrayListr<Pairr<String>>,由于虚拟机有一个“原始”Pair类,所以无法阻止插入Pair<Date>。
9.5.6 关于可选操作的说明
- 在集合和迭代器接口的API文档中,许多方法描述为“可选操作”。这看起来与接口的概念有冲突。毕竟,接口的设计目的难道不就是明确一个类必须实现的方法吗?确实,从理论的角度看,这种安排不太令人满意。一个更好的解决方案是为只读视图和不能改变集合大小的视图建立单独的接口。不过,这将会使接口的数量增至原来的三倍,这让类库设计者无法接受。
- 自己写的时候不要用可选操作。
9.6算法
9.6.1 为什么使用泛型算法
- 只需要实现一次。
- 将max方法实现为能够接收任何实现了 Collection接口的对象。
public static <T extends Comparable> T max(Collection<T> c)
9.6.2 排序与混排
- Collections类中的sort方法可以对实现了 List接口的集合进行排序。这个方法假定列表元素实现了Comparable接口。
- 如果想采用其他方式对列表进行排序, 可以使用List接口的sort方法并传人一个Comparator对象。
- Collections.reverseOrder()这个方法将返回一个比较器,比较器则返回b.compareTo(a),Comparator.reverseOrder()将返回Collections.reverseOrder()。例如,
staff.sort(Comparator.reverseOrder())
这个方法将根据元素类型的compareTo方法所给定的排序顺序,按逆序对列表staff中的元素进行排序。同样地,staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed())
将按工资逆序排序。 - 使用一种归并排序对链表高效地排序。不过,Java是将所有元素转人一个数组,对数组进行排序,然后,再将排序后的序列复制回链表。
- Collections类有一个算法shuffle,其功能与排序刚好相反,它会随机地混排列表中元素
的顺序。如果提供的列表没有实现RandomAccess接口,shuffle方法会将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。
9.6.3 二分查找
- Collections类的binarysearch方法实现了这个算法。必须提供实现List接口的集合。
- 如果binarysearch方法返回一个非负的值,这表示匹配对象的索引。
如果返回负值,则表示没有匹配的元素。插人的位置是insertionpoint=-i-1;if(i<0) c.add(-i-1,element);
这并不是简单的-i,因为0值是不确定的。如果插入位置是-i,没办法表示没找到,插入位置为0,因为返回-0 = 0,会认为是找到了在0位置。
- 如果为binarysearch算法提供一个链表,它将自动地退化为线性查找。
9.6.4 简单算法
见API。