目录
List、Set和Map接口上方便的静态工厂方法允许轻松创建不可修改的列表、集合和映射。
如果不能添加、删除或替换元素,则集合被认为是不可修改的。创建集合的不可修改实例后,只要存在对它的引用,它就保存相同的数据。
可修改的集合必须维护记录的数据,以支持未来的修改。这增加了存储在可修改集合中的数据的开销。不可修改的集合不需要这个额外的簿记数据。因为集合永远不需要修改,所以集合中包含的数据可以更密集地打包。不可修改的集合实例通常比包含相同数据的可修改的集合实例消耗更少的内存。
一、用例
使用不可修改集合还是可修改集合取决于集合中的数据。
不可修改的收集提供了空间效率优势,并防止收集被意外修改,这可能导致程序工作不正确。
对于以下情况,建议使用不可修改的集合:
- 由编写程序时已知的常量初始化的集合
- 在程序开始时根据计算的数据或从配置文件等文件中读取的数据初始化的集合
对于保存在程序整个过程中被修改的数据的集合,可修改集合是最佳选择。修改是就地执行的,因此增量添加或删除数据元素的成本相当低。如果对不可修改的集合执行此操作,则必须进行完整的复制以添加或删除单个元素,这通常具有不可接受的开销。
二、语法
这些集合的API很简单,特别是对于少量元素。
包括:
2.1 不可修改的静态工厂方法列表
List.of 静态工厂方法提供了一种方便的方法来创建不可修改的列表。
列表是允许重复元素的有序集合。不允许空值。
这些方法的语法是:
List.of()
List.of(e1)
List.of(e1, e2) // 固定参数表单最多重载10个元素
List.of(elements...) // varargs表单支持任意数量的元素或数组
示例2-1
JDK 8版本:
List<String> stringList = Arrays.asList("a", "b", "c");
stringList = Collections.unmodifiableList(stringList);
JDK 9或以后版本:
List<String> stringList = List.of("a", "b", "c");
详细内容可查看:Unmodifiable Lists
2.2 不可修改设置静态工厂方法
Set.of 静态工厂的方法提供了一种方便的方法来创建不可修改的集合。
集合是不包含重复元素的集合。如果检测到重复条目,则抛出一个IllegalArgumentException。不允许空值。
这些方法的语法是:
Set.of()
Set.of(e1)
Set.of(e1, e2) // 固定参数表单最多重载10个元素
Set.of(elements...) // varargs表单支持任意数量的元素或数组
示例2-2
JDK 8版本:
Set<String> stringSet = new HashSet<>(Arrays.asList("a", "b", "c"));
stringSet = Collections.unmodifiableSet(stringSet);
JDK 9或以后版本:
Set<String> stringSet = Set.of("a", "b", "c");
详细内容可查看:Unmodifiable Sets
2.3 不可修改的静态工厂方法映射
Map.of 和 Map.ofEntries工厂方法提供了一种方便的方式来创建不可修改的映射。
Map不能包含重复的键。如果检测到重复的密钥,则抛出一个IllegalArgumentException。
每个键与一个值相关联。Null不能用于Map键或值。
这些方法的语法是:
Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2) // 固定参数形式重载最多10个键值对
Map.ofEntries(entry(k1, v1), entry(k2, v2),...)
// varargs表单支持任意数量的Entry对象或数组
示例2-3
JDK 8版本:
Map<String, Integer> stringMap = new HashMap<String, Integer>();
stringMap.put("a", 1);
stringMap.put("b", 2);
stringMap.put("c", 3);
stringMap = Collections.unmodifiableMap(stringMap);
JDK 9或以后版本:
Map<String, Integer> stringMap = Map.of("a", 1, "b", 2, "c", 3);
详细内容可查看:Unmodifiable Sets
示例2-4 映射具有任意数量的对
如果键值对超过10对,那么使用map创建映射条目。方法,并将这些对象传递给Map。ofEntries方法。例如:
import static java.util.Map.entry;
Map <Integer, String> friendMap = Map.ofEntries(
entry(1, "Tom"),
entry(2, "Dick"),
entry(3, "Harry"),
...
entry(99, "Mathilde"));
详细内容可查看: Unmodifiable Maps
三、创建集合的不可修改副本
让我们考虑这样一种情况:您通过添加元素和修改它来创建一个集合,然后在某个时刻,您需要该集合的不可修改快照。使用copyOf系列方法创建副本。
例如,假设你有一些代码从几个地方收集元素:
List<Item> list = new ArrayList<>();
list.addAll(getItemsFromSomewhere());
list.addAll(getItemsFromElsewhere());
list.addAll(getItemsFromYetAnotherPlace());
使用List.of 创建不可修改的集合很不方便。这样做需要创建合适大小的数组,将列表中的元素复制到数组中,然后调用List.of(array)来创建不可修改的快照。相反,使用copyOf静态工厂方法一步完成:
List<Item> snapshot = List.copyOf(list);
Set和Map有相应的静态工厂方法,称为Set.copyOf 和 Map.copyOf。因为List.copyOf 和Set.copyOf是Collection集合,那么可以创建一个包含Set元素的不可修改的List和一个包含List元素的不可修改的Set。如果你使用Set.copyOf从List创建Set,并且List包含重复的元素,则不会抛出异常。相反,结果Set中包含任意一个重复元素,则会抛出异常。
如果要复制的集合是可修改的,那么copyOf方法将创建一个不可修改的集合,该集合是原始集合的副本。也就是说,结果包含与原始结果相同的所有元素。如果向原始集合中添加或删除元素,则不会影响副本。
如果原始集合已经不可修改,那么copyOf方法只返回对原始集合的引用。复制的目的是将返回的集合与对原始集合的更改隔离开来。但如果原始收藏不能更改,就没有必要复制它。
在这两种情况下,如果元素是可变的,并且修改了一个元素,那么这种更改会导致原始集合和副本看起来都发生了更改。
四、从流中创建不可修改的集合
Streams库包括一组终端操作,称为Collectors。
Collector通常用于创建包含流元素的新集合。
java.util.stream.Collectors 类具有从流的元素创建新的不可修改集合的collections。
如果你想保证返回的集合是不可修改的,你应该使用一个toUnmodifiable- collector。
这些集合是:
Collectors.toUnmodifiableList()
Collectors.toUnmodifiableSet()
Collectors.toUnmodifiableMap(keyMapper, valueMapper)
Collectors.toUnmodifiableMap(keyMapper, valueMapper, mergeFunction)
例如,要转换源集合的元素并将结果放置到不可修改的集合中,您可以执行以下操作:
Set<Item> unmodifiableSet =
sourceCollection.stream()
.map(...)
.collect(Collectors.toUnmodifiableSet());
如果流包含重复的元素,那么 toUnmodifiableSet 收集器会选择任意一个重复的元素来包含在结果集中。
对于toUnmodifiableMap(keyMapper, valueMapper)收集器,
如果keyMapper函数产生重复的键,将抛出一个IllegalStateException。
如果可能存在重复的键,则使用toUnmodifiableMap(keyMapper, valueMapper, mergeFunction) 收集器代替。
如果出现重复键,则调用mergeFunction将每个重复键的值合并为单个值。
在概念上,toUnmodifiable- 收集器与对应的toList、toSet以及对应的两个toMap方法相似,但它们有不同的特征。具体来说,toList、toSet和toMap方法不保证返回集合的可修改性,但是,toUnmodifiable- 收集器方法保证结果是不可修改的。
五、随机迭代顺序
Set元素和Map键的迭代顺序是随机的,每次JVM运行的迭代顺序都可能不同。这是故意的,可以更容易地识别依赖于迭代顺序的代码。对迭代顺序的无意依赖可能会导致难以调试的问题。
下面的例子展示了jshell重启后的迭代顺序是如何不同的。
jshell> var stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {b=2, c=3, a=1}
jshell> /exit
| Goodbye
C:\Program Files\Java\jdk\bin>jshell
jshell> var stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {a=1, b=2, c=3}
随机迭代顺序适用于Set.of, Map.of, 和 Map.ofEntries方法、toUnmodifiableSet 和 toUnmodifiableMap 收集器创建的集合实例。集合实现(如HashMap和HashSet)的迭代顺序没有改变。
六、关于不可修改集合
JDK 9中添加的便利工厂方法返回的集合是不可修改的。从这些集合中添加、设置或删除元素的任何尝试都会导致抛出UnsupportedOperationException。
但是,如果所包含的元素是可变的,那么这可能会导致集合的行为不一致或使其内容看起来发生了变化。
让我们看一个不可修改集合包含可变元素的示例。使用jshell,使用ArrayList类创建两个String对象列表,其中第二个列表是第一个列表的副本。微不足道的jshell输出被删除了。
jshell> List<String> list1 = new ArrayList<>();
jshell> list1.add("a")
jshell> list1.add("b")
jshell> list1
list1 ==> [a, b]
jshell> List<String> list2 = new ArrayList<>(list1);
list2 ==> [a, b]
接下来,使用List.of方法,创建指向第一个列表的unmodlist1和unmodlist2。如果尝试修改unmodlist1,则会看到一个异常错误,因为unmodlist1是不可修改的。任何修改尝试都会引发异常。
jshell> List<List<String>> unmodlist1 = List.of(list1, list1);
unmodlist1 ==> [[a, b], [a, b]]
jshell> List<List<String>> unmodlist2 = List.of(list2, list2);
unmodlist2 ==> [[a, b], [a, b]]
jshell> unmodlist1.add(new ArrayList<String>())
| java.lang.UnsupportedOperationException thrown:
| at ImmutableCollections.uoe (ImmutableCollections.java:71)
| at ImmutableCollections$AbstractImmutableList.add (ImmutableCollections
.java:75)
| at (#8:1)
但是,如果修改原来的list1, unmodlist1的内容就会发生变化,即使unmodlist1是不可修改的。
jshell> list1.add("c")
jshell> list1
list1 ==> [a, b, c]
jshell> unmodlist1
ilist1 ==> [[a, b, c], [a, b, c]]
jshell> unmodlist2
ilist2 ==> [[a, b], [a, b]]
jshell> unmodlist1.equals(unmodlist2)
$14 ==> false
不可修改的集合 vs. 不可修改的视图
不可修改集合的行为方式与Collections.unmodifiable...方法返回的不可修改视图相同。(请参阅JavaDoc API文档中Collection 接口中的Unmodifiable View Collections不可修改视图集合)。
但是,不可修改的集合不是视图—这些是由类实现的数据结构,其中任何修改数据的尝试都将导致抛出异常。
如果创建一个List并将其传递给Collections.unmodifiableList方法,你就会得到一个不可修改的视图。底层列表仍然是可修改的,对它的修改通过返回的list可见,因此它实际上不是不可变的。
为了演示这个行为,创建一个List并将其传递给Collections.unmodifiableList。如果你尝试直接添加到该List,则会抛出一个异常。
jshell> List<String> list1 = new ArrayList<>();
jshell> list1.add("a")
jshell> list1.add("b")
jshell> list1
list1 ==> [a, b]
jshell> List<String> unmodlist1 = Collections.unmodifiableList(list1);
unmodlist1 ==> [a, b]
jshell> unmodlist1.add("c")
| Exception java.lang.UnsupportedOperationException
| at Collections$UnmodifiableCollection.add (Collections.java:1058)
| at (#8:1)
注意,unmodlist1是list1的一个视图。您不能直接更改视图,但可以更改原始列表,从而更改视图。如果更改原来的list1,则不会产生错误,并且unmodlist1列表已经被修改。
jshell> list1.add("c")
$19 ==> true
jshell> list1
list1 ==> [a, b, c]
jshell> unmodlist1
unmodlist1 ==> [a, b, c]
不可修改视图的原因是不能通过调用视图上的方法来修改集合。但是,任何引用底层集合并能够修改它的人都可能导致不可修改的视图发生更改。
七、空间效率
便利的工厂方法返回的集合比可修改的等价集合更节省空间。
这些集合的所有实现都是隐藏在静态工厂方法后面的私有类。调用静态工厂方法时,它会根据集合的大小选择实现类。数据可以存储在紧凑的基于字段或基于数组的布局中。
让我们看看两个备选实现所消耗的堆空间。
首先,这里有一个不可修改的HashSet,包含两个字符串:
Set<String> set = new HashSet<>(3); // 3 buckets
set.add("silly");
set.add("string");
set = Collections.unmodifiableSet(set);
这个集合包括6个对象:
1. 不可修改的包装器wrapper;
2. HashSet,包含一个HashMap;
3. 桶的表(数组);
4. 以及两个Node实例(每个元素一个)。
在一个典型的VM上,每个对象有一个12字节的头,总开销为96字节+ 28 * 2 = 152字节。
与存储的数据量相比,这是一个很大的开销。
此外,访问数据不可避免地需要多个方法调用和指针解引用。
相反,我们可以使用Set.of来实现集合:
Set<String> set = Set.of("silly", "string");
因为这是一个基于字段的实现,所以集合包含一个对象和两个字段。开销为20字节。就固定开销和每个元素而言,新的集合消耗更少的堆空间。
不需要支持突变也有助于节省空间。此外,还改进了引用的局部性,因为保存数据所需的对象更少了。
八、线程安全
如果多个线程共享可修改的数据结构,则必须采取步骤确保一个线程所做的修改不会对其他线程造成意外的副作用。然而,由于不可变对象不能被更改,因此它被认为是线程安全的,不需要任何额外的努力。
当程序的几个部分共享数据结构时,程序的一部分对结构所做的修改对其他部分是可见的。如果程序的其他部分没有为数据的更改做好准备,那么就会发生错误、崩溃或其他意想不到的行为。但是,如果一个程序的不同部分共享一个不可变的数据结构,那么这种意外的行为就永远不会发生,因为共享的结构不能更改。
类似地,当多个线程共享一个数据结构时,每个线程在修改该数据结构时都必须采取预防措施。通常,线程在读取或写入任何共享数据结构时必须持有锁。未能正确锁定可能会导致竞态条件或数据结构中的不一致,这可能会导致bug、崩溃或其他意外行为。但是,如果多个线程共享一个不可变的数据结构,那么即使没有锁,这些问题也不会发生。因此,不可变的数据结构被认为是线程安全的,而不需要任何额外的工作,比如添加锁代码。
如果不能添加、删除或替换元素,则集合被认为是不可修改的。但是,只有在集合中包含的元素是不可变的情况下,不可修改集合才是不可变的。为了确保线程安全,使用静态工厂方法创建的集合和toUnmodifiable-收集器必须只包含不可变元素。
374

被折叠的 条评论
为什么被折叠?



