Java8

基本

Java8接口允许有默认方法
Lambda
函数式接口
方法和构造函数引用
Lambda的范围
内置函数式接口

- Predicates是一个布尔类型的函数,该函数只有一个输入参数。Predicate接口包含了多种默认方法,用于处理复杂的逻辑动词(and, or,negate)
- Functions接口接收一个参数,并返回单一的结果。默认方法可以将多个函数串在一起(compse, andThen)
- Suppliers接口产生一个给定类型的结果。与Function不同的是,Supplier没有输入参数。
- Consumers代表了在一个输入参数上需要进行的操作。
- Comparatorsr接口在早期的Java版本中非常著名。Java 8 为这个接口添加了不同的默认方法。
- OptionalsOptional不是一个函数式接口,而是一个精巧的工具接口,用来防止NullPointerException产生。这个概念在下一节会显得很重要,所以我们在这里快速地浏览一下Optional的工作原理。Optional是一个简单的值容器,这个值可以是null,也可以是non-null。考虑到一个方法可能会返回一个non-null的值,也可能返回一个空值。为了不直接返回null,我们在Java 8中就返回一个Optional.

Streams

- Filter
- Sorted
- Map
- Match
- Count
- Reduce

Parallel Streams
Map
时间日期API

- Clock
- Timezones
- LocalTime
- LocalDate
- LocalDateTime

Annotations

数据流

数据流可以是一个类型,有串行流和并行流.
List和Set支持新方法stream() 和 parallelStream(),来创建串行流或并行流,并行流能够在多个线程上执行操作.使用Stream.of(),就可以从一系列对象引用中创建数据流。普通的对象数据流,Java8还自带了特殊种类的流,用于处理基本数据类型int、long 和 double。你可能已经猜到了它是IntStream、LongStream 和 DoubleStream。

处理顺序
原始的方法会在数据流的所有元素上,一个接一个地水平执行所有操作。但是每个元素在调用链上垂直移动。

复用数据流
我们创建一个数据流供应器,来构建新的数据流,并且设置好所有衔接操作:

Supplier<Stream> streamSupplier =
() -> Stream.of(“d2”, “a2”, “b1”, “b3”, “c”)
.filter(s -> s.startsWith(“a”));

streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok

每次对get()的调用都构造了一个新的数据流,我们将其保存来调用终止操作。

高级操作:collect、flatMap和reduce。

collect是非常有用的终止操作,将流中的元素存放在不同类型的结果中,例如List、Set或者Map。collect接受收集器(Collector),它由四个不同的操作组成:供应器(supplier)、累加器(accumulator)、组合器(combiner)和终止器(finisher)。这在开始听起来十分复杂,但是Java8通过内置的Collectors类支持多种内置的收集器。所以对于大部分常见操作,你并不需要自己实现收集器。

.collect(Collectors.toList());
.collect(Collectors.toSet());
.collect(Collectors.groupingBy(p -> p.age));
.collect(Collectors.averagingInt(p -> p.age));
.collect(Collectors.summarizingInt(p -> p.age))

Collector.of()创建了一个新的收集器。我们需要传递一个收集器的四个组成部分:供应器、累加器、组合器和终止器。

flatMap,通过使用map操作,将流中的对象转换为另一种类型,flatMap 将一个对象转换为多个或零个其他对象,flatMap接受返回对象流的函数

reduce归约操作将所有流中的元素组合为单一结果。Java8支持三种不同类型的reduce方法
Java8支持三种不同类型的reduce方法。第一种将流中的元素归约为流中的一个元素。第二个reduce方法接受一个初始值,和一个BinaryOperator累加器。这个方法可以用于从流中的其它Person对象中构造带有聚合后名称和年龄的新Person对象。第三个reduce对象接受三个参数:初始值,BiFunction累加器和BinaryOperator类型的组合器函数。

并行流
流可以并行执行,在大量输入元素上可以提升运行时的性能。并行流使用公共的ForkJoinPool,由ForkJoinPool.commonPool()方法提供。底层线程池的大小最大为五个线程 – 取决于CPU的物理核数。

并行流上的sort在背后使用了Java8中新的方法Arrays.parallelSort()

并行流的操作,例如reduce和collect需要额外的计算(组合操作),这在串行执行时并不需要。

并发

Thread 和 Runnable

