目录

附录B_类库的更新

本附录会审视Java 8方法库中重要的更新。

集合

Collection API在Java 8中最重大的更新就是引入了流,我们已经在第4章到6章进行了介绍。 当然,除此之外,Collection API还有一部分更新,本附录会简要地讨论。

其他新增的方法

Java API的设计者们充分利用默认方法,为集合接口和类新增了多个新的方法。这些新增的 方法我们已经列在表B-1中了。

https://gitee.com/lienhui68/picStore/raw/master/null/20200819145155.png

Collection

removeIf 方法可以移除集合中满足某个谓词的所有元素。 注意, 这一方法与我们在介绍 Stream API时提到的filter方法不大一样。Stream API中的filter方法会产生一个新的流,不会 对当前作为数据源的流做任何变更。

Map

getOrDefault

1
default V getOrDefault(Object key, V defaultValue) {

Map接口的变化最大,它增加了多个新方法,利用这些新方法能更加便利地操纵Map中的数 据。比如,getOrDefault方法就可以替换现在检测Map中是否包含给定键映射的惯用方法。如 果Map中不存在这样的键映射,你可以提供一个默认值,方法会返回该默认值。使用之前版本的 Java,要实现这一目的,你可能会如下编这段代码:

1
2
3
4
5
Map<String, Integer> carInventory = new HashMap<>();
Integer count = 0;
if (carInventory.containsKey("Aston Martin")) {
    count = carInventory.get("Aston Martin");
}

使用新的Map接口之后,你只需要简单地编写一行代码就能实现这一功能,代码如下:

1
2
Map<String, Integer> carInventory = new HashMap<>();
Integer count = carInventory.getOrDefault("Aston Martin", 0);

注意,这一方法仅在没有映射时才生效。比如,如果键被显式地映射到了空值,那么该方法是不会返回你设定的默认值的。

computeIfAbsent

1
2
default V computeIfAbsent(K key,
        Function<? super K, ? extends V> mappingFunction) {

另一个特别有用的方法是computeIfAbsent,这个方法在第14章解释记忆表时曾经简要地 提到过。它能帮助你非常方便地使用缓存模式。比如,我们假设你需要从不同的网站抓取和处理 数据。这种场景下,如果能够缓存数据是非常有帮助的,这样你就不需要每次都执行(代价极高 的)数据抓取操作了:

https://gitee.com/lienhui68/picStore/raw/master/null/20200819145821.png

这段代码,你现在可以通过computeIfAbsent用更加精炼的方式实现,代码如下所示:

1
2
3
public String getData(String url) {
    return cache.computeIfAbsent(url, this::getData);
}

上面介绍的这些方法, 其更详细的内容都能在Java API的官方文档中找到。 注意, ConcurrentHashMap也进行了更新,提供了新的方法。我们会在B.2节讨论。

List

replaceAll方法会对列表中的每一个元素执行特定的操作,并用处理的结果替换该元素。 它的功能和Stream中的map方法非常相似,不过replaceAll会修改列表中的元素。与此相反, map方法会生成新的元素。

比如,下面这段代码会打印输出[2,4,6,8,10],因为列表中的元素被原地修改了:

1
2
3
4
5
6
7
public static void main(String[] args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    numbers.replaceAll(x -> x * 2);
    System.out.println(numbers);
}
// 运行结果
[246810]

Collections类

Collections类已经存在了很长的时间,它的主要功能是操作或者返回集合。Java 8中它又 新增了一个方法,该方法可以返回不可修改的、同步的、受检查的或者是空的NavigableMap或 NavigableSet。除此之外,它还引入了checkedQueue方法,该方法返回一个队列视图,可以 扩展进行动态类型检查。

Comparator

Comparator接口现在同时包含了默认方法和静态方法。你可以使用第3章中介绍的静态方 法Comparator.comparing返回一个Comparator对象,该对象提供了一个函数可以提取排序关键字。 新的实例方法(对实现类来说是实例方法,对接口来说是默认方法)包含了下面这些。

  • reversed—— 对 当 前 的 Comparator 对 象 进 行 逆 序 排 序 , 并 返 回 排 序 之 后 新 的 Comparator对象。
  • thenComparing——当两个对象相同时, 返回使用另一个 Comparator 进行比较的 Comparator对象。
  • thenComparingInt、thenComparingDouble、thenComparingLong——这些方法的 工作方式和thenComparing方法类似,不过它们的处理函数是特别针对某些基本数据类 型(分别对应于ToIntFunction、ToDoubleFunction和ToLongFunction)的。

新的静态方法包括下面这些。

  • comparingInt、comparingDouble、comparingLong——它们的工作方式和compa- ring类似,但接受的函数特别针对某些基本数据类型(分别对应于 ToIntFunction、 ToDoubleFunction和ToLongFunction)。
  • naturalOrder——对Comparable对象进行自然排序,返回一个Comparator对象。
  • nullsFirst、nullsLast——对空对象和非空对象进行比较,你可以指定空对象(null) 比非空对象(non-null)小或者比非空对象大,返回值是一个Comparator对象。
  • reverseOrder——和naturalOrder().reversed()方法类似。

并发

Java 8中引入了多个与并发相关的更新。首当其冲的当然是并行流,我们在第7章详细讨论过。 另外一个就是第11章中介绍的CompletableFuture类。

除此之外,还有一些值得注意的更新。比如,Arrays类现在支持并发操作了。我们会在B.3 节讨论这些内容。

这一节,我们想要围绕java.util.concurrent.atomic包的更新展开讨论。这个包的主 要功能是处理原子变量(atomic variable)。除此之外,我们还会讨论ConcurrentHashMap类的 更新,它现在又新增了几个方法。

原子操作

java.util.concurrent.atomic包提供了多个对数字类型进行操作的类,比如Atomic- Integer和AtomicLong,它们支持对单一变量的原子操作。这些类在Java 8中新增了更多的方 法支持。

  • getAndUpdate——以原子方式用给定的方法更新当前值,并返回变更之前的值。

    1
    
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
    
  • updateAndGet——以原子方式用给定的方法更新当前值,并返回变更之后的值。

    1
    
    public final int updateAndGet(IntUnaryOperator updateFunction) {
    
  • getAndAccumulate——以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之前的值。

    1
    2
    
    public final int getAndAccumulate(int x,
                                      IntBinaryOperator accumulatorFunction) {
    
  • accumulateAndGet——以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之后的值。

    1
    2
    
    public final int accumulateAndGet(int x,
                                      IntBinaryOperator accumulatorFunction) {
    

下面的例子向我们展示了如何以原子方式比较一个现存的原子整型值和一个给定的观测值 (比如10),并将变量设定为二者中较小的一个。

1
int min = atomicInteger.accumulateAndGet(10, Integer::min);

Adder和Accumulator

多线程的环境中,如果多个线程需要频繁地进行更新操作,且很少有读取的动作(比如,在 统计计算的上下文中),Java API文档中推荐大家使用新的类LongAdder、LongAccumulator、 DoubleAdder以及DoubleAccumulator,尽量避免使用它们对应的原子类型。这些新的类在设计之初就考虑了动态增长的需求,可以有效地减少线程间的竞争。

LongAddr 和 DoubleAdder 类都支持加法操作, 而 LongAccumulator 和 DoubleAccu- mulator可以使用给定的方法整合多个值。比如,可以像下面这样使用LongAdder计算多个值的总和。

https://gitee.com/lienhui68/picStore/raw/master/null/20200819151224.png

ConcurrentHashMap

ConcurrentHashMap类的引入极大地提升了HashMap现代化的程度,新引入的ConcurrentHashMap对并发的支持非常友好。ConcurrentHashMap允许并发地进行新增和更新操作,因为它仅对内部数据结构的某些部分上锁。因此,和另一种选择,即同步式的Hashtable比较起来,它 具有更高的读写性能。

性能

为了改善性能,要对ConcurrentHashMap的内部数据结构进行调整。典型情况下,map的 条目会被存储在桶中,依据键生成哈希值进行访问。但是,如果大量键返回相同的哈希值,由于桶是由List实现的,它的查询复杂度为$O(n)$,这种情况下性能会恶化。在Java 8中,当桶过于臃肿时,它们会被动态地替换为排序树(sorted tree),新的数据结构具有更好的查询性能(排序树 的查询复杂度为$O(log(n))$)。注意,这种优化只有当键是可以比较的(比如String或者Number 类)时才可能发生。

类流操作

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中的最大值:

1
2
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); 
Optional<Integer> maxValue = Optional.of(map.reduceValues(1, Integer::max));

注意,对int、long和double,它们的reduce操作各有不同(比如reduceValuesToInt、 reduceKeysToLong等)。

计数

ConcurrentHashMap类提供了一个新的方法,名叫mappingCount,它以长整型long返回 map中映射的数目。我们应该尽量使用这个新方法,而不是老的size方法,size方法返回的类 型为int。这是因为映射的数量可能是int无法表示的。

集合视图

ConcurrentHashMap 类还提供了一个名为 KeySet 的新方法, 该方法以 Set 的形式返回 ConcurrentHashMap的一个视图(对map的修改会反映在该Set中,反之亦然)。你也可以使用 新的静态方法newKeySet,由ConcurrentHashMap创建一个Set。

Arrays

Arrays类提供了不同的静态方法对数组进行操作。现在,它又包括了四个新的方法(它们都有特别重载的变量)。

使用parallelSort

parallelSort方法会以并发的方式对指定的数组进行排序,你可以使用自然顺序,也可以 为数组对象定义特别的Comparator。

使用setAll和parallelSetAll

setAll和parallelSetAll方法可以以顺序的方式也可以用并发的方式,使用提供的函数 计算每一个元素的值,对指定数组中的所有元素进行设置。该函数接受元素的索引,返回该索引 元素对应的值。由于parallelSetAll需要并发执行,所以提供的函数必须没有任何副作用,就 如第7章和第13章中介绍的那样。

举例来说,你可以使用setAll方法生成一个值为0, 2, 4, 6, …的数组:

1
2
int[] evenNumbers = new int[10]; 
Arrays.setAll(evenNumbers, i -> i * 2);

使用parallelPrefix

parallelPrefix方法以并发的方式,用用户提供的二进制操作符对给定数组中的每个元素 进行累积计算。通过下面这段代码,你会得到这样的一些值:1, 2, 3, 4, 5, 6, 7, …。

使用parallelPrefix并发地累积数组中的元素

https://gitee.com/lienhui68/picStore/raw/master/null/20200819152735.png

使用数组的前一个值和自己相加再原位更新

Number和Math

Java 8 API对Number和Math也做了改进,为它们增加了新的方法。

Number

Number类中新增的方法如下。

  • Short、Integer、Long、Float和Double类提供了静态方法sum、min和max。在第5 章介绍reduce操作时,你已经见过这些方法。
  • Integer和Long类提供了compareUnsigned、divideUnsigned、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类型的值转换为对应的基础类型。不过,如果在转换过程中有信息的丢失,方法会抛出算术异常。

Math

如果Math中的方法在操作中出现溢出,Math类提供了新的方法可以抛出算术异常。支持这 一异常的方法包括使用 int 和 long 参数的 addExact 、 subtractExact 、 multipleExact 、 incrementExact、decrementExact和negateExact。此外,Math类还新增了一个静态方法 toIntExact,可以将long值转换为int值。其他的新增内容包括静态方法floorMod、floorDiv 和nextDown。

exact表示精确的,如果出现溢出则抛出异常

Files

Files类最引人注目的改变是,你现在可以用文件直接产生流。第5章中提到过新的静态方 法Files.lines,通过该方法你可以以延迟方式读取文件的内容,并将其作为一个流。此外, 还有一些非常有用的静态方法可以返回流。

  • Files.list——生成由指定目录中所有条目构成的Stream<Path>。这个列表不是递归包含的。由于流是延迟消费的,处理包含内容非常庞大的目录时,这个方法非常有用。

  • Files.walk——和 Files.list 有些类似, 它也生成包含给定目录中所有条目的 Stream<Path>。不过这个列表是递归的,你可以设定递归的深度。注意,该遍历是依照 深度优先进行的。

  • Files.find—— 通 过 递 归 地 遍 历 一 个 目 录 找 到 符 合 条 件 的 条 目 , 并 生 成 一 个Stream<Path>对象。

    1
    2
    3
    4
    5
    6
    
    public static Stream<Path> find(Path start,
                                    int maxDepth,
                                    BiPredicate<Path, BasicFileAttributes> matcher,
                                    FileVisitOption... options)
        throws IOException
    {
    

Reflection

附录A中已经讨论过Java 8中注解机制的几个变化。Reflection API的变化就是为了支撑这些 改变。

除此之外,Relection接口的另一个变化是新增了可以查询方法参数信息的API,比如,你现 在可以使用新增的java.lang.reflect.Parameter类查询方法参数的名称和修饰符,这个类 被新的java.lang.reflect.Executable类所引用,而java.lang.reflect.Executable通 用函数和构造函数共享的父类。

1
public final class Method extends Executable {

String

String类也新增了一个静态方法,名叫join。你大概已经猜出它的功能了,它可以用一个 分隔符将多个字符串连接起来。你可以像下面这样使用它:

1
2
3
4
5
6
public static void main(String[] args) {
    String authors = String.join(", ", "Raoul", "Mario", "Alan");
    System.out.println(authors);
}
// 运行结果
Raoul, Mario, Alan