8.1 集合工厂
Java 9引入了一些新的方法,可以很简便地创建由少量对象构成的Collection。首先,我们会探讨为什么程序员需要新方法,然后会介绍如何使用新的工厂方法创建对象。
先来回顾一下如何使用Java创建一个由少量元素构成的列表。譬如,你想要收集准备一起度假的朋友的名字。下面是一种实现方法:
List<String> friends = new ArrayList<>();friends.add("Raphael");friends.add("Olivia");friends.add("Thibaut");
不过,这种方式很冗长,仅仅为了保存三个人名就写了这么多代码!实现同样的功能,更简洁的方式是使用 Arrays.asList()工厂方法:
List<String> friends= Arrays.asList("Raphael", "Olivia", "Thibaut");
通过上面的代码,你创建了一个固定大小的列表,列表的元素可以更新,但不能增加或者删除。如果你尝试向其中添加元素,JVM就会抛出一个UnsupportedModificationException异常。使用Set方法更新元素是允许的,如下所示:
List<String> friends = Arrays.asList("Raphael", "Olivia");friends.set(0, "Richard");friends.add("Thibaut"); ←---- 抛出一个UnsupportedModificationException异常
这种行为让人有点儿意外,不过也可以解释,因为通过工厂方法创建的Collection的底层是大小固定的可变数组。
那么创建Set也有工厂方法吗?非常抱歉,目前Java中还没有Arrays.asSet()这种工厂方法,你得通过别的方法实现类似的效果。譬如,你可以向HashSet的构造器传递一个列表,如下所示:
Set<String> friends "= new HashSet<>(Arrays.asList("Raphael", "Olivia", Thibaut"));
或者,你还可以使用Stream API:
Set<String> friends= Stream.of("Raphael", "Olivia", "Thibaut").collect(Collectors.toSet());
然而,这两种方案都并非完美,背后都有不必要的对象分配。此外,还得注意,你最终得到的是一个可变的Set。
那Map呢?目前还没有优雅的方式来创建小规模的Map,不过别担心,Java 9新增的工厂方法可以简化小规模List、Set或者Map的创建。
让我们开始探索Java中创建集合的新方法吧。首先从List的新特性入手。
集合常量
包括Python、Groovy在内的多种语言都支持集合常量,你可以通过譬如
[42, 1, 5]这样的语法格式创建含有三个数字的集合。Java并没有提供集合常量的语法支持,原因是这种语言上的变化往往伴随着高昂的维护成本,并且会限制将来可能使用的语法。与此相反,Java 9通过增强Collection API,另辟蹊径地增加了对集合常量的支持。
8.1.1 List工厂
通过工厂方法List.of可以非常容易地创建一个列表,例如:
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");System.out.println(friends); ←---- [Raphael, Olivia, Thibaut]
不过,你可能会发现一些比较奇怪的情况。试着往你的朋友列表里添加一个元素:
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");friends.add("Chih-Chun");
执行这段代码时,你会遇到一个java.lang.UnsupportedOperationException异常。事实上,你刚刚创建的这个列表是一个只读列表。如果你试着使用set()方法替换它的一个成员,也会抛出一个类似的异常。所以,你也不能通过调用set修改它。不过,这种限制是好事,因为它可以保护你的集合,以免被意外地修改。你依然能够创建一个由可变元素构成的列表。如果你需要一个可变列表,也可以通过手动创建。最后,请留意一点,为了避免不可预知的缺陷,同时以更紧凑的方式存储内部数据,不要在工厂方法创建的列表中存放null元素。
重载(overloading)和变参(vararg)
如果你进一步审视
List接口,会发现List.of包含了多个重载的版本,包括:
static <E> List<E> of(E e1, E e2, E e3, E e4)static <E> List<E> of(E e1, E e2, E e3, E e4, E e5)你可能想知道Java API为什么不提供一个使用可变参数的方法,像下面这样接受任意数目的元素:
static <E> List<E> of(E… elements)“知其然,更要知其所以然”,变参版本的函数需要额外分配一个数组,这个数组被封装于列表中。使用变参版本的方法,你就要负担分配数组、初始化以及最后对它进行垃圾回收的开销。使用定长(最多为10个)元素版本的函数,就没有这部分开销。注意,如果使用
List.of创建超过10个元素的列表,这种情况下实际调用的还是变参类型的函数。类似的情况也会出现在Set.of和Map.of中。
可能你会问能不能使用Stream API而不是新的集合工厂方法来创建这种列表。毕竟前面章节里曾经使用收集器的Collectors.toList()方法将流转换为了列表。我的建议是除非你需要进行某种形式的数据处理并对数据进行转换,否则应该尽量使用工厂方法。工厂方法使用起来更简单,实现也更容易,并且在大多数情况下就够用了。
现在,你应该已经了解了List新引入的工厂方法,接下来继续讨论Set。
8.1.2 Set工厂
你可以用类似于List.of的方式,创建列表元素的不可变Set集合:
Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");System.out.println(friends); ←---- [Raphael,Olivia, Thibaut]
如果你试图使用一个包含重复元素的列表创建Set,就会收到一个IllegalArgumentException异常。这个异常反映了Set这种数据结构所遵守的原则,即它所包含的所有元素都是唯一的:
Set<String> friends = Set.of("Raphael", "Olivia", "Olivia"); ←---- java.lang.IllegalArgumentException:重复元素:Olivia
Java语言中另一种流行的数据结构是Map。接下来会学习创建Map的新方法。
8.1.3 Map工厂
跟创建List和Set比较起来,创建Map稍显复杂,因为你需要同时传递键和值。Java 9中提供了两种初始化一个不可变Map的方式。你可以使用工厂方法Map.of,该方法交替地以列表中的元素作为键和值,如下所示:
Map<String, Integer> ageOfFriends= Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);System.out.println(ageOfFriends); ←---- {Olivia=25, Raphael=30, Thibaut=26}
如果你只需要创建不到10个键值对的小型Map,那么使用这种方法比较方便。如果键值对的规模比较大,则可以考虑使用另外一种叫作Map.ofEntries的工厂方法,这种工厂方法接受以变长参数列表形式组织的Map.Entry对象作为参数。使用第二种方法,你需要创建额外的对象,从而实现对键和值的封装,如下所示:
import static java.util.Map.entry;Map<String, Integer> ageOfFriends= Map.ofEntries(entry("Raphael", 30),entry("Olivia", 25),entry("Thibaut", 26));System.out.println(ageOfFriends); ←---- {Olivia=25, Raphael=30, Thibaut=26}
Map.entry是一个新的用于创建Map.Entry对象的工厂方法。
测验8.1
以下代码片段的输出是什么?
List<String> actors = List.of("Keanu", "Jessica")actors.set(0, "Brad");System.out.println(actors)答案:执行该代码片段会抛出一个
UnsupportedOperationException异常,因为由List.of方法构造的集合对象是不可修改的。
至此,我们已经介绍完了Java 9中新引入的用于创建集合对象的工厂方法,使用工厂方法创建集合非常简单。不过在实际项目中,你还是需要对集合进行处理。下一节会介绍List和Set的几个新的增强功能,这些功能别出心裁地将一些通用处理模式抽象出来,极大地方便了集合的处理。