Executor ExecutorService作为一个在程序中直接使用Thread的高层次的替换方案。Executos支持运行异步任务,通常管理一个线程池,这样一来我们就不需要手动去创建新的线程。在不断地处理任务的过程中,线程池内部线程将会得到复用,因此,在我们可以使用一个executor service来运行和我们想在我们整个程序中执行的一样多的并发任务。

Executors类提供了便利的工厂方法来创建不同类型的 executor services。

Executors必须显式的停止-否则它们将持续监听新的任务。ExecutorService提供了两个方法来达到这个目的——shutdwon()会等待正在执行的任务执行完而shutdownNow()会终止所有正在执行的任务并立即关闭executer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
System.out.println("attempt to shutdown executor");
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
System.err.println("tasks interrupted");
}
finally {
if (!executor.isTerminated()) {
System.err.println("cancel non-finished tasks");
}
executor.shutdownNow();
System.out.println("shutdown finished");
}

Callable 和 Future
除了Runnable,executor还支持另一种类型的任务——Callable。Callables也是类似于runnables的函数接口,不同之处在于,Callable返回一个值。

Callbale也可以像runnbales一样提交给 executor services。但是callables的结果怎么办?因为submit()不会等待任务完成,executor service不能直接返回callable的结果。不过,executor 可以返回一个Future类型的结果,它可以用来在稍后某个时间取出实际的结果。Future调用get()方法时,当前线程会阻塞等待,直到callable在返回实际的结果执行完成。

Future与底层的executor service紧密的结合在一起。记住,如果你关闭executor,所有的未中止的future都会抛出异常。

超时:任何future.get()调用都会阻塞,然后等待直到callable中止。在最糟糕的情况下,一个callable持续运行——因此使你的程序将没有响应。我们可以简单的传入一个时长来避免这种情况。

invokeAll
Executors支持通过invokeAll()一次批量提交多个callable。这个方法结果一个callable的集合,然后返回一个future的列表。

invokeAny
等待future对象的过程中,这个方法将会阻塞直到第一个callable中止然后返回这一个callable的结果。

newWorkStealingPool()。这个工厂方法是Java8引入的,返回一个ForkJoinPool类型的 executor,它的工作方法与其他常见的execuotr稍有不同。与使用一个固定大小的线程池不同,ForkJoinPools使用一个并行因子数来创建,默认值为主机CPU的可用核心数。

ScheduledExecutor
ScheduledExecutorService支持任务调度,持续执行或者延迟一段时间后执行。
调度一个任务将会产生一个专门的future类型——ScheduleFuture,它除了提供了Future的所有方法之外,他还提供了getDelay()方法来获得剩余的延迟。在延迟消逝后,任务将会并发执行。

为了调度任务持续的执行,executors 提供了两个方法scheduleAtFixedRate()和scheduleWithFixedDelay()
scheduleAtFixedRate()并不考虑任务的实际用时
scheduleWithFixedDelay() 等待时间 period 的应用是在一次任务的结束和下一个任务的开始之间

并发工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ConcurrentUtils {

public static void stop(ExecutorService executor) {
try {
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
System.err.println("termination interrupted");
}
finally {
if (!executor.isTerminated()) {
System.err.println("killing non-finished tasks");
}
executor.shutdownNow();
}
}

public static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}

同步和锁

同步synchronized
Java在内部使用所谓的“监视器”(monitor),也称为监视器锁(monitor lock)或内在锁( intrinsic lock)来管理同步。监视器绑定在对象上,例如,当使用同步方法时,每个方法都共享相应对象的相同监视器。

所有隐式的监视器都实现了重入(reentrant)特性。
重入的意思是锁绑定在当前线程上。线程可以安全地多次获取相同的锁,而不会产生死锁(例如,同步方法调用相同对象的另一个同步方法)。

ReentrantLock
锁可以通过lock()来获取,通过unlock()来释放。把你的代码包装在try-finally代码块中来确保异常情况下的解锁非常重要。

ReadWriteLock
ReadWriteLock接口规定了锁的另一种类型,包含用于读写访问的一对锁。读写锁的理念是,只要没有任何线程写入变量,并发读取可变变量通常是安全的。所以读锁可以同时被多个线程持有,只要没有线程持有写锁。这样可以提升性能和吞吐量,因为读取比写入更加频繁。

