本附录会审视Java 8方法库中重要的更新。
B.1 集合
Collection API在Java 8中最重大的更新就是引入了流,我们已经在第4章到6章进行了介绍。当然,除此之外,Collection API还有一部分更新,本附录会简要地讨论。
B.1.1 其他新增的方法
Java API的设计者们充分利用默认方法,为集合接口和类新增了多个新的方法。这些新增的方法我们已经列在表B-1中了。
表B-1 集合类和接口中新增的方法
类/接口
新方法
Map
getOrDefault
,forEach
,compute
,computeIfAbsent
,computeIfPresent
,merge
,putIfAbsent
,remove(key,value)
,replace
,replaceAll
Iterable
forEach
,spliterator
Iterator
forEachRemaining
Collection
removeIf
,stream
,parallelStream
List
replaceAll
,sort
BitSet
stream
1. Map
Map
接口的变化最大,它增加了多个新方法,利用这些新方法能更加便利地操纵Map
中的数据。比如,getOrDefault
方法就可以替换现在检测Map
中是否包含给定键映射的惯用方法。如果Map
中不存在这样的键映射,你可以提供一个默认值,方法会返回该默认值。使用之前版本的Java,要实现这一目的,你可能会如下编这段代码:
Map<String, Integer> carInventory = new HashMap<>;Integer count = 0;if(map.containsKey("Aston Martin")){ count = map.get("Aston Martin");}
使用新的Map
接口之后,你只需要简单地编写一行代码就能实现这一功能,代码如下:
Integer count = map.getOrDefault("Aston Martin", 0);
注意,这一方法仅在没有映射时才生效。比如,如果键被显式地映射到了空值,那么该方法是不会返回你设定的默认值的。
另一个特别有用的方法是computeIfAbsent
,这个方法在第14章解释记忆表时曾经简要地提到过。它能帮助你非常方便地使用缓存模式。比如,我们假设你需要从不同的网站抓取和处理数据。这种场景下,如果能够缓存数据是非常有帮助的,这样你就不需要每次都执行(代价极高的)数据抓取操作了:
public String getData(String url){ String data = cache.get(url); if(data == null){ ←─检查数据是否已经缓存 data = getData(url); cache.put(url, data); ←─如果数据没有缓存,那就访问网站抓取数据,紧接着对Map中的数据进行缓存,以备将来使用之需 } return data;}
这段代码,你现在可以通过computeIfAbsent
用更加精炼的方式实现,代码如下所示:
public String getData(String url){ return cache.computeIfAbsent(url, this::getData);}
上面介绍的这些方法,其更详细的内容都能在Java API的官方文档中找到1。注意,ConcurrentHashMap
也进行了更新,提供了新的方法。我们会在B.2节讨论。
1更多细节请参考http://docs.oracle.com/javase/8/docs/api/java/util/Map.html。
2. 集合
removeIf
方法可以移除集合中满足某个谓词的所有元素。注意,这一方法与我们在介绍Stream API时提到的filter
方法不大一样。Stream API中的filter
方法会产生一个新的流,不会对当前作为数据源的流做任何变更。
3. 列表
replaceAll
方法会对列表中的每一个元素执行特定的操作,并用处理的结果替换该元素。它的功能和Stream中的map
方法非常相似,不过replaceAll
会修改列表中的元素。与此相反,map
方法会生成新的元素。
比如,下面这段代码会打印输出[2,4,6,8,10],因为列表中的元素被原地修改了:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);numbers.replaceAll(x -> x * 2);System.out.println(numbers); ←─打印输出[2,4,6,8,10]
B.1.2 Collections
类
Collections
类已经存在了很长的时间,它的主要功能是操作或者返回集合。Java 8中它又新增了一个方法,该方法可以返回不可修改的、同步的、受检查的或者是空的NavigableMap
或NavigableSet
。除此之外,它还引入了checkedQueue
方法,该方法返回一个队列视图,可以扩展进行动态类型检查。
B.1.3 Comparator
Comparator
接口现在同时包含了默认方法和静态方法。你可以使用第3章中介绍的静态方法Comparator.comparing
返回一个Comparator
对象,该对象提供了一个函数可以提取排序关键字。
新的实例方法包含了下面这些。
reversed
——对当前的Comparator
对象进行逆序排序,并返回排序之后新的Comparator
对象。thenComparing
——当两个对象相同时,返回使用另一个Comparator
进行比较的Comparator
对象。thenComparingInt
、thenComparingDouble
、thenComparingLong
——这些方法的工作方式和thenComparing
方法类似,不过它们的处理函数是特别针对某些基本数据类型(分别对应于ToIntFunction
、ToDoubleFunction
和ToLongFunction
)的。
新的静态方法包括下面这些。
comparingInt
、comparingDouble
、comparingLong
——它们的工作方式和comparing
类似,但接受的函数特别针对某些基本数据类型(分别对应于ToIntFunction
、ToDoubleFunction
和ToLongFunction
)。naturalOrder
——对Comparable
对象进行自然排序,返回一个Comparator
对象。nullsFirst
、nullsLast
——对空对象和非空对象进行比较,你可以指定空对象(null)比非空对象(non-null)小或者比非空对象大,返回值是一个Comparator
对象。reverseOrder
——和naturalOrder.reversed
方法类似。
B.2 并发
Java 8中引入了多个与并发相关的更新。首当其冲的当然是并行流,我们在第7章详细讨论过。另外一个就是第11章中介绍的CompletableFuture
类。
除此之外,还有一些值得注意的更新。比如,Arrays类现在支持并发操作了。我们会在B.3节讨论这些内容。
这一节,我们想要围绕java.util.concurrent.atomic
包的更新展开讨论。这个包的主要功能是处理原子变量(atomic variable)。除此之外,我们还会讨论ConcurrentHashMap
类的更新,它现在又新增了几个方法。
B.2.1 原子操作
java.util.concurrent.atomic
包提供了多个对数字类型进行操作的类,比如AtomicInteger
和AtomicLong
,它们支持对单一变量的原子操作。这些类在Java 8中新增了更多的方法支持。
getAndUpdate
——以原子方式用给定的方法更新当前值,并返回变更之前的值。updateAndGet
——以原子方式用给定的方法更新当前值,并返回变更之后的值。getAndAccumulate
——以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之前的值。accumulateAndGet
——以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之后的值。
下面的例子向我们展示了如何以原子方式比较一个现存的原子整型值和一个给定的观测值(比如10),并将变量设定为二者中较小的一个。
int min = atomicInteger.accumulateAndGet(10, Integer::min);
Adder
和Accumulator
多线程的环境中,如果多个线程需要频繁地进行更新操作,且很少有读取的动作(比如,在统计计算的上下文中),Java API文档中推荐大家使用新的类LongAdder
、LongAccumulator
、Double-Adder
以及DoubleAccumulator
,尽量避免使用它们对应的原子类型。这些新的类在设计之初就考虑了动态增长的需求,可以有效地减少线程间的竞争。
LongAddr
和DoubleAdder
类都支持加法操作,而LongAccumulator
和DoubleAccumulator
可以使用给定的方法整合多个值。比如,可以像下面这样使用LongAdder
计算多个值的总和。
代码清单B-1 使用LongAdder
计算多个值之和
LongAdder adder = new LongAdder; ←─使用默认构造器,初始的sum值被置为0adder.add(10); ←─在多个不同的线程中进行加法运算// …long sum = adder.sum; ←─到某个时刻得出sum的值
或者,你也可以像下面这样使用LongAccumulator
实现同样的功能。
代码清单B-2 使用LongAccumulator
计算多个值之和
LongAccumulator acc = new LongAccumulator(Long::sum, 0);acc.accumulate(10); ←─在几个不同的线程中累计计算值// …long result = acc.get; ←─在某个时刻得出结果
B.2.2 ConcurrentHashMap
ConcurrentHashMap
类的引入极大地提升了HashMap
现代化的程度,新引入的ConcurrentHashMap
对并发的支持非常友好。ConcurrentHashMap
允许并发地进行新增和更新操作,因为它仅对内部数据结构的某些部分上锁。因此,和另一种选择,即同步式的Hashtable
比较起来,它具有更高的读写性能。
1. 性能
为了改善性能,要对ConcurrentHashMap
的内部数据结构进行调整。典型情况下,map
的条目会被存储在桶中,依据键生成哈希值进行访问。但是,如果大量键返回相同的哈希值,由于桶是由List
实现的,它的查询复杂度为O(n),这种情况下性能会恶化。在Java 8中,当桶过于臃肿时,它们会被动态地替换为排序树(sorted tree),新的数据结构具有更好的查询性能(排序树的查询复杂度为O(log(n)))。注意,这种优化只有当键是可以比较的(比如String
或者Number
类)时才可能发生。
2. 类流操作
ConcurrentHashMap
支持三种新的操作,这些操作和你之前在流中所见的很像:
forEach
——对每个键值对进行特定的操作reduce
——使用给定的精简函数(reduction function),将所有的键值对整合出一个结果search
——对每一个键值对执行一个函数,直到函数的返回值为一个非空值
以上每一种操作都支持四种形式,接受使用键、值、Map.Entry
以及键值对的函数:
使用键和值的操作(
forEach
、reduce
、search
)使用键的操作(
forEachKey
、reduceKeys
、searchKeys
)使用值的操作 (
forEachValue
、reduceValues
、searchValues
)使用
Map.Entry
对象的操作(forEachEntry
、reduceEntries
、searchEntries
)
注意,这些操作不会对ConcurrentHashMap
的状态上锁。它们只会在运行过程中对元素进行操作。应用到这些操作上的函数不应该对任何的顺序,或者其他对象,抑或在计算过程发生变化的值,有依赖。
除此之外,你需要为这些操作指定一个并发阈值。如果经过预估当前map
的大小小于设定的阈值,操作会顺序执行。使用值1
开启基于通用线程池的最大并行。使用值Long.MAX_VALUE
设定程序以单线程执行操作。
下面这个例子中,我们使用reduceValues
试图找出map
中的最大值:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>;Optional<Integer> maxValue = Optional.of(map.reduceValues(1, Integer::max));
注意,对int
、long
和double
,它们的reduce
操作各有不同(比如reduceValuesToInt
、reduceKeysToLong
等)。
3. 计数
ConcurrentHashMap
类提供了一个新的方法,名叫mappingCount
,它以长整型long
返回map
中映射的数目。我们应该尽量使用这个新方法,而不是老的size
方法,size
方法返回的类型为int
。这是因为映射的数量可能是int
无法表示的。
4. 集合视图
ConcurrentHashMap
类还提供了一个名为KeySet
的新方法,该方法以Set
的形式返回ConcurrentHashMap
的一个视图(对map
的修改会反映在该Set
中,反之亦然)。你也可以使用新的静态方法newKeySet
,由ConcurrentHashMap
创建一个Set
。
B.3 Arrays
Arrays
类提供了不同的静态方法对数组进行操作。现在,它又包括了四个新的方法(它们都有特别重载的变量)。
B.3.1 使用parallelSort
parallelSort
方法会以并发的方式对指定的数组进行排序,你可以使用自然顺序,也可以为数组对象定义特别的Comparator
。
B.3.2 使用setAll
和parallelSetAll
setAll
和parallelSetAll
方法可以以顺序的方式也可以用并发的方式,使用提供的函数计算每一个元素的值,对指定数组中的所有元素进行设置。该函数接受元素的索引,返回该索引元素对应的值。由于parallelSetAll
需要并发执行,所以提供的函数必须没有任何副作用,就如第7章和第13章中介绍的那样。
举例来说,你可以使用setAll
方法生成一个值为0, 2, 4, 6, …的数组:
int evenNumbers = new int[10];Arrays.setAll(evenNumbers, i -> i * 2);
B.3.3 使用parallelPrefix
parallelPrefix
方法以并发的方式,用用户提供的二进制操作符对给定数组中的每个元素进行累积计算。通过下面这段代码,你会得到这样的一些值:1, 2, 3, 4, 5, 6, 7, …。
代码清单B-3 使用parallelPrefix
并发地累积数组中的元素
int ones = new int[10];Arrays.fill(ones, 1);Arrays.parallelPrefix(ones, (a, b) -> a + b); ←─ones现在的内容是[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
B.4 Number
和Math
Java 8 API对Number
和Math
也做了改进,为它们增加了新的方法。
B.4.1 Number
Number
类中新增的方法如下。
Short
、Integer
、Long
、Float
和Double
类提供了静态方法sum
、min
和max
。在第5章介绍reduce
操作时,你已经见过这些方法。Integer
和Long
类提供了compareUnsigned
、pideUnsigned
、remainderUnsigned
和toUnsignedLong
方法来处理无符号数。Integer
和Long
类也分别提供了静态方法parseUnsignedInt
和parseUnsignedLong
将字符解析为无符号int
或者long
类型。Byte
和Short
类提供了toUnsignedInt
和toUnsignedLong
方法通过无符号转换将参数转化为int
或者long
类型。类似地,Integer
类现在也提供了静态方法toUnsignedLong
。Double
和Float
类提供了静态方法isFinite
,可以检查参数是否为有限浮点数。Boolean
类现在提供了静态方法logicalAnd
、logicalOr
和logicalXor
,可以在两个boolean
之间执行and
、or
和xor
操作。BigInteger
类提供了byteValueExact
、shortValueExact
、intValueExact
和longValueExact
,可以将BigInteger
类型的值转换为对应的基础类型。不过,如果在转换过程中有信息的丢失,方法会抛出算术异常。
B.4.2 Math
如果Math
中的方法在操作中出现溢出,Math
类提供了新的方法可以抛出算术异常。支持这一异常的方法包括使用int
和long
参数的addExact
、subtractExact
、multipleExact
、incrementExact
、decrementExact
和negateExact
。此外,Math
类还新增了一个静态方法toIntExact
,可以将long
值转换为int
值。其他的新增内容包括静态方法floorMod
、floorDiv
和nextDown
。
B.5 Files
Files
类最引人注目的改变是,你现在可以用文件直接产生流。第5章中提到过新的静态方法Files.lines
,通过该方法你可以以延迟方式读取文件的内容,并将其作为一个流。此外,还有一些非常有用的静态方法可以返回流。
Files.list
——生成由指定目录中所有条目构成的Stream<Path>
。这个列表不是递归包含的。由于流是延迟消费的,处理包含内容非常庞大的目录时,这个方法非常有用。Files.walk
——和Files.list
有些类似,它也生成包含给定目录中所有条目的Stream<Path>
。不过这个列表是递归的,你可以设定递归的深度。注意,该遍历是依照深度优先进行的。Files.find
——通过递归地遍历一个目录找到符合条件的条目,并生成一个Stream<Path>
对象。
B.6 Reflection
附录A中已经讨论过Java 8中注解机制的几个变化。Reflection API的变化就是为了支撑这些改变。
除此之外,Relection接口的另一个变化是新增了可以查询方法参数信息的API,比如,你现在可以使用新增的java.lang.reflect.Parameter
类查询方法参数的名称和修饰符,这个类被新的java.lang.reflect.Executable
类所引用,而java.lang.reflect.Executable
通用函数和构造函数共享的父类。
B.7 String
String
类也新增了一个静态方法,名叫join
。你大概已经猜出它的功能了,它可以用一个分隔符将多个字符串连接起来。你可以像下面这样使用它:
String authors = String.join(", ", "Raoul", "Mario", "Alan");System.out.println(authors); ←─Raoul, Mario,Alan