Java 中文官方教程 2022 版(二十七)(2)https://developer.aliyun.com/article/1486849
干扰
流操作中的 Lambda 表达式不应该干扰。当流的源在流水线处理流时被修改时就会发生干扰。例如,下面的代码尝试连接List
listOfStrings
中包含的字符串。然而,它会抛出ConcurrentModificationException
:
try { List<String> listOfStrings = new ArrayList<>(Arrays.asList("one", "two")); // This will fail as the peek operation will attempt to add the // string "three" to the source after the terminal operation has // commenced. String concatenatedString = listOfStrings .stream() // Don't do this! Interference occurs here. .peek(s -> listOfStrings.add("three")) .reduce((a, b) -> a + " " + b) .get(); System.out.println("Concatenated string: " + concatenatedString); } catch (Exception e) { System.out.println("Exception caught: " + e.toString()); }
此示例使用reduce
操作将listOfStrings
中包含的字符串连接成一个Optional
值,reduce
是一个终端操作。然而,此处的管道调用了中间操作peek
,它试图向listOfStrings
添加一个新元素。请记住,所有中间操作都是惰性的。这意味着在此示例中,管道在调用操作get
时开始执行,并在get
操作完成时结束执行。peek
操作的参数在管道执行过程中尝试修改流源,这会导致 Java 运行时抛出ConcurrentModificationException
。
有状态的 Lambda 表达式
避免在流操作中将有状态的 lambda 表达式用作参数。有状态的 lambda 表达式是指其结果取决于在管道执行过程中可能发生变化的任何状态。以下示例使用map
中间操作将List
listOfIntegers
中的元素添加到新的List
实例中。它分别使用串行流和并行流执行两次:
List<Integer> serialStorage = new ArrayList<>(); System.out.println("Serial stream:"); listOfIntegers .stream() // Don't do this! It uses a stateful lambda expression. .map(e -> { serialStorage.add(e); return e; }) .forEachOrdered(e -> System.out.print(e + " ")); System.out.println(""); serialStorage .stream() .forEachOrdered(e -> System.out.print(e + " ")); System.out.println(""); System.out.println("Parallel stream:"); List<Integer> parallelStorage = Collections.synchronizedList( new ArrayList<>()); listOfIntegers .parallelStream() // Don't do this! It uses a stateful lambda expression. .map(e -> { parallelStorage.add(e); return e; }) .forEachOrdered(e -> System.out.print(e + " ")); System.out.println(""); parallelStorage .stream() .forEachOrdered(e -> System.out.print(e + " ")); System.out.println("");
Lambda 表达式e -> { parallelStorage.add(e); return e; }
是一个有状态的 lambda 表达式。其结果可能每次运行代码时都会有所不同。此示例打印如下内容:
Serial stream: 8 7 6 5 4 3 2 1 8 7 6 5 4 3 2 1 Parallel stream: 8 7 6 5 4 3 2 1 1 3 6 2 4 5 8 7
操作forEachOrdered
按照流指定的顺序处理元素,无论流是串行还是并行执行。然而,当流并行执行时,map
操作处理由 Java 运行时和编译器指定的流元素。因此,lambda 表达式e -> { parallelStorage.add(e); return e; }
向List
parallelStorage
添加元素的顺序可能每次运行代码时都会有所不同。为了获得确定性和可预测的结果,请确保流操作中的 lambda 表达式参数不是有状态的。
注意:此示例调用了方法synchronizedList
,以使List
parallelStorage
线程安全。请记住,集合不是线程安全的。这意味着多个线程不应同时访问特定集合。假设在创建parallelStorage
时未调用方法synchronizedList
:
List<Integer> parallelStorage = new ArrayList<>();
该示例行为不稳定,因为多个线程访问和修改parallelStorage
而没有像同步这样的机制来安排特定线程何时可以访问List
实例。因此,该示例可能打印类似以下内容的输出:
Parallel stream: 8 7 6 5 4 3 2 1 null 3 5 4 7 8 1 2
问题和练习:聚合操作
原文:
docs.oracle.com/javase/tutorial/collections/streams/QandE/questions.html
问题
- 一系列的聚合操作被称为 ___。
- 每个流水线包含零个或多个 ___ 操作。
- 每个流水线以一个 ___ 操作结束。
- 什么样的操作以另一个流作为输出?
- 描述
forEach
聚合操作与增强的for
语句或迭代器之间的一个区别。 - 真或假:流类似于集合,因为它是一个存储元素的数据结构。
- 在这段代码中识别中间操作和终端操作:
double average = roster .stream() .filter(p -> p.getGender() == Person.Sex.MALE) .mapToInt(Person::getAge) .average() .getAsDouble();
- 代码
p -> p.getGender() == Person.Sex.MALE
是什么的一个例子? - 代码
Person::getAge
是什么的一个例子? - 将流的内容组合并返回一个值的终端操作被称为什么?
Stream.reduce
方法和Stream.collect
方法之间的一个重要区别是什么?- 如果你想处理一个包含姓名的流,提取男性姓名,并将它们存储在一个新的
List
中,那么Stream.reduce
或Stream.collect
是最合适的操作? - 真或假:聚合操作使得可以在非线程安全的集合中实现并行性。
- 流通常是串行的,除非另有规定。如何请求以并行方式处理流?
练习
- 将以下增强的
for
语句编写为使用 lambda 表达式的流水线。提示:使用filter
中间操作和forEach
终端操作。
for (Person p : roster) { if (p.getGender() == Person.Sex.MALE) { System.out.println(p.getName()); } }
- 将以下代码转换为一个使用 lambda 表达式和聚合操作而不是嵌套
for
循环的新实现。提示:创建一个依次调用filter
、sorted
和collect
操作的流水线。
List<Album> favs = new ArrayList<>(); for (Album a : albums) { boolean hasFavorite = false; for (Track t : a.tracks) { if (t.rating >= 4) { hasFavorite = true; break; } } if (hasFavorite) favs.add(a); } Collections.sort(favs, new Comparator<Album>() { public int compare(Album a1, Album a2) { return a1.name.compareTo(a2.name); }});
检查你的答案。
教训:实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/index.html
实现是用于存储集合的数据对象,实现了接口部分中描述的接口。本课程描述了以下类型的实现:
- 通用实现是最常用的实现,设计用于日常使用。它们在标题为通用实现的表中总结。
- 特殊用途实现设计用于特殊情况,并显示非标准性能特征、使用限制或行为。
- 并发实现旨在支持高并发性,通常以牺牲单线程性能为代价。这些实现是
java.util.concurrent
包的一部分。 - 包装实现通常与其他类型的实现结合使用,通常是通用实现,以提供附加或受限功能。
- 便利实现是迷你实现,通常通过静态工厂方法提供,为特殊集合提供方便、高效的替代方案(例如,单例集合)。
- 抽象实现是骨架实现,有助于构建自定义实现,稍后在自定义集合实现部分中描述。这是一个高级主题,不是特别困难,但相对较少的人会需要这样做。
通用实现总结如下表所示。
通用实现
接口 | 哈希表实现 | 可调整大小数组实现 | 树实现 | 链表实现 | 哈希表 + 链表实现 |
Set |
HashSet |
TreeSet |
LinkedHashSet |
||
List |
ArrayList |
LinkedList |
|||
Queue |
|||||
Deque |
ArrayDeque |
LinkedList |
|||
Map |
HashMap |
TreeMap |
LinkedHashMap |
正如您从表中所看到的,Java 集合框架提供了几个通用实现的Set
、List
和Map
接口。在每种情况下,一个实现——HashSet
、ArrayList
和HashMap
——显然是大多数应用程序中要使用的实现,其他条件相等。请注意,SortedSet
和SortedMap
接口在表中没有行。每个接口都有一个实现(TreeSet
和TreeMap
),并列在Set
和Map
行中。有两个通用的Queue
实现——LinkedList
,也是List
实现,和PriorityQueue
,在表中被省略。这两个实现提供非常不同的语义:LinkedList
提供 FIFO 语义,而PriorityQueue
根据其值对元素进行排序。
每个通用实现都提供其接口中包含的所有可选操作。所有允许null
元素、键和值。没有同步(线程安全)。所有都有快速失败迭代器,在迭代期间检测到非法并发修改,并快速干净地失败,而不是在未来的某个不确定时间冒险出现任意、非确定性的行为。所有都是Serializable
,并支持公共clone
方法。
这些实现不同步的事实代表了与过去的断裂:传统集合Vector
和Hashtable
是同步的。采取当前方法是因为在同步没有好处时集合经常被使用。这些用途包括单线程使用、只读使用以及作为执行自身同步的较大数据对象的一部分使用。一般来说,良好的 API 设计实践是不让用户为他们不使用的功能付费。此外,不必要的同步可能在某些情况下导致死锁。
如果你需要线程安全的集合,包装器实现部分描述的同步包装器允许任何集合转换为同步集合。因此,同步对于通用实现是可选的,而对于传统实现是强制的。此外,java.util.concurrent
包提供了BlockingQueue
接口的并发实现,它扩展了Queue
,以及ConcurrentMap
接口的并发实现,它扩展了Map
。这些实现比单纯的同步实现具有更高的并发性。
通常情况下,你应该考虑接口,而不是实现。这就是为什么本节中没有编程示例。在很大程度上,实现的选择只影响性能。如接口部分所述,首选的风格是在创建Collection
时选择一个实现,并立即将新集合分配给相应接口类型的变量(或将集合传递给期望接口类型参数的方法)。通过这种方式,程序不会依赖于给定实现中添加的任何方法,使程序员可以自由更改实现,只要性能或行为细节需要。
接下来的部分简要讨论了实现。使用诸如常数时间、对数、线性、n log(n)和二次等词语描述了实现的性能,以指代执行操作的时间复杂度的渐近上界。所有这些都是相当复杂的术语,如果你不知道它们的含义也没关系。如果你对更多信息感兴趣,请参考任何一本优秀的算法教材。要记住的一件事是,这种性能指标有其局限性。有时,名义上更慢的实现可能更快。如果有疑问,就要测量性能!
集合实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/set.html
Set
实现分为通用实现和特殊实现两类。
通用集合实现
有三种通用的Set
实现 — HashSet
, TreeSet
, 和 LinkedHashSet
。在这三种中选择哪一种通常很简单。HashSet
比TreeSet
快得多(大部分操作的时间复杂度是常数时间对数时间),但不提供排序保证。如果你需要使用SortedSet
接口中的操作,或者需要按值排序的迭代,使用TreeSet
;否则,使用HashSet
。很可能大部分时间你会使用HashSet
。
LinkedHashSet
在某种意义上介于HashSet
和TreeSet
之间。作为一个带有链表的哈希表,它提供了插入顺序的迭代(最近插入的到最近插入的)并且运行速度几乎和HashSet
一样快。LinkedHashSet
实现避免了HashSet
提供的未指定、通常混乱的排序,而又不会增加TreeSet
的成本。
关于HashSet
值得注意的一点是,迭代在条目数和桶数(容量)的总和上是线性的。因此,选择一个初始容量太高会浪费空间和时间。另一方面,选择一个初始容量太低会浪费时间,因为每次强制增加容量时都需要复制数据结构。如果你不指定初始容量,默认值是 16。过去,选择一个质数作为初始容量有一些优势。但现在不再成立。在内部,容量总是向上舍入为 2 的幂。初始容量是通过使用int
构造函数指定的。以下代码行分配了一个初始容量为 64 的HashSet
。
Set<String> s = new HashSet<String>(64);
HashSet
类还有一个称为负载因子的调整参数。如果你非常关心你的HashSet
的空间消耗,阅读HashSet
文档以获取更多信息。否则,只需接受默认值;这几乎总是正确的做法。
如果你接受默认的负载因子但想指定一个初始容量,选择一个大约是你期望集合增长到两倍大小的数字。如果你的猜测完全错误,可能会浪费一点空间、时间或两者,但这不太可能成为一个大问题。
LinkedHashSet
具有与HashSet
相同的调整参数,但迭代时间不受容量影响。TreeSet
没有调整参数。
特殊用途的 Set 实现
有两个特殊用途的Set
实现 — EnumSet
和 CopyOnWriteArraySet
。
EnumSet
是用于枚举类型的高性能Set
实现。枚举集合的所有成员必须是相同的枚举类型。在内部,它由一个位向量表示,通常是一个long
。枚举集合支持对枚举类型范围的迭代。例如,给定一周中的工作日的枚举声明,你可以迭代工作日。EnumSet
类提供了一个静态工厂,使其易于使用。
for (Day d : EnumSet.range(Day.MONDAY, Day.FRIDAY)) System.out.println(d);
枚举集合也为传统位标志提供了丰富的、类型安全的替代品。
EnumSet.of(Style.BOLD, Style.ITALIC)
CopyOnWriteArraySet
是一个由写时复制数组支持的Set
实现。所有的变动操作,比如add
、set
和remove
,都是通过创建数组的新副本来实现的;永远不需要加锁。即使在元素插入和删除的同时进行迭代也是安全的。与大多数Set
实现不同,add
、remove
和contains
方法所需的时间与集合大小成正比。这种实现仅适用于很少修改但频繁迭代的集合。它非常适合维护必须防止重复的事件处理程序列表。
列表实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/list.html
List
实现分为通用和特殊用途的实现。
通用列表实现
有两种通用的List
实现——ArrayList
和LinkedList
。大多数情况下,你可能会使用ArrayList
,它提供常数时间的位置访问,并且非常快速。它不必为List
中的每个元素分配一个节点对象,并且在需要同时移动多个元素时可以利用System.arraycopy
。将ArrayList
视为没有同步开销的Vector
。
如果你经常在List
的开头添加元素或者迭代List
以删除其内部的元素,你应该考虑使用LinkedList
。这些操作在LinkedList
中需要常数时间,在ArrayList
中需要线性时间。但你会在性能上付出很大的代价。在LinkedList
中,位置访问需要线性时间,在ArrayList
中需要常数时间。此外,LinkedList
的常数因子要糟糕得多。如果你认为你想使用LinkedList
,在做出选择之前用LinkedList
和ArrayList
测量你的应用程序的性能;ArrayList
通常更快。
ArrayList
有一个调整参数——初始容量,指的是ArrayList
在增长之前可以容纳的元素数量。LinkedList
没有调整参数,但有七个可选操作,其中之一是clone
。另外六个是addFirst
、getFirst
、removeFirst
、addLast
、getLast
和removeLast
。LinkedList
还实现了Queue
接口。
特殊用途的列表实现
CopyOnWriteArrayList
是一个由写入时复制数组支持的List
实现。这种实现与CopyOnWriteArraySet
类似。即使在迭代期间,也不需要同步,并且迭代器永远不会抛出ConcurrentModificationException
。这种实现非常适合维护事件处理程序列表,其中变化不频繁,遍历频繁且可能耗时。
如果你需要同步,Vector
比使用Collections.synchronizedList
同步的ArrayList
稍快一些。但Vector
有很多遗留操作,所以一定要小心,始终使用List
接口操作Vector
,否则你将无法在以后替换实现。
如果你的List
大小是固定的 — 也就是说,你永远不会使用remove
、add
或者除了containsAll
之外的任何批量操作 — 那么你有第三个选项,绝对值得考虑。查看方便实现部分的Arrays.asList
获取更多信息。
Map 实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/map.html
Map
实现分为通用、特殊和并发实现。
通用 Map 实现
三种通用Map
实现是HashMap
、TreeMap
和LinkedHashMap
。如果需要SortedMap
操作或基于键排序的Collection
-view 迭代,请使用TreeMap
;如果希望最大速度且不关心迭代顺序,请使用HashMap
;如果希望接近HashMap
性能且插入顺序迭代,请使用LinkedHashMap
。在这方面,Map
的情况类似于Set
。同样,Set Implementations 部分中的其他所有内容也适用于Map
实现。
LinkedHashMap
提供了两个LinkedHashSet
不可用的功能。当您创建一个LinkedHashMap
时,您可以根据键访问而不是插入对其进行排序。换句话说,仅查找与键关联的值会将该键移到地图的末尾。此外,LinkedHashMap
提供了removeEldestEntry
方法,可以被覆盖以在向地图添加新映射时自动实施删除过时映射的策略。这使得实现自定义缓存非常容易。
例如,这个覆盖将允许地图增长到多达 100 个条目,然后每次添加新条目时都会删除最老的条目,保持 100 个条目的稳定状态。
private static final int MAX_ENTRIES = 100; protected boolean removeEldestEntry(Map.Entry eldest) { return size() > MAX_ENTRIES; }
特殊用途 Map 实现
有三种特殊用途的 Map 实现 — EnumMap
、WeakHashMap
和IdentityHashMap
。EnumMap
,内部实现为array
,是用于枚举键的高性能Map
实现。此实现将Map
接口的丰富性和安全性与接近数组的速度结合在一起。如果要将枚举映射到值,应始终优先使用EnumMap
而不是数组。
WeakHashMap
是Map
接口的一个实现,只存储对其键的弱引用。只存储弱引用允许在其键不再在WeakHashMap
之外被引用时,键值对可以被垃圾回收。这个类提供了利用弱引用功能的最简单方法。它对于实现“注册表样”数据结构非常有用,其中一个条目的实用性在其键不再被任何线程引用时消失。
IdentityHashMap
是基于哈希表的基于身份的Map
实现。这个类对于保持拓扑结构的对象图转换非常有用,比如序列化或深拷贝。为了执行这样的转换,你需要维护一个基于身份的“节点表”,用于跟踪哪些对象已经被看到。基于身份的映射也用于在动态调试器和类似系统中维护对象到元信息的映射。最后,基于身份的映射对于阻止由于故意扭曲的equals
方法而导致的“欺骗攻击”非常有用,因为IdentityHashMap
永远不会在其键上调用equals
方法。这个实现的一个额外好处是它很快。
并发映射实现
java.util.concurrent
包含了ConcurrentMap
接口,它通过原子的putIfAbsent
、remove
和replace
方法扩展了Map
,以及该接口的ConcurrentHashMap
实现。
ConcurrentHashMap
是一个高度并发、高性能的哈希表实现。在执行检索操作时,此实现永远不会阻塞,并允许客户端选择更新的并发级别。它旨在作为Hashtable
的一个可替换项:除了实现ConcurrentMap
外,它还支持所有Hashtable
特有的传统方法。再次强调,如果你不需要传统操作,请小心使用ConcurrentMap
接口来操作它。
队列实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/queue.html
Queue
实现分为通用和并发实现。
通用队列实现
如前一节所述,LinkedList
实现了 Queue
接口,为 add
、poll
等提供先进先出(FIFO)队列操作。
PriorityQueue
类是基于 堆 数据结构的优先队列。此队列根据在构造时指定的顺序对元素进行排序,可以是元素的自然顺序或由显式 Comparator
强加的顺序。
队列检索操作 — poll
、remove
、peek
和 element
— 访问队列头部的元素。队列的 头部 是相对于指定顺序的最小元素。如果多个元素具有最小值,则头部是这些元素之一;平局将被任意打破。
PriorityQueue
及其迭代器实现了 Collection
和 Iterator
接口的所有可选方法。在 iterator
方法中提供的迭代器不能保证以任何特定顺序遍历 PriorityQueue
的元素。对于有序遍历,请考虑使用 Arrays.sort(pq.toArray())
。
并发队列实现
java.util.concurrent
包含一组同步的 Queue
接口和类。BlockingQueue
扩展了 Queue
,具有在检索元素时等待队列变得非空以及在存储元素时等待队列中有空间可用的操作。该接口由以下类实现:
LinkedBlockingQueue
— 由链表节点支持的可选有界 FIFO 阻塞队列ArrayBlockingQueue
— 由数组支持的有界 FIFO 阻塞队列PriorityBlockingQueue
— 由堆支持的无界阻塞优先级队列DelayQueue
— 由堆支持的基于时间的调度队列SynchronousQueue
— 使用BlockingQueue
接口的简单会合机制
在 JDK 7 中,TransferQueue
是一个专门的BlockingQueue
,其中向队列添加元素的代码可以选择等待(阻塞),直到另一个线程中的代码检索元素。TransferQueue
只有一个实现:
LinkedTransferQueue
— 基于链表节点的无界TransferQueue
Deque 实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/deque.html
Deque
接口,发音为*“deck”*, 代表双端队列。Deque
接口可以被实现为各种类型的Collections
。Deque
接口的实现被分为通用和并发实现。
通用 Deque 实现
通用实现包括LinkedList
和 ArrayDeque
类。Deque
接口支持在两端插入、删除和检索元素。ArrayDeque
类是Deque
接口的可调整大小数组实现,而LinkedList
类是列表实现。
Deque
接口中的基本插入、删除和检索操作有addFirst
、addLast
、removeFirst
、removeLast
、getFirst
和 getLast
。addFirst
方法在头部添加元素,而addLast
方法在Deque
实例的尾部添加元素。
LinkedList
实现比ArrayDeque
实现更灵活。LinkedList
实现了所有可选的列表操作。LinkedList
实现允许null
元素,但ArrayDeque
实现不允许。
就效率而言,ArrayDeque
在两端的添加和删除操作上比LinkedList
更高效。在迭代过程中,LinkedList
实现中最好的操作是删除当前元素。LinkedList
实现不是理想的迭代结构。
LinkedList
实现比ArrayDeque
实现消耗更多内存。对于ArrayDeque
实例的遍历,可以使用以下任意一种:
foreach
foreach
是一种快速且适用于各种列表的方法。
ArrayDeque<String> aDeque = new ArrayDeque<String>(); . . . for (String str : aDeque) { System.out.println(str); }
迭代器
Iterator
可用于所有类型的数据的所有类型列表的正向遍历。
ArrayDeque<String> aDeque = new ArrayDeque<String>(); . . . for (Iterator<String> iter = aDeque.iterator(); iter.hasNext(); ) { System.out.println(iter.next()); }
ArrayDeque
类在本教程中用于实现Deque
接口。本教程中使用的示例完整代码在ArrayDequeSample
中可用。LinkedList
和 ArrayDeque
类都不支持多线程并发访问。
并发 Deque 实现
LinkedBlockingDeque
类是Deque
接口的并发实现。如果双端队列为空,则takeFirst
和 takeLast
等方法会等待直到元素变为可用,然后检索并移除相同的元素。
包装器实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/wrapper.html
包装器实现将所有真正的工作委托给指定的集合,但在该集合提供的功能之上添加额外的功能。对于设计模式爱好者,这是装饰者模式的一个例子。虽然这可能看起来有点奇特,但实际上非常简单。
这些实现是匿名的;而不是提供一个公共类,库提供了一个静态工厂方法。所有这些实现都在 Collections
类中,该类仅包含静态方法。
同步包装器
同步包装器为任意集合添加了自动同步(线程安全)。六个核心集合接口 — Collection
, Set
, List
, Map
, SortedSet
, 和 SortedMap
— 都有一个静态工厂方法。
public static <T> Collection<T> synchronizedCollection(Collection<T> c); public static <T> Set<T> synchronizedSet(Set<T> s); public static <T> List<T> synchronizedList(List<T> list); public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m); public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s); public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
这些方法中的每一个都返回由指定集合支持的同步(线程安全)Collection
。为了保证串行访问,所有对支持集合的访问必须通过返回的集合完成。确保这一点的简单方法是不保留对支持集合的引用。使用以下技巧创建同步集合。
List<Type> list = Collections.synchronizedList(new ArrayList<Type>());
以这种方式创建的集合与通常同步的集合(如 Vector
)一样线程安全。
面对并发访问时,在迭代时,用户必须手动对返回的集合进行同步。原因是迭代是通过对集合的多次调用完成的,这些调用必须组合成单个原子操作。以下是迭代包装同步集合的惯用法。
Collection<Type> c = Collections.synchronizedCollection(myCollection); synchronized(c) { for (Type e : c) foo(e); }
如果使用显式迭代器,则必须在 synchronized
块内调用 iterator
方法。不遵循此建议可能导致不确定的行为。在同步 Map
的 Collection
视图上进行迭代的惯用法类似。在迭代任何 Collection
视图时,用户必须同步在同步的 Map
上,而不是在 Collection
视图本身上进行同步,如下例所示。
Map<KeyType, ValType> m = Collections.synchronizedMap(new HashMap<KeyType, ValType>()); ... Set<KeyType> s = m.keySet(); ... // Synchronizing on m, not s! synchronized(m) { while (KeyType k : s) foo(k); }
使用包装器实现的一个小缺点是,您无法执行包装实现的任何非接口操作。因此,在前面的List
示例中,您无法在包装的ArrayList
上调用ArrayList
的ensureCapacity
操作。
不可修改的包装器
与添加功能到包装集合的同步包装器不同,不可修改的包装器会剥夺功能。特别是,它们剥夺了通过拦截所有可能修改集合的操作并抛出UnsupportedOperationException
来修改集合的能力。不可修改的包装器有两个主要用途,如下所示:
- 使集合在构建后变为不可变。在这种情况下,最好不要保留对支持集合的引用。这绝对保证了不可变性。
- 允许某些客户端对您的数据结构进行只读访问。您保留对支持集合的引用,但分发对包装器的引用。这样,客户端可以查看但不能修改,而您保持完全访问权限。
与同步包装器一样,每个六个核心Collection
接口都有一个静态工厂方法。
public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c); public static <T> Set<T> unmodifiableSet(Set<? extends T> s); public static <T> List<T> unmodifiableList(List<? extends T> list); public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m); public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s); public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);
检查接口包装器
Collections.checked
接口包装器用于与泛型集合一起使用。这些实现返回指定集合的动态类型安全视图,如果客户端尝试添加错误类型的元素,则会抛出ClassCastException
。语言中的泛型机制提供了编译时(静态)类型检查,但有可能击败此机制。动态类型安全视图完全消除了这种可能性。
便利实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/convenience.html
这一部分描述了几个迷你实现,当你不需要它们的全部功能时,它们可能比通用实现更方便、更高效。本节中的所有实现都是通过静态工厂方法而不是public
类提供的。
数组的列表视图
Arrays.asList
方法返回其数组参数的List
视图。对List
的更改会写入数组,反之亦然。集合的大小与数组相同且不可更改。如果在List
上调用add
或remove
方法,将导致UnsupportedOperationException
。
这种实现的正常用法是作为基于数组和基于集合的 API 之间的桥梁。它允许你将数组传递给期望Collection
或List
的方法。然而,这种实现还有另一个用途。如果你需要一个固定大小的List
,它比任何通用List
实现更高效。这就是惯用法。
List<String> list = Arrays.asList(new String[size]);
注意,不会保留对支持数组的引用。
不可变多副本列表
有时你会需要一个由多个相同元素副本组成的不可变List
。Collections.nCopies
方法返回这样一个列表。这种实现有两个主要用途。第一个是初始化一个新创建的List
;例如,假设你想要一个最初由 1,000 个null
元素组成的ArrayList
。下面的咒语就能实现。
List<Type> list = new ArrayList<Type>(Collections.nCopies(1000, (Type)null));
当然,每个元素的初始值不一定是null
。第二个主要用途是扩展现有的List
。例如,假设你想要向List
的末尾添加 69 个字符串"fruit bat"
的副本。不清楚为什么你想要做这样的事情,但让我们假设你确实想要。下面是如何做到的。
lovablePets.addAll(Collections.nCopies(69, "fruit bat"));
通过使用同时接受索引和Collection
的addAll
形式,你可以将新元素添加到List
的中间而不是末尾。
Java 中文官方教程 2022 版(二十七)(4)https://developer.aliyun.com/article/1486855