StampedLock
Java 8 自带了一种新的锁,叫做StampedLock,它同样支持读写锁,就像上面的例子那样。与ReadWriteLock不同的是,StampedLock的锁方法会返回表示为long的标记。你可以使用这些标记来释放锁,或者检查锁是否有效。此外,StampedLock支持另一种叫做乐观锁(optimistic locking)的模式。
通过readLock() 或 writeLock()来获取读锁或写锁会返回一个标记,它可以在稍后用于在finally块中解锁。要记住StampedLock并没有实现重入特性。每次调用加锁都会返回一个新的标记,并且在没有可用的锁时阻塞,即使相同线程已经拿锁了。所以你需要额外注意不要出现死锁。
乐观锁:乐观的读锁通过调用tryOptimisticRead()获取,它总是返回一个标记而不阻塞当前线程,无论锁是否真正可用。如果已经有写锁被拿到,返回的标记等于0。你需要总是通过lock.validate(stamp)检查标记是否有效。
乐观锁在刚刚拿到锁之后是有效的。和普通的读锁不同的是,乐观锁不阻止其他线程同时获取写锁。在第一个线程暂停一秒之后,第二个线程拿到写锁而无需等待乐观的读锁被释放。此时,乐观的读锁就不再有效了。甚至当写锁释放时,乐观的读锁还处于无效状态。

所以在使用乐观锁时,你需要每次在访问任何共享可变变量之后都要检查锁,来确保读锁仍然有效。
将读锁转换为写锁而不用再次解锁和加锁十分实用。StampedLock为这种目的提供了tryConvertToWriteLock()方法

信号量
限制你程序某个部分的并发访问总数时非常实用。
Semaphore semaphore = new Semaphore(5);
permit = semaphore.tryAcquire(1, TimeUnit.SECONDS);

原子变量和 ConcurrentMap

AtomicInteger 其它实用的原子类有AtomicBoolean、AtomicLong 和 AtomicReference。
java.concurrent.atomic包包含了许多实用的类,用于执行原子操作。如果你能够在多线程中同时且安全地执行某个操作,而不需要synchronized关键字或上一章中的锁,那么这个操作就是原子的。

在只需要并发修改单个可变变量的情况下,优先使用原子类,而不是展示的锁。

LongAdder
LongAdder是AtomicLong的替代,用于向某个数值连续添加值。
LongAdder提供了add()和increment()方法,就像原子数值类一样,同样是线程安全的。但是这个类在内部维护一系列变量来减少线程之间的争用,而不是求和计算单一结果。实际的结果可以通过调用sum()或sumThenReset()来获取。

LongAccumulator
LongAccumulator是LongAdder的更通用的版本。LongAccumulator以类型为LongBinaryOperatorlambda表达式构建,而不是仅仅执行加法操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ExecutorService executor = Executors.newFixedThreadPool(2);
StampedLock lock = new StampedLock();

executor.submit(() -> {
long stamp = lock.readLock();
try {
if (count == 0) {
stamp = lock.tryConvertToWriteLock(stamp);
if (stamp == 0L) {
System.out.println("Could not convert to write lock");
stamp = lock.writeLock();
}
count = 23;
}
System.out.println(count);
} finally {
lock.unlock(stamp);
}
});

stop(executor);

我们使用函数2 * x + y创建了LongAccumulator,初始值为1。每次调用accumulate(i)的时候,当前结果和值i都会作为参数传入lambda表达式。

LongAccumulator就像LongAdder那样,在内部维护一系列变量来减少线程之间的争用。

ConcurrentMap
ConcurrentMap接口继承自Map接口,并定义了最实用的并发集合类型之一。Java8通过将新的方法添加到这个接口,引入了函数式编程。
forEach()方法接受类型为BiConsumer的lambda表达式,以映射的键和值作为参数传递。它可以作为for-each循环的替代,来遍历并发映射中的元素。迭代在当前线程上串行执行。
新方法putIfAbsent()只在提供的键不存在时,将新的值添加到映射中。至少在ConcurrentHashMap的实现中,这一方法像put()一样是线程安全的,所以你在不同线程中并发访问映射时,不需要任何同步机制。
getOrDefault()方法返回指定键的值。在传入的键不存在时,会返回默认值:
replaceAll()接受类型为BiFunction的lambda表达式。BiFunction接受两个参数并返回一个值。函数在这里以每个元素的键和值调用,并返回要映射到当前键的新值。
compute()允许我们转换单个元素,而不是替换映射中的所有值。这个方法接受需要处理的键,和用于指定值的转换的BiFunction。
除了compute()之外还有两个变体:computeIfAbsent() 和 computeIfPresent()。这些方法的函数式参数只在键不存在或存在时被调用。
merge()方法可以用于以映射中的现有值来统一新的值。这个方法接受键、需要并入现有元素的新值,以及指定两个值的合并行为的BiFunction。

ConcurrentHashMap
所有这些方法都是ConcurrentMap接口的一部分,因此可在所有该接口的实现上调用。此外,最重要的实现ConcurrentHashMap使用了一些新的方法来改进,便于在映射上执行并行操作。

就像并行流那样,这些方法使用特定的ForkJoinPool,由Java8中的ForkJoinPool.commonPool()提供。

Java8引入了三种类型的并行操作:forEach、search 和 reduce。这些操作中每个都以四种形式提供,接受以键、值、元素或键值对为参数的函数。
forEach()方法可以并行迭代映射中的键值对。BiConsumer以当前迭代元素的键和值调用。为了将并行执行可视化,我们向控制台打印了当前线程的名称。要注意在我这里底层的ForkJoinPool最多使用三个线程。
search()方法接受BiFunction并为当前的键值对返回一个非空的搜索结果,或者在当前迭代不匹配任何搜索条件时返回null。只要返回了非空的结果,就不会往下搜索了。要记住ConcurrentHashMap是无序的。搜索函数应该不依赖于映射实际的处理顺序。如果映射的多个元素都满足指定搜索函数,结果是非确定的。
reduce()方法已经在Java 8 的数据流之中用过了,它接受两个BiFunction类型的lambda表达式。第一个函数将每个键值对转换为任意类型的单一值。第二个函数将所有这些转换后的值组合为单一结果,并忽略所有可能的null值。

字符串、数值、算术和文件

处理字符串
两个新的方法可在字符串类上使用:join和chars
正则表达式模式串也能受益于数据流。我们可以分割任何模式串,并创建数据流来处理它们,而不是将字符串分割为单个字符的数据流,像下面这样:

1
2
3
4
5
6
Pattern.compile(":")
.splitAsStream("foobar:foo:bar")
.filter(s -> s.contains("bar"))
.sorted()
.collect(Collectors.joining(":"));
// => bar:foobar

处理数值
Java8添加了对无符号数的额外支持。Java中的数值总是有符号的

算术运算
Java8添加了严格数学运算的支持来解决这个问题。Math扩展了一些方法,它们全部以exact结尾,例如addExact。当运算结果不能被数值类型装下时,这些方法通过抛出ArithmeticException异常来合理地处理溢出。

处理文件
Files工具类首次在Java7中引入,作为NIO的一部分。JDK8 API添加了一些额外的方法,它们可以将文件用于函数式数据流。
列出文件
Files.list方法将指定目录的所有路径转换为数据流,便于我们在文件系统的内容上使用类似filter和sorted的流操作。数据流的创建包装在try-with语句中。数据流实现了AutoCloseable,并且这里我们需要显式关闭数据流,因为它基于IO操作。

读写文件
Files.lines方法来作为内存高效的替代。这个方法读取每一行,并使用函数式数据流来对其流式处理,而不是一次性把所有行都读进内存。
更多的精细控制,你需要构造一个新的BufferedReader来代替Files.newBufferedReader(path), Files.newBufferedWriter(path)

在 Java 8 中避免 Null 检查

Null 引用的发明者 Tony Hoare 在 2009 年道歉,并称这种错误为他的十亿美元错误。

我将其称之为自己的十亿美元错误。它的发明是在1965 年,那时我用一个面向对象语言(ALGOL W)设计了第一个全面的引用类型系统。我的目的是确保所有引用的使用都是绝对安全的,编译器会自动进行检查。但是我未能抵御住诱惑,加入了 Null 引用,仅仅是因为实现起来非常容易。它导致了数不清的错误、漏洞和系统崩溃,可能在之后 40 年中造成了十亿美元的损失。

Java 8 的 Optional 类型来摆脱所有这些 null 检查。map 方法接收一个 Function 类型的 lambda 表达式,并自动将每个 function 的结果包装成一个 Optional 对象。这使我们能够在一行中进行多个 map 操作。Null 检查是在底层自动处理的。
还有一种实现相同作用的方式就是通过利用一个 supplier 函数来解决嵌套路径的问题: