Java8 使用
链接:https://www.jianshu.com/p/936d97ba0362
链接:https://www.jianshu.com/p/41de7b5ac7b9
本文主要总结了《Java8实战》,适用于学习 Java8 的同学,也可以作为一个 API 手册文档适用,平时使用时可能由于不熟练,忘记 API 或者语法。
Java8 新特性:
- Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
- 方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
- 默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。
新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。 - Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
- Date Time API − 加强对日期与时间的处理。
- Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
- Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。
Lambda 表达式
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
方法引用
- 方法引用通过方法的名字来指向一个方法。
- 方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
- 方法引用使用一对冒号 :: 。
// jdk1.8接口
@FunctionalInterface
public interface Supplier <T> {
T get();
}
public class Car {
//Supplier是jdk1.8的接口,这里和lamda一起使用了
public static Car create(final Supplier <Car> supplier) {
return supplier.get();
}
}
构造器引用
它的语法是Class::new,或者更一般的Class< T >::new实例如下:
final Car car = Car.create(Car::new);
final List <Car> cars = Arrays.asList(car);
无参构造
Supplier <Apple> c1 = Apple::new;
Apple a1 = c1.get();
一个参数构造
Function <Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
两个参数构造
BiFunction <String, Integer, Apple> c3 = Apple::new;
Apple c3 = c3.apply("green", 110);
多个参数构造
可自定义Function实现,如
public interface TriFunction <T, U, V, R> {
R apply(T t, U u, V v);
}
静态方法引用
它的语法是Class::static_method,实例如下:
cars.forEach(Car::collide);
特定类的任意对象的方法引用
它的语法是Class::method实例如下:
cars.forEach(Car::repair);
特定对象的方法引用
它的语法是instance::method实例如下:
final Car police = Car.create(Car::new);
cars.forEach(police::follow);
函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为lambda表达式。
函数式接口可以现有的函数友好地支持 lambda。
JDK 1.8之前已有的函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
JDK 1.8 新增加的函数接口: - java.util.function
java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口有:
接口名 | 参数 | 返回值 | 用途 |
---|---|---|---|
Predicate | T | boolean | 断言 |
Consumer | T | void | 消费 |
Function<T,R> | T | R | 函数 |
Supplier | None | T | 工厂方法 |
UnaryOperator | T | T | 逻辑非 |
BinaryOperator | (T,T) | T | 二元操作 |
函数式接口各类介绍:
接口 | 描述 |
---|---|
BiConsumer<T,U> | 代表了一个接受两个输入参数的操作,并且不返回任何结果 |
BiFunction<T,U,R> | 代表了一个接受两个输入参数的方法,并且返回一个结果 |
BinaryOperator<T> | 代表了一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果 |
BiPredicate<T,U> | 代表了一个两个参数的boolean值方法 |
BooleanSupplier | 代表了boolean值结果的提供方 |
Consumer<T> | 代表了接受一个输入参数并且无返回的操作 |
DoubleBinaryOperator | 代表了作用于两个double值操作符的操作,并且返回了一个double值的结果 |
DoubleConsumer | 代表一个接受double值参数的操作,并且不返回结果 |
DoubleFunction<R> | 代表接受一个double值参数的方法,并且返回结果 |
DoublePredicate | 代表一个拥有double值参数的boolean值方法 |
DoubleSupplier | 代表一个double值结构的提供方 |
DoubleToIntFunction | 接受一个double类型输入,返回一个int类型结果 |
DoubleToLongFunction | 接受一个double类型输入,返回一个long类型结果 |
DoubleUnaryOperator | 接受一个参数同为类型double,返回值类型也为double |
Function<T,R> | 接受一个输入参数,返回一个结果 |
IntBinaryOperator | 接受两个参数同为类型int,返回值类型也为int |
IntConsumer | 接受一个int类型的输入参数,无返回值 |
IntFunction<R> | 接受一个int类型输入参数,返回一个结果 |
IntPredicate | 接受一个int输入参数,返回一个布尔值的结果 |
IntSupplier | 无参数,返回一个int类型结果 |
IntToDoubleFunction | 接受一个int类型输入,返回一个double类型结果 |
IntToLongFunction | 接受一个int类型输入,返回一个long类型结果 |
IntUnaryOperator | 接受一个参数同为类型int,返回值类型也为int |
LongBinaryOperator | 接受两个参数同为类型long,返回值类型也为long |
LongConsumer | 接受一个long类型的输入参数,无返回值 |
LongFunction<R> | 接受一个long类型输入参数,返回一个结果 |
LongPredicate | R接受一个long输入参数,返回一个布尔值类型结果 |
LongSupplier | 无参数,返回一个结果long类型的值 |
LongToDoubleFunction | 接受一个long类型输入,返回一个double类型结果 |
LongToIntFunction | 接受一个long类型输入,返回一个int类型结果 |
LongUnaryOperator | 接受一个参数同为类型long,返回值类型也为long |
ObjDoubleConsumer<T> | 接受一个object类型和一个double类型的输入参数,无返回值 |
ObjIntConsumer<T> | 接受一个object类型和一个int类型的输入参数,无返回值 |
ObjLongConsumer<T> | 接受一个object类型和一个long类型的输入参数,无返回值。 |
Predicate<T> | 接受一个输入参数,返回一个布尔值结果 |
Supplier<T> | 无参数,返回一个结果 |
ToDoubleBiFunction<T,U> | 接受两个输入参数,返回一个double类型结果 |
ToDoubleFunction<T> | 接受一个输入参数,返回一个double类型结果 |
ToIntBiFunction<T,U> | 接受两个输入参数,返回一个int类型结果 |
ToIntFunction<T> | 接受一个输入参数,返回一个int类型结果 |
ToLongBiFunction<T,U> | 接受两个输入参数,返回一个long类型结果 |
ToLongFunction<T> | 接受一个输入参数,返回一个long类型结果 |
UnaryOperator<T> | 接受一个参数为类型T,返回值类型也为T |
函数式接口实例
@FunctionalInterface
这个标注用于表示该接口会设计成一个函数式接口。如果你用 @FunctionalInterface 定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo” , 表明存在多个抽象方法。 请注意, @FunctionalInterface 不是必需的, 但对于为此设计的接口而言, 使用它是比较好的做法。 它就像是 @Override标注表示方法被重写了。
// 定义函数式接口
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
// 定义方法
public static String processFile(BufferedReaderProcessor p) throws
IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
// 调用
String result = processFile(br -> br.readLine() + br.readLine());
Predicate
Predicate <T> 接口是一个函数式接口,它接受一个输入参数 T,返回一个布尔值结果。
该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。
该接口用于测试对象是 true 或 false。
与:predicate.and()
或:predicate.or()
非:predicate.negate()
a.or(b).and(c) 可以看作 (a || b) && c
我们可以通过以下实例(Java8Tester.java)来了解函数式接口 Predicate <T> 的使用:
public class Java8Tester {
public static void main(String args[]) {
List < Integer > list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// Predicate<Integer> predicate = n -> true
// n 是一个参数传递到 Predicate 接口的 test 方法
// n 如果存在则 test 方法返回 true
System.out.println("输出所有数据:");
// 传递参数 n
eval(list, n -> true);
// Predicate<Integer> predicate1 = n -> n%2 == 0
// n 是一个参数传递到 Predicate 接口的 test 方法
// 如果 n%2 为 0 test 方法返回 true
System.out.println("输出所有偶数:");
eval(list, n -> n % 2 == 0);
// Predicate<Integer> predicate2 = n -> n > 3
// n 是一个参数传递到 Predicate 接口的 test 方法
// 如果 n 大于 3 test 方法返回 true
System.out.println("输出大于 3 的所有数字:");
eval(list, n -> n > 3);
}
public static void eval(List < Integer > list, Predicate < Integer > predicate) {
for (Integer n: list) {
if (predicate.test(n)) {
System.out.println(n + " ");
}
}
}
}
Consumer
java.util.function.Consumer<T> 定义了一个名叫 accept 的抽象方法,它接受泛型 T 的对象,没有返回( void ) 。你如果需要访问类型 T 的对象,并对其执行某些操作,就可以使用这个接口。
public static <T> void forEach(List <T> list, Consumer <T> c) {
for (T i: list) {
c.accept(i);
}
}
forEach(Arrays.asList(1, 2, 3, 4, 5), System.out::println);
Function
java.util.function.Function<T, R> 接口定义了一个叫作 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。
public static <T, R> List <R> map(List <T> list,
Function <T, R> f) {
List <R> result = new ArrayList < > ();
for (T s: list) {
result.add(f.apply(s));
}
return result;
}
// [7, 2, 6]
List <Integer> l = map(Arrays.asList("lambdas", "in", "action"), String::length);
Function 接口所代表的Lambda表达式复合起来。 Function 接口为此配了 andThen 和 compose 两个默认方法,它们都会返回 Function 的一个实例。
andThen 方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。
比如,假设有一个函数 f 给数字加1 (x -> x + 1) ,另一个函数 g 给数字乘2,你可以将它们组合成一个函数 h ,先给数字加1,再给结果乘2:
Function <Integer, Integer> f = x -> x + 1;
Function <Integer, Integer> g = x -> x * 2;
Function <Integer, Integer> h = f.andThen(g);
int result = h.apply(1); // 4
数学上会写作 g(f(x)) 或(g o f)(x)
compose 方法
Function < Integer, Integer > f = x -> x + 1;
Function < Integer, Integer > g = x -> x * 2;
Function < Integer, Integer > h = f.compose(g);
int result = h.apply(1); // 3
数学上会写作 f(g(x)) 或 (f o g)(x)
Stream
Stream主要用于操作集合,更方便的去处理数据。
java.util.stream.Stream 中的 Stream 接口定义了许多操作。它们可以分为两大类:可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。可以理解为有返回值是Stream的方法是中间操作,返回值非Stream的方法是终端操作。
中间操作会返回另一个流,但是如果没有终端操作,中间操作不会执行任何处理。
Stream调用了终端操作之后,如果再调用,抛出以下异常:
java.lang.IllegalStateException: stream has already been operated upon or closed
Stream只能被消费一次!!!
外部迭代与内部迭代
使用 Collection 接口需要用户去做迭代(比如用 for-each ) ,这称为外部迭代。 相反,Streams库使用内部迭代。
外部迭代:外部迭代实际是使用Iterator对象。
开发中如何选择两种迭代方式:
- 如果循环体需要引用外部变量,或者需要抛Checked Exception,并不可try catch的情况下,推荐使用外部迭代,否则,随意。
- 如果对循环结果无顺序要求,循环之间没有使用操作共同数据,并对执行效率有要求,可以使用
内部迭代-parallelStream。 - 如果需要对集合数据进行处理、分组、过滤等操作,可以使用内部迭代-stream。
流的使用一般包括三件事
- 一个数据源(如集合)来执行一个查询;
- 一个中间操作链,形成一条流的流水线;
- 一个终端操作,执行流水线,并能生成结果。
筛选(filter)
该方法会接受一个谓词(Predicate)(一个返回boolean 的函数)作为参数,并返回一个包括所有符合谓词(Predicate)的元素的流。
List <Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());
切片(limit)
该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给 limit 。如果流是有序的,则最多会返回前 n 个元素。如果流是无序的,limit的结果不会以任务顺序排列。
List <Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(toList());
去重(distinct )
该方法会返回一个元素各异(根据流所生成元素的hashCode 和equals 方法实现)的流。
List <Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream().filter(i -> i % 2 == 0).distinct() .forEach(System.out::println);
跳过元素(skip)
返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。请注意, limit(n) 和 skip(n) 是互补的!
List <Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(toList());
映射(map和flatMap)
map:
一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。
可以改变原有流的类型,重新返回一个新的类型集合。
List <String> dishNames = menu.stream().map(Dish::getName).collect(toList());
flatMap:
一个用于把 Stream<String[]> 转换成Stream<String> 的操作方法,将流扁平化。
Arrays.stream() 的方法可以接受一个数组并产生一个流。
如果需要把一个String[] arrayOfWords = {"Hello", "World"};转换成[G, o, o, d, b, y, e, W, o, r, l, d]
String[] arrayOfWords = {"Hello", "World"};
Stream <String> streamOfwords = Arrays.stream(arrayOfWords);
streamOfwords.map(word -> word.split("")).flatMap(Arrays::stream).collect(Collectors.toList());
匹配( allMatch和anyMatch和noneMatch )
检查是否至少匹配一个元素(anyMatch ):
boolean isVegetarian = menu.stream().anyMatch(Dish::isVegetarian);
检查是否匹配所有元素(allMatch):
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
检查是否所有元素不匹配(noneMatch ):
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
查找(findFirst和findAny)
返回当前流中的任意元素(findAny):
Optional <Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();
获取集合中第一个元素(findFirst):
Optional <Dish> dish = menu.stream().filter(Dish::isVegetarian).findFirst();
何时使用 findFirst 和 findAny
你可能会想,为什么会同时有 findFirst 和 findAny 呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用 findAny ,因为它在使用并行流时限制较少。
归约(reduce)
reduce 接受两个参数:
- 一个初始值,这里是0;
- 一个 BinaryOperator<T> 来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b) -> a + b 。
求和
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
等同于
int sum = numbers.stream().mapToInt(n -> n).sum();
最大值
Optional <Integer> max = numbers.stream().reduce(Integer::max);
等同于
int max = numbers.stream().mapToInt(n -> n).max();
最小值
Optional <Integer> min = numbers.stream().reduce(Integer::min);
等同于
int min = numbers.stream().mapToInt(n -> n).min();
执行原理:
0 作为Lambda( a )的第一个参数,从流中获得 4 作为第二个参数( b ) 。 0 + 4 得到 4 ,它成了新的累积值。然后再用累积值和流中下一个元素 5 调用Lambda,产生新的累积值 9 。接下来,再用累积值和下一个元素 3调用Lambda,得到 12 。最后,用 12 和流中最后一个元素 9 调Lambda,得到最终结果 21 。
ps:reduce如果不设置初始值,会返回一个 Optional 对象。
流操作:无状态和有状态
无状态:操作集合数据时,每一个元素之间数据不相互影响,如map或者filter等操作。
有状态:操作集合数据时,元素之间数据有影响,如sort或者distinct等操作,需要知道每个元素值才能执行处理。
操 作 | 类 型 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream<T> | Predicate<T> | T -> boolean |
distinct | 中间 (有状态-无界) | Stream<T> | ||
skip | 中间 (有状态-有界) | Stream<T> | long | |
limit | 中间 (有状态-有界) | Stream<T> | long | |
map | 中间 | Stream<T> | Function<T, R> | T -> R |
flatMap | 中间 | Stream<T> | Function<T, Stream<R>> | T -> Stream<R> |
sorted | 中间 (有状态-无界) | Stream<T> | Comparator<T> | (T, T) -> int |
anyMatch | 终端 | boolean | Predicate<T> | T -> boolean |
noneMatch | 终端 | boolean | Predicate<T> | T -> boolean |
allMatch | 终端 | boolean | Predicate<T> | T -> boolean |
findAny | 终端 | Optional<T> | ||
findFirst | 终端 | Optional<T> | ||
forEach | 终端 | void | Consumer<T> | T -> void |
collect | 终端 | R | Collector<T, A, R> | |
reduce | 终端 (有状态-有界) | Optional<T> | BinaryOperator<T> | (T, T) -> T |
count | 终端 | long |
原始类型流转换
IntStream 、 DoubleStream 和 LongStream ,分别将流中的元素特化为 int 、 long 和 double,从而避免了暗含的装箱成本。转换的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似 int 和 Integer 之间的效率差异。将流转换为转换的常用方法是 mapToInt 、 mapToDouble 和 mapToLong 。
转换回对象流:
要把原始流转换成一般流(每个 int 都会装箱成一个Integer ) ,可以使用 boxed 方法。
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream <Integer> stream = intStream.boxed();
默认值 OptionalInt:
Optional 可以用Integer 、 String 等参考类型来参数化。对于三种原始流特化,也分别有一个 Optional 原始类型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong 。
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
int max = maxCalories.orElse(1);
数值范围(range 和 rangeClosed):
range 和 rangeClosed这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但
range 是不包含结束值的,而 rangeClosed 则包含结束值。
由值创建流(Stream.of):
Stream.of 通过显式值创建一个流,它可以接受任意数量的参数。
Stream <String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
也可以创建一个空流:
Stream <String> emptyStream = Stream.empty();
由数组创建流(Arrays.stream):
Arrays.stream 从数组创建一个流,它接受一个数组作为参数。
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
由文件生成流:
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。
java.nio.file.Files 中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines ,它会返回一个由指定文件中的各行构成的字符串流。
long uniqueWords = 0;
try (Stream <String> lines =Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
uniqueWords = lines.flatMap(line - > Arrays.stream(line.split(" "))) .distinct().count();
} catch (IOException e) {}
由函数生成流:创建无限流:
Stream API提供了两个静态方法来从函数生成流: Stream.iterate 和 Stream.generate 。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由 iterate和 generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。
Stream.iterate:
Stream.iterate(0, n - > n + 2).limit(10).forEach(System.out::println);
Stream.generate:
Stream.generate(Math::random).limit(5).forEach(System.out::println);
收集器
汇总(Collectors.counting 、 Collectors.summingInt、 Collectors.summingLong 、Collectors.summingDouble、 Collectors.averagingInt、 Collectors.averagingLong 、Collectors.averagingDouble、Collectors.summarizingInt、Collectors.summarizingLong、Collectors.summarizingDouble):
总数:
Collectors.counting:
long howManyDishes = menu.stream().collect(Collectors.counting());
等同于
long howManyDishes = menu.stream().count();
求和:
Collectors.summingInt:
int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));
等同于
int sum = menu.stream().mapToInt(Dish::getCalories).sum();
Collectors.summingLong:
long totalCalories = menu.stream().collect(Collectors.summingLong(Dish::getCalories));
等同于
long sum = menu.stream().mapToLong(Dish::getCalories).sum();
Collectors.summingDouble:
double totalCalories = menu.stream().collect(Collectors.summingDouble(Dish::getCalories));
等同于
double sum = menu.stream().mapToDouble(Dish::getCalories).sum();
平均数:
Collectors.averagingInt:
int avgCalories = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));
Collectors.averagingLong:
long avgCalories = menu.stream().collect(Collectors.averagingLong(Dish::getCalories));
Collectors.averagingDouble:
double avgCalories = menu.stream().collect(Collectors.averagingDouble(Dish::getCalories));
汇总(总和、平均值、最大值和最小值):
Collectors.summarizingInt:
IntSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
Collectors.summarizingLong:
LongSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingLong(Dish::getCalories));
Collectors.summarizingDouble:
DoubleSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingDouble(Dish::getCalories));
查找流中的最大值和最小值(Collectors.maxBy 和 Collectors.minBy):
Collectors.maxBy :
Comparator <Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional <Dish> mostCalorieDish = menu.stream().collect(Collectors.maxBy(dishCaloriesComparator));
连接字符串(Collectors.joining):
joining 工厂方法返回的收集器会把对流中每一个对象应用 toString 方法得到的所有字符串连接成一个字符串。 joining 在内部使用了 StringBuilder 来把生成的字符串逐个追加起来。 joining 工厂方法有一个重载版本可以接受元素之间的分界符。
String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining());
分隔符:
String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining(”, “));
分组(Collectors.groupingBy):
普通的单参数 groupingBy(f) (其中 f 是分类函数)实际上是 groupingBy(f, toList()) 的简便写法。
第一个参数是指定以什么分组
第二个参数是指定使用的Map
第三个参数是指定Collector
Map <Dish.Type, List <Dish>> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));
多级分组:
Map<Dish.Type, Map<Long, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.groupingBy(Dish::getCalories)));
分组汇总:
Map <Dish.Type, Long> typesCount = menu.stream().collect(Collectors.groupingBy(Dish::getType, counting()));
分组汇总最大值:
Map Dish.Type, Optional <Dish>> mostCaloricByType = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))));
分组结果包装Optional转换具体值(Collectors.collectingAndThen)
Map <Dish.Type, Dish> mostCaloricByType = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));
分组类型转换(Collectors.mapping):
这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。
Map <Dish.Type, Set <CaloricLevel>> caloricLevelsByType = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.mapping(
dish - > {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}, Collectors.toSet())));
分区(Collectors.partitioningBy)
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组 Map 的键类型是 Boolean ,于是它最多可以分为两组—— true 是一组, false 是一组。
Map <Boolean, List <Dish>> partitionedMenu = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
排序(Comparator )
Comparator 接口现在同时包含了默认方法和静态方法。可以使用静态方法 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() 方法类似。
并行流
使用并行流可以通过parallelStream或者parallel方法。对顺序流调用 parallel 方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个 boolean 标志,表示你想让调用 parallel 之后进行的所有操作都并行执行。并行流转换成顺序流使用sequential方法。
并行化并不是没有代价的。并行化过程本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间移动数据的代价也可能比你想的要大, 所以很重要的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长。总而言之,很多情况下不可能或不方便并行化。然而,在使用并行 Stream 加速代码之前,你必须确保用得对;如果结果错了,算得快就毫无意义了。让我们来看一个常见的陷阱。
分支/合并框架
分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。它是 ExecutorService 接口的一个实现,它把子任务分配给线程池(称为 ForkJoinPool )中的工作线程。首先来看看如何定义任务和子任务。
RecursiveTask:
要把任务提交到这个池, 必须创建 RecursiveTask<R> 的一个子类, 其中 R 是并行化任务 (以及所有子任务)产生的结果类型,或者如果任务不返回结果,则是 RecursiveAction 类型(当然它可能会更新其他非局部机构) 。要定义 RecursiveTask, 只需实现它唯一的抽象方法compute :
protected abstract R compute();
这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成单个子任务结果的逻辑。正由于此,这个方法的实现类似于下面的伪代码:
if (任务足够小或不可分) {
顺序计算该任务
} else {
将任务分成两个子任务
递归调用本方法,拆分每个子任务,等待所有子任务完成
合并每个子任务的结果
}
使用分支/合并框架的最佳做法:
虽然分支/合并框架还算简单易用,不幸的是它也很容易被误用。以下是几个有效使用它的最佳做法。
- 对一个任务调用 join 方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂,因为每个子任务都必须等待另一个子任务完成才能启动。
- 不应该在 RecursiveTask 内部使用 ForkJoinPool 的 invoke 方法。相反,你应该始终直接调用 compute 或 fork 方法,只有顺序代码才应该用 invoke 来启动并行计算。
- 对子任务调用 fork 方法可以把它排进 ForkJoinPool 。同时对左边和右边的子任务调用它似乎很自然,但这样做的效率要比直接对其中一个调用 compute 低。这样做你可以为其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
- 调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在你喜欢的IDE里面看栈跟踪(stack trace)来找问题,但放在分支合并计算上就不行了,因为调用 compute的线程并不是概念上的调用方,后者是调用 fork 的那个。
- 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计算快。我们已经说过,一个任务可以分解成多个独立的子任务,才能让性能在并行化时有所提升。所有这些子任务的运行时间都应该比分出新任务所花的时间长;一个惯用方法是把输入/输出放在一个子任务里,计算放在另一个里,这样计算就可以和输入/输出同时进行。此外,在比较同一算法的顺序和并行版本的性能时还有别的因素要考虑。就像任何其他Java代码一样,分支/合并框架需要“预热”或者说要执行几遍才会被JIT编译器优化。这就是为什么在测量性能之前跑几遍程序很重要,我们的测试框架就是这么做的。同时还要知道,编译器内置的优化可能会为顺序版本带来一些优势(例如执行死码分析——删去从未被使用的计算) 。
Spliterator:
Spliterator 是Java 8中加入的另一个新接口;这个名字代表“可分迭代器” (splitable iterator) 。和 Iterator 一样, Spliterator 也用于遍历数据源中的元素,但它是为了并行执行而设计的。虽然在实践中可能用不着自己开发 Spliterator ,但了解一下它的实现方式会让你对并行流的工作原理有更深入的了解。Java 8已经为集合框架中包含的所有数据结构提供了一个默认的 Spliterator 实现。 集合实现了 Spliterator 接口, 接口提供了一个 spliterator 方法。
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
与往常一样, T 是 Spliterator 遍历的元素的类型。 tryAdvance 方法的行为类似于普通的Iterator ,因为它会按顺序一个一个使用 Spliterator 中的元素,并且如果还有其他元素要遍历就返回 true 。 但 trySplit 是专为 Spliterator 接口设计的, 因为它可以把一些元素划出去分给第二个 Spliterator (由该方法返回) ,让它们两个并行处理。 Spliterator 还可通过estimateSize 方法估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值也有助于让拆分均匀一点。
流拆分过程:
将 Stream 拆分成多个部分的算法是一个递归过程,第一步是对第一个Spliterator 调用 trySplit ,生成第二个 Spliterator 。第二步对这两个 Spliterator 调用trySplit ,这样总共就有了四个 Spliterator 。这个框架不断对 Spliterator 调用 trySplit直到它返回 null ,表明它处理的数据结构不能再分割,最后,这个递归拆分过程到第四步就终止了,这时所有的 Spliterator 在调用 trySplit 时都返回了 null 。
这个拆分过程也受 Spliterator 本身的特性影响,而特性是通过 characteristics 方法声明的,它将返回一个 int ,代表 Spliterator 本身特性集的编码。使用 Spliterator 的客户可以用这些特性来更好地控制和优化它的使用。
异步编程
Future
Future 接口在Java 5中被引入,设计初衷是对将来某个时刻会发生的结果进行建模。它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future 中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要呆呆等待耗时的操作完成。
ExecutorService executor = Executors.newCachedThreadPool();
Future < Double > future = executor.submit(new Callable <Double> () {
public Double call() {
return doSomeLongComputation();
}
});
doSomethingElse();
try {
Double result = future.get(1, TimeUnit.SECONDS);
} catch (ExecutionException ee) {
// 计算抛出一个异常
} catch (InterruptedException ie) {
// 当前线程在等待过程中被中断
} catch (TimeoutException te) {
// 在Future对象完成之前超过已过期
}
这种编程方式让你的线程可以在 ExecutorService 以并发方式调用另一个线程执行耗时操作的同时,去执行一些其他的任务。
局限性:
Future 接口提供了方法来检测异步计算是否已经结束(使用isDone 方法) ,等待异步操作结束,以及获取计算的结果。
- 将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。
- 等待 Future 集合中的所有任务都完成。
- 仅等待 Future 集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同一个值) ,并返回它的结果。
- 通过编程方式完成一个 Future 任务的执行(即以手工设定异步操作结果的方式) 。
- 应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并能使用 Future计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果) 。
CompletableFuture
CompletableFuture 的 completeExceptionally 方法将导致 CompletableFuture 内发生问题的异常抛出。客户端现在会收到一个 ExecutionException 异常,该异常接收了一个包含失败原因的Exception 参数。
使用工厂方法 supplyAsync 创建 CompletableFuture:
public Future <Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
supplyAsync 方法接受一个生产者( Supplier )作为参数,返回一个 CompletableFuture对象, 该对象完成异步执行后会读取调用生产者方法的返回值。 生产者方法会交由 ForkJoinPool池中的某个执行线程( Executor )运行,但是你也可以使用 supplyAsync 方法的重载版本,传递第二个参数指定不同的执行线程执行生产者方法。一般而言,向 CompletableFuture 的工厂方法传递可选参数,指定生产者方法的执行线程是可行的。
CompletableFuture和stream组合使用:
public List <String> findPrices(String product) {
List < CompletableFuture <String>> priceFutures = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() - > shop.getName() + " price is " +
shop.getPrice(product)))
.collect(Collectors.toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
利用 CompletableFutures 向其提交任务执行是个不错的主意。处理需大量使用异步操作的情况时,这几乎是最有效的策略。
构造同步和异步操作:
public List <String> findPrices(String product) {
List <CompletableFuture <String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote ->
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor)))
.collect(Collectors.toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
Java 8的 CompletableFuture API提供了名为 thenCompose 的方法,它就是专门为这一目的而设计的, thenCompose 方法允许你对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。创建两个 CompletableFutures 对象,对第一个 CompletableFuture 对象调用 thenCompose ,并向其传递一个函数。当第一个CompletableFuture 执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一个 CompletableFuture 的返回做输入计算出的第二个 CompletableFuture 对象。thenCompose 方法像 CompletableFuture 类中的其他方法一样,也提供了一个以 Async 后缀结尾的版本 thenComposeAsync 。通常而言,名称中不带 Async的方法和它的前一个任务一样,在同一个线程中运行;而名称以 Async 结尾的方法会将后续的任务提交到一个线程池,所以每个任务是由不同的线程处理的。
方法名 | 描述 |
---|---|
allOf(CompletableFuture<?>... cfs) | 等待所有任务完成,构造后CompletableFuture完成 |
anyOf(CompletableFuture<?>... cfs) | 只要有一个任务完成,构造后CompletableFuture就完成 |
runAsync(Runnable runnable) | 使用ForkJoinPool.commonPool()作为它的线程池执行异步代码 |
runAsync(Runnable runnable, Executor executor) | 使用指定的thread pool执行异步代码 |
supplyAsync(Supplier<U> supplier) | 使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作有返回值 |
supplyAsync(Supplier<U> supplier,Executor executor) | 使用指定的thread pool执行异步代码,异步操作有返回值 |
complete(T t) | 完成异步执行,并返回future的结果 |
completeExceptionlly(Throwable ex) | 异步执行不正常的结束 |
cancel(boolean mayInterruptIfRunning) | 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束 |
isCancelled() | 任务是否已经取消,任务正常完成前将其取消,则返回 true |
isDone() | 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true |
get() | throws InterruptedException, ExecutionException 等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException |
get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException | 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计 算超时,将抛出TimeoutException |
thenApply(Function<? super T,? extends U> fn) | 转换一个新的CompletableFuture对象 |
thenApplyAsync(Function<? super T,? extends U> fn) | 异步转换一个新的CompletableFuture对象 |
thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) | 使用指定的thread pool执行异步代码,异步转换一个新的CompletableFuture对象 |
thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) | 在异步操作完成的时候对异步操作的结果进行一些操作,并且仍然返回CompletableFuture类型 |
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) | 在异步操作完成的时候对异步操作的结果进行一些操作,并且仍然返回CompletableFuture类型。使用ForkJoinPool |
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) | 在异步操作完成的时候对异步操作的结果进行一些操作,并且仍然返回CompletableFuture类型。使用指定的线程池 |
thenAccept(Consumer<? super T> action) | 当CompletableFuture完成计算结果,只对结果执行Action,而不返回新的计算值 |
thenAcceptAsync(Consumer<? super T> action) | 当CompletableFuture完成计算结果,只对结果执行Action,而不返回新的计算值,使用ForkJoinPool |
thenAcceptAsync(Consumer<? super T> action,Executor executor) | 当CompletableFuture完成计算结果,只对结果执行Action,而不返回新的计算值 |
thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) | 当两个CompletableFuture都正常完成后,执行提供的fn,用它来组合另外一个CompletableFuture的结果 |
thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) | 当两个CompletableFuture都正常完成后,执行提供的fn,用它来组合另外一个CompletableFuture的结果,使用ForkJoinPool |
thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor) | 当两个CompletableFuture都正常完成后,执行提供的fn,用它来组合另外一个CompletableFuture的结果,使用指定的线程池 |
thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action) | 当两个CompletableFuture都正常完成后,执行提供的action,用它来组合另外一个CompletableFuture的结果 |
thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action) | 当两个CompletableFuture都正常完成后,执行提供的action,用它来组合另外一个CompletableFuture的结果,使用ForkJoinPool |
thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action, Executor executor) | 当两个CompletableFuture都正常完成后,执行提供的action,用它来组合另外一个CompletableFuture的结果,使用指定的线程池 |
whenComplete(BiConsumer<? super T,? super Throwable> action) | 当CompletableFuture完成计算结果时对结果进行处理,或者当CompletableFuture产生异常的时候对异常进行处理 |
whenCompleteAsync(BiConsumer<? super T,? super Throwable> action) | 当CompletableFuture完成计算结果时对结果进行处理,或者当CompletableFuture产生异常的时候对异常进行处理,使用ForkJoinPool |
whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor) | 当CompletableFuture完成计算结果时对结果进行处理,或者当CompletableFuture产生异常的时候对异常进行处理,使用指定的线程池。 |
handle(BiFunction<? super T, Throwable, ? extends U> fn) | 当CompletableFuture完成计算结果或者抛出异常的时候,执行提供的fn |
handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) | 当CompletableFuture完成计算结果或者抛出异常的时候,执行提供的fn,使用ForkJoinPool |
handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) | 当CompletableFuture完成计算结果或者抛出异常的时候,执行提供的fn,使用指定的线程池 |
- thenApply的功能相当于将CompletableFuture<T>转换成CompletableFuture<U>
- thenCompose可以用于组合多个CompletableFuture,将前一个结果作为下一个计算的参数,它们之间存在着先后顺序
- 现在有CompletableFuture<T>、CompletableFuture<U>和一个函数(T,U)->V,thenCompose就是将CompletableFuture<T>和CompletableFuture<U>变为CompletableFuture<V>
- 使用thenCombine()之后future1、future2之间是并行执行的,最后再将结果汇总。这一点跟thenCompose()不同
- thenAcceptBoth跟thenCombine类似,但是返回CompletableFuture<Void>类型
- handle()的参数是BiFunction,apply()方法返回R,相当于转换的操作
- whenComplete()的参数是BiConsumer,accept()方法返回void
- thenAccept()是只会对计算结果进行消费而不会返回任何结果的方法
时间API
Clock
Clock类提供了访问当前日期和时间的方法,Clock是时区敏感的,可以用来取代 System.currentTimeMillis() 来获取当前的微秒数。某一个特定的时间点也可以使用Instant类来表示,Instant类也可以用来创建老的java.util.Date对象。
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);
LocalDate
该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。通过静态工厂方法 of 创建一个 LocalDate 实例。 LocalDate 实例提供了多种方法来读取常用的值,比如年份、月份、星期几等。
LocalDate date = LocalDate.of(2018, 10, 1);
int year = date.getYear();
Month month = date.getMonth();
int day = date.getDayOfMonth();
DayOfWeek dow = date.getDayOfWeek();
int len = date.lengthOfMonth();
boolean leap = date.isLeapYear();
等同于
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
获取当前时间:
LocalDate today = LocalDate.now();
LocalTime
一天中的时间,比如13:45:20,可以使用 LocalTime 类表示。你可以使用 of 重载的两个工厂方法创建 LocalTime 的实例。 第一个重载函数接收小时和分钟, 第二个重载函数同时还接收秒。同 LocalDate 一样, LocalTime 类也提供了一些 getter 方法访问这些变量的值。
LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();
LocalDate 和 LocalTime 都可以通过解析代表它们的字符串创建。使用静态方法 parse:
LocalDate date = LocalDate.parse("2018-03-18");
LocalTime time = LocalTime.parse("13:45:20");
可以向 parse 方法传递一个 DateTimeFormatter 。该类的实例定义了如何格式化一个日
期或者时间对象。它是替换老版 java.util.DateFormat 的推荐替代品。一旦传递的字符串参数无法被解析为合法的 LocalDate 或 LocalTime 对象, 这两个 parse 方法都会抛出一个继承自 RuntimeException 的 DateTimeParseException 异常。
LocalDateTime
这个复合类名叫 LocalDateTime ,是 LocalDate 和 LocalTime 的合体。它同时表示了日期和时间, 但不带有时区信息, 你可以直接创建, 也可以通过合并日期和时间对象构造。
LocalDateTime dt1 = LocalDateTime.of(2018, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
通过它们各自的 atTime 或者 atDate 方法,向 LocalDate 传递一个时间对象,或者向LocalTime 传递一个日期对象的方式,你可以创建一个 LocalDateTime 对象。你也可以使用toLocalDate 或者 toLocalTime 方法,从 LocalDateTime 中提取 LocalDate 或者 LocalTime组件:
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
Instant
可以通过向静态工厂方法 ofEpochSecond 传递一个代表秒数的值创建一个该类的实例。 静态工厂方法 ofEpochSecond 还有一个增强的重载版本,它接收第二个以纳秒为单位的参数值,对传入作为秒数的参数进行调整。重载的版本会调整纳秒参数,确保保存的纳秒分片在0到999 999999之间。
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000); // 2秒 之 后 再 加上100万纳秒(1秒)
Instant.ofEpochSecond(4, -1_000_000_000); // 4秒之前的100万纳秒(1秒)
修改操作:
如果你已经有一个 LocalDate 对象, 想要创建它的一个修改版, 最直接也最简单的方法是使用 withAttribute 方法。 withAttribute 方法会创建对象的一个副本,并按照需要修改它的属性。
LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.withYear(2011); // 2011-03-18
LocalDate date3 = date2.withDayOfMonth(25); // 2011-03-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9); // 2011-09-25
LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.plusWeeks(1); // 2014-03-25
LocalDate date3 = date2.minusYears(3); // 2011-03-25
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2011-09-25
LocalDate 、 LocalTime 、 LocalDateTime 以及 Instant通用方法
方法名 | 是否是静态方法 | 描述 |
---|---|---|
from | 是 | 依据传入的 Temporal 对象创建对象实例 |
now | 是 | 依据系统时钟创建 Temporal 对象 |
of | 是 | 由 Temporal 对象的某个部分创建该对象的实例 |
parse | 是 | 由字符串创建 Temporal 对象的实例 |
atOffset | 否 | 将 Temporal 对象和某个时区偏移相结合 |
atZone | 否 | 将 Temporal 对象和某个时区相结合 |
format | 否 | 使用某个指定的格式器将 Temporal 对象转换为字符串 ( Instant 类不提供该方法) |
get | 否 | 读取 Temporal 对象的某一部分的值 |
minus | 否 | 创建 Temporal 对象的一个副本, 通过将当前 Temporal 对象的值减去一定的时长创建该副本 |
plus | 否 | 创建 Temporal 对象的一个副本, 通过将当前 Temporal 对象的值加上一定的时长创建该副本 |
with | 否 | 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本 |
LocalDate date = LocalDate.of(2014, 3, 18);
date = date.with(ChronoField.MONTH_OF_YEAR, 9);
date = date.plusYears(2).minusDays(10);
date.withYear(2011);
答案: 2016-09-08 。
每个动作都会创建一个新的 LocalDate 对象,后续的方法调用可以操纵前一方法创建的对象。这段代码的最后一句不会产生任何我们能看到的效果,因为它像前面的那些操作一样,会创建一个新的 LocalDate 实例,不过我们并没有将这个新创建的值赋给任何的变量。
Duration
用于比较LocalTime之间的时间差, Duration 类主要用于以秒和纳秒衡量时间的长短。
LocalTime time1 = LocalTime.now();
LocalTime time2 = LocalTime.of(11, 0, 0);
Duration d1 = Duration.between(time1, time2);
Period
用于比较LocalDate之间的时间差, Period类主要用于以年月日衡量时间的长短。
Period tenDays = Period.between(LocalDate.of(2014, 3, 8), LocalDate.of(2014, 3, 18));
Duration和Period通用方法
方法名 | 是否是静态方法 | 方法描述 |
---|---|---|
between | 是 | 创建两个时间点之间的 interval |
from | 是 | 由一个临时时间点创建 interval |
of | 是 | 由它的组成部分创建 interval的实例 |
parse | 是 | 由字符串创建 interval 的实例 |
addTo | 否 | 创建该 interval 的副本,并将其叠加到某个指定的 temporal 对象 |
get | 否 | 读取该 interval 的状态 |
isNegative | 否 | 检查该 interval 是否为负值,不包含零 |
isZero | 否 | 检查该 interval 的时长是否为零 |
minus | 否 | 通过减去一定的时间创建该 interval 的副本 |
multipliedBy | 否 | 将 interval 的值乘以某个标量创建该 interval 的副本 |
negated | 否 | 以忽略某个时长的方式创建该 interval 的副本 |
plus | 否 | 以增加某个指定的时长的方式创建该 interval 的副本 |
subtractFrom | 否 | 从指定的 temporal 对象中减去该 interval |
TemporalAdjuster
将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的 with 方法, 向其传递一个提供了更多定制化选择的 TemporalAdjuster 对象,更加灵活地处理日期。
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth());
方法名 | 描述 |
---|---|
dayOfWeekInMonth | 创建一个新的日期,它的值为同一个月中每一周的第几天 |
firstDayOfMonth | 创建一个新的日期,它的值为当月的第一天 |
firstDayOfNextMonth | 创建一个新的日期,它的值为下月的第一天 |
firstDayOfNextYear | 创建一个新的日期,它的值为明年的第一天 |
firstDayOfYear | 创建一个新的日期,它的值为当年的第一天 |
firstInMonth | 创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值 |
lastDayOfMonth | 创建一个新的日期,它的值为当月的最后一天 |
lastDayOfNextMonth | 创建一个新的日期,它的值为下月的最后一天 |
lastDayOfNextYear | 创建一个新的日期,它的值为明年的最后一天 |
lastDayOfYear | 创建一个新的日期,它的值为今年的最后一天 |
lastInMonth | 创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值 |
next/previous | 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期 |
nextOrSame/previousOrSame | 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象 |
DateTimeFormatter
处理日期和时间对象时,格式化以及解析日期时间对象是另一个非常重要的功能。新的java.time.format 包就是特别为这个目的而设计的。这个包中,最重要的类是 DateTime-Formatter 。 创建格式器最简单的方法是通过它的静态工厂方法以及常量。 像 BASIC_ISO_DATE和 ISO_LOCAL_DATE 这 样 的 常 量 是 DateTimeFormatter 类 的 预 定 义 实 例 。 所 有 的DateTimeFormatter 实例都能用于以一定的格式创建代表特定日期或时间的字符串。
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18
等同于
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
和老的 java.util.DateFormat 相比较,所有的 DateTimeFormatter 实例都是线程安全的。所以,你能够以单例模式创建格式器实例,就像 DateTimeFormatter 所定义的那些常量,并能在多个线程间共享这些实例。 DateTimeFormatter 类还支持一个静态工厂方法,它可以按照某个特定的模式创建格式器。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);
如果还需要更加细粒度的控制, DateTimeFormatterBuilder 类还提供了更复杂的格式器,你可以选择恰当的方法,一步一步地构造自己的格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式) 、填充,以及在格式器中指定可选节。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
ZoneId
之前看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间API新增加的重要功能,使用新版日期和时间API时区的处理被极大地简化了。新的 java.time.ZoneId类是老版 java.util.TimeZone 的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心,比如处理日光时(Daylight Saving Time,DST)这种问题。跟其他日期和时间类一样, ZoneId 类也是无法修改的。
ZoneId romeZone = ZoneId.of("Europe/Rome");
地区ID都为 “{区域}/{城市}” 的格式, 这些地区集合的设定都由英特网编号分配机构 (IANA)的时区数据库提供。你可以通过Java 8的新方法 toZoneId 将一个老的时区对象转换为 ZoneId :
ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦得到一个 ZoneId 对象,你就可以将它与 LocalDate 、 LocalDateTime 或者是 Instant对象整合起来,构造为一个 ZonedDateTime 实例,它代表了相对于指定时区的时间点。
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
ZonedDateTime
将 LocalDateTime 转换为 Instant :
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
将 Instant 转换为 LocalDateTime :
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
计算时区
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
日历系统
Java 8中另外还提供了4种其他的日历系统。这些日历系统中的每一个都有一个对应的日志类,分别是 ThaiBuddhistDate 、MinguoDate 、 JapaneseDate 以及 HijrahDate 。所有这些类以及 LocalDate 都实现了ChronoLocalDate 接口,能够对公历的日期进行建模。利用 LocalDate 对象,你可以创建这些类的实例。
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(date);
等同于
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
java8类库
@Repeatable
如果一个注解在设计之初就是可重复的,你可以直接使用它。但是,如果你提供的注解是为用户提供的,那么就需要做一些工作,说明该注解可以重复。
新增方法
类/接口 | 新方法 |
---|---|
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 |
Map
forEach 该方法签名为void forEach(BiConsumer<? super K,? super V> action),作用是对Map中的每个映射执行action指定的操作,其中BiConsumer是一个函数接口,里面有一个待实现方法void accept(T t, U u)。
java8之前写法:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
java8:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
getOrDefault 方法就可以替换现在检测 Map 中是否包含给定键映射的惯用方法。如果 Map 中不存在这样的键映射,你可以提供一个默认值,方法会返回该默认值。
java8之前写法:
Map<String, Integer> carInventory = new HashMap<>();
Integer count = 0;
if (map.containsKey("Aston Martin")) {
count = map.get("Aston Martin");
}
java8:
Integer count = map.getOrDefault("Aston Martin", 0);
putIfAbsent 方法签名为V putIfAbsent(K key, V value),作用是只有在不存在key值的映射或映射值为null时,才将value指定的值放入到Map中,否则不对Map做更改.该方法将条件判断和赋值合二为一,使用起来更加方便。
remove(Object key, Object value)方法,只有在当前Map中key正好映射到value时才删除该映射,否则什么也不做。
replace 在Java7及以前,要想替换Map中的映射关系可通过put(K key, V value)方法实现,该方法总是会用新值替换原来的值.为了更精确的控制替换行为,Java8在Map中加入了两个replace()方法,分别如下:
- replace(K key, V value),只有在当前Map中key的映射存在时才用value去替换原来的值,否则什么也不做。
- replace(K key, V oldValue, V newValue),只有在当前Map中key的映射存在且等于oldValue时才用newValue去替换原来的值,否则什么也不做。
replaceAll 该方法签名为replaceAll(BiFunction<? super K,? super V,? extends V> function),作用是对Map中的每个映射执行function指定的操作,并用function的执行结果替换原来的value,其中BiFunction是一个函数接口,里面有一个待实现方法R apply(T t, U u)。
java8之前写法:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
entry.setValue(entry.getValue().toUpperCase());
}
java8:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());
merge 该方法签名为merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction),作用是:
如果Map中key对应的映射不存在或者为null,则将value(不能是null)关联到key上;
否则执行remappingFunction,如果执行结果非null则用该结果跟key关联,否则在Map中删除key的映射。
Map<String, String> myMap = new HashMap<>();
myMap.put("A", "str01A");
myMap.merge("A", "merge01", String::concat); // str01A merge01
compute 该方法签名为compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) ,如果map里有这个key,那么remappingFunction输入的v就是现在的值,返回的是对应value,如果没有这个key,那么输入的v是null。
map.compute(key, (k, v) -> v == null ? newMsg : v.concat(newMsg));
computeIfAbsent 该方法签名为V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction),作用是:只有在当前Map中不存在key值的映射或映射值为null时,才调用mappingFunction,并在mappingFunction执行结果非null时,将结果跟key关联。
java8之前写法:
Map<Integer, Set<String>> map = new HashMap<>();
if (map.containsKey(1)) {
map.get(1).add("one");
} else {
Set<String> valueSet = new HashSet<String>();
valueSet.add("one");
map.put(1, valueSet);
}
java8:
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");
computeIfPresent 该方法签名为V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),作用跟computeIfAbsent()相反,即,只有在当前Map中存在key值的映射且非null时,才调用remappingFunction,如果remappingFunction执行结果为null,则删除key的映射,否则使用该结果替换key原来的映射。
Collection
removeIf 该方法签名为boolean removeIf(Predicate<? super E> filter),作用是删除容器中所有满足filter指定条件的元素,其中Predicate是一个函数接口,里面只有一个待实现方法boolean test(T t),同样的这个方法的名字根本不重要,因为用的时候不需要书写这个名字。
java8之前写法:
// 使用迭代器删除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().length()>3) { // 删除长度大于3的元素
it.remove();
}
}
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
// 删除长度大于3的元素
list.removeIf(str -> str.length() > 3);
replaceAll 该方法签名为void replaceAll(UnaryOperator<E> operator),作用是对每个元素执行operator指定的操作,并用操作结果来替换原来的元素。其中UnaryOperator是一个函数接口,里面只有一个待实现函数T apply(T t)。
java8之前写法:
// 使用下标实现元素替换
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for (int i=0; i<list.size(); i++) {
String str = list.get(i);
if (str.length()>3) {
list.set(i, str.toUpperCase());
}
}
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
if (str.length() > 3) {
return str.toUpperCase();
}
return str;
});
sort 该方法定义在List接口中,方法签名为void sort(Comparator<? super E> c),该方法根据c指定的比较规则对容器元素进行排序。Comparator接口我们并不陌生,其中有一个方法int compare(T o1, T o2)需要实现,显然该接口是个函数接口。
java8之前写法:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>() {
@Override public int compare(String str1, String str2) {
return str1.length() - str2.length();
}
});
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length() - str2.length());
spliterator 方法签名为Spliterator<E> spliterator(),该方法返回容器的可拆分迭代器。从名字来看该方法跟iterator()方法有点像,我们知道Iterator是用来迭代容器的,Spliterator也有类似作用,但二者有如下不同:
- Spliterator既可以像Iterator那样逐个迭代,也可以批量迭代。批量迭代可以降低迭代的开销。
- Spliterator是可拆分的,一个Spliterator可以通过调用Spliterator<T> trySplit()方法来尝试分成两个。一个是this,另一个是新返回的那个,这两个迭代器代表的元素没有重叠。
可通过(多次)调用Spliterator.trySplit()方法来分解负载,以便多线程处理。
stream和parallelStream 分别返回该容器的Stream视图表示,不同之处在于parallelStream()返回并行的Stream。Stream是Java函数式编程的核心类。
并发包
原子操作
java.util.concurrent.atomic 包提供了多个对数字类型进行操作的类,比如 AtomicInteger 和 AtomicLong ,它们支持对单一变量的原子操作。这些类在Java 8中新增了更多的方法支持。
- getAndUpdate —— 以原子方式用给定的方法更新当前值,并返回变更之前的值。
- updateAndGet —— 以原子方式用给定的方法更新当前值,并返回变更之后的值。
- getAndAccumulate —— 以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之前的值。
- accumulateAndGet —— 以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之后的值。
Adder 和 Accumulator:
多线程的环境中,如果多个线程需要频繁地进行更新操作,且很少有读取的动作(比如,在统计计算的上下文中) ,Java API文档中推荐大家使用新的类 LongAdder 、 LongAccumulator 、DoubleAdder 以及 DoubleAccumulator ,尽量避免使用它们对应的原子类型。这些新的类在设计之初就考虑了动态增长的需求,可以有效地减少线程间的竞争。
LongAddr 和 DoubleAdder 类都支持加法操作,而 LongAccumulator 和 DoubleAccumulator 可以使用给定的方法整合多个值。
LongAdder adder = new LongAdder();
adder.add(10);
long sum = adder.sum();
等同于
LongAccumulator acc = new LongAccumulator(Long::sum, 0);
acc.accumulate(10);
long result = acc.get();
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 中的最大值:
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
使用 parallelSort
parallelSort 方法会以并发的方式对指定的数组进行排序,你可以使用自然顺序,也可以为数组对象定义特别的 Comparator 。
使用 setAll 和 parallelSetAll
setAll 和 parallelSetAll 方法可以以顺序的方式也可以用并发的方式,使用提供的函数计算每一个元素的值,对指定数组中的所有元素进行设置。该函数接受元素的索引,返回该索引元素对应的值。由于 parallelSetAll 需要并发执行,所以提供的函数必须没有任何副作用。
int[] evenNumbers = new int[10];
Arrays.setAll(evenNumbers, i -> i * 2); // 0, 2, 4, 6...
使用 parallelPrefix
parallelPrefix 方法以并发的方式, 用用户提供的二进制操作符对给定数组中的每个元素进行累积计算。
int[] ones = new int[10];
Arrays.fill(ones, 1);
Arrays.parallelPrefix(ones, (a, b) -> a + b);
Number
Number 类中新增的方法如下。
- Short 、 Integer 、 Long 、 Float 和 Double 类提供了静态方法 sum 、 min 和 max 。
- 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 。
Files
Files 类最引人注目的改变是,你现在可以用文件直接产生流。通过 Files.lines 方法你可以以延迟方式读取文件的内容,并将其作为一个流。此外,还有一些非常有用的静态方法可以返回流。
- Files.list —— 生成由指定目录中所有条目构成的 Stream<Path> 。这个列表不是递归包含的。由于流是延迟消费的,处理包含内容非常庞大的目录时,这个方法非常有用。
- Files.walk —— 和 Files.list 有些类似,它也生成包含给定目录中所有条目的Stream<Path> 。不过这个列表是递归的,你可以设定递归的深度。注意,该遍历是依照深度优先进行的。
- Files.find —— 通过递归地遍历一个目录找到符合条件的条目,并生成一个Stream<Path> 对象。
Reflection
Relection 接口的另一个变化是新增了可以查询方法参数信息的API,比如,你现在可以使用新增的 java.lang.reflect.Parameter 类查询方法参数的名称和修饰符,这个类被新的java.lang.reflect.Executable 类所引用, 而 java.lang.reflect.Executable 通用函数和构造函数共享的父类。
String
String 类也新增了一个静态方法,名叫 join 。你大概已经猜出它的功能了,它可以用一个分隔符将多个字符串连接起来。
String authors = String.join(", ", "Raoul", "Mario", "Alan");
PS
泛型
Java类型要么是引用类型(比如 Byte 、 Integer 、 Object 、 List ) ,要么是原始类型(比如 int 、 double 、 byte 、 char ) 。但是泛型(比如 Consumer<T> 中的 T )只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应accept 方法的实现Lambda是 Function接口的 apply 方法的实现的原始类型,叫作拆箱(unboxing) 。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。
工具类库
Guava、Apache和lambdaj
广义归约( Collectors.reducing)
它需要三个参数。
第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言 0 是一个合适的值。
第二个参数就是转换成一个表示其所含热量的 int 。
第三个参数是一个 BinaryOperator ,将两个项目累积成一个同类型的值。
求和:
int totalCalories = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j));
找出集合中最大值:
Optional <Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
Collectors 类的静态工厂方法
工厂方法 | 返回类型 | 描 述 | 使用示例 |
---|---|---|---|
toList | List<T> | 把流中所有项目收集到一个 List | List<Dish> dishes = menuStream.collect(Collectors.toList()); |
toSet | Set<T> | 把流中所有项目收集到一个 Set ,删除重复项 | Set<Dish> dishes = menuStream.collect(Collectors.toSet()); |
toMap | Map<T, K> | 把流中所有项目收集到一个 Map ,删除重复项,默认情况下,出现重复数据会报错 | Map<Long, Dish> dishesMap = menuStream.collect(Collectors.toMap(Dish::getCalories));如有重复数据,可以设置使用哪一个数据 Map<Long, Dish> dishesMap = menuStream.collect(Collectors.toMap(Dish::getCalories, d -> d, (d1, d2) -> d1, LinkedHashMap::new)); |
toCollection | Collection<T> | 把流中所有项目收集到给定的供应源创建的集合 | Collection<Dish> dishes = menuStream.collect(Collectors.toCollection(), ArrayList::new); |
counting | Long | 计算流中元素的个数 | long howManyDishes = menuStream.collect(Collectors.counting()); |
summingInt | Integer | 对流中项目的一个整数属性求和 | int totalCalories = menuStream.collect(Collectors.summingInt(Dish::getCalories)); |
averagingInt | Integer | 计算流中项目 Integer 属性的平均值 | int avgCalories = menuStream.collect(Collectors.averagingInt(Dish::getCalories)); |
summarizingInt | IntSummaryStatistics | 收集关于流中项目 Integer 属性的统计值,例如最大、最小、总和与平均值 | IntSummaryStatistics menuStatistics = menuStream.collect(Collectors.summarizingInt(Dish::getCalories)); |
joining | String | 连接对流中每个项目调用 toString 方法所生成的字符串 | String shortMenu = menuStream.map(Dish::getName).collect(Collectors.joining(", ")); |
maxBy | Optional<T> | 一个包裹了流中按照给定比较器选出的最大元素的 Optional ,或如果流为空则为 Optional.empty() | Optional<Dish> fattest = menuStream.collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))); |
minBy | Optional<T> | 一个包裹了流中按照给定比较器选出的最小元素的 Optional ,或如果流为空则为 Optional.empty() | Optional<Dish> lightest = menuStream.collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories))); |
reducing | 归约操作产生的类型 | 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值 | int totalCalories = menuStream.collect(Collectors.reducing(0, Dish::getCalories, Integer::sum)); |
collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果应用转换函数 | int howManyDishes = menuStream.collect(Collectors.collectingAndThen(toList(), List::size)); |
groupingBy | Map<K, List<T>> | 根据项目的一个属性的值对流中的项目分组组,并将属性值分组结果 Map 的键 | Map<Dish.Type,List<Dish>> dishesByType = menuStream.collect(Collectors.groupingBy(Dish::getType)); |
partitioningBy | Map<Boolean,List<T>> | 根据对流中每个项目应用谓词的结果来对项目进行分区 | Map<Boolean,List<Dish>> vegetarianDishes = menuStream.collect(Collectors.partitioningBy(Dish::isVegetarian)); |
Optional介绍
Optional<T> 类( java.util.Optional )是一个容器类,代表一个值存在或不存在。
- isPresent() 将在 Optional 包含值的时候返回 true , 否则返回 false 。
- ifPresent(Consumer<T> block) 会在值存在的时候执行给定的代码块。
- T get() 会在值存在时返回值,否则抛出一个 NoSuchElement 异常。
- T orElse(T other) 会在值存在时返回值,否则返回一个默认值。
线程个数计算方式
如果线程池中线程的数量过多,最终它们会竞争稀缺的处理器和内存资源,浪费大量的时间在上下文切换上。反之,如果线程的数目过少,正如你的应用所面临的情况,处理器的一些核可能就无法充分利用。Brian Goetz建议,线程池大小与处理器的利用率之比可以使用下面的公式进行估算:
N threads = N CPU * U CPU * (1 + W/C)
其中:
- N CPU 是处理器的核的数目,可以通过 Runtime.getRuntime().availableProcessors() 得到
- U CPU 是期望的CPU利用率(该值应该介于0和1之间)
- W/C 是等待时间与计算时间的比率
并行——使用parallelStream还是 CompletableFutures ?
目前为止, 你已经知道对集合进行并行计算有两种方式: 要么将其转化为parallelStream, 利用map这样的操作开展工作,要么枚举出集合中的每一个元素,创建新的线程,在 Completable-Future 内对其进行操作。后者提供了更多的灵活性,你可以调整线程池的大小,而这能帮助你确保整体的计算不会因为线程都在等待I/O而发生阻塞。
我们对使用这些API的建议如下。
如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用 Stream 接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要创建比处理器核数更多的线程) 。
反之,如果你并行的工作单元还涉及等待I/O的操作(包括网络连接等待) ,那么使用CompletableFuture 灵活性更好,你可以像前文讨论的那样,依据等待/计算,或者W/C的比率设定需要使用的线程数。这种情况不使用并行流的另一个原因是,处理流的流水线中如果发生I/O等待, 流的延迟特性会让我们很难判断到底什么时候触发了等待。
配置并行流使用的线程池
并行流内部使用了默认的 ForkJoinPool,它默认的线程数量就是你的处理器数量 , 这个值是由Runtime.getRuntime().available-Processors() 得到的。
但是可以通 过系统属性java.util.concurrent.ForkJoinPool.common.parallelism 来改变线程池大小,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个
并行流指定这个值。一般而言,让 ForkJoinPool 的大小等于处理器数量是个不错的默认值,
除非你有很好的理由,否则我们强烈建议你不要修改它。
测量流性能
我们声称并行求和方法应该比顺序和迭代方法性能好。然而在软件工程上,靠猜绝对不是什么好办法!特别是在优化性能时,你应该始终遵循三个黄金规则:测量,测量,再测量。
- 并行流并不总是比顺序流快。
有些操作本身在并行流上的性能就比顺序流差。特别是 limit 和 findFirst 等依赖于元素顺序的操作,它们在并行流上执行的代价非常大。例如, findAny 会比 findFirst 性能好,因为它不一定要按顺序来执行。你总是可以调用 unordered 方法来把有序流变成无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用limit 可能会比单个有序流(比如数据源是一个 List )更高效。 - 留意装箱。自动装箱和拆箱操作会大大降低性能。Java 8中有原始类型流( IntStream 、LongStream 、 DoubleStream )来避免这种操作,但凡有可能都应该用这些流。
- 考虑流的操作流水线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味着使用并行流时性能好的可能性比较大。
- 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行处理少数几个元素的好处还抵不上并行化造成的额外开销。
- 考虑流背后的数据结构是否易于分解。例如, ArrayList 的拆分效率比 LinkedList 高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用 range 工厂方法创建的原始类型流也可以快速分解。
可分解性总结了一些流数据源适不适于并行:
源 | 可分解性 |
---|---|
ArrayList | 极佳 |
LinkedList | 差 |
IntStream.range | 极佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |