本文主要介绍了Java 8引入的新特性,结合代码示例,讲解了这些特性存在的意义、使用方法和注意事项。阅读本文,将使你的编码能力更上一层楼。
一、引入
Java每三年会有一个"长期支持版本"(Long Term Support release,简称LTS ),该版本会提供为期三年的支持,目前LTS版本有JDK8 、JDK11 和JDK17。Oracle JDK在2020年1月份开始对Java SE 8(8u201/202)之后的版本开始进行商用收费,虽然Java已经发布到Java 18*,但是国内Java编程主流还是使用Java8。今天,我们就一起来看看Java8的新特性。
编程语言都追求更简洁、更清晰、更高效,Java 8新特性也体现着这一点,主要有:
- Lambda 表达式:允许把函数作为一个方法的参数来传递;
- 函数式接口:一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,这种接口可以作为 Lambda 表达式使用;
- 方法引用:直接引用已有Java类或对象的方法或构造器。
- 默认方法:在接口中提供方法的一个实现;
- Stream API:从支持数据处理操作的源生成的元素序列;
- Optional 类:为"不存在的值"建模,用于取代null;
- Date Time API:对java.util.Date和Calendar不足的改进,本文将不做介绍。
本文参考了由陆明刚、劳佳翻译的国外著作《Java 8 in Action》
二、Lambda表达式
函数和方法有区别:函数(尤其是静态方法)不能访问共享的可变数据,被称为"纯函数"或"无副作用函数"或"无状态函数"。
没有共享的可变数据,具备将方法和函数即代码传递给其他方法的能力,是函数式编程范式的基石。
lambda表达式,就是通过行为参数化传递代码,本质是极简的匿名内部类 (可推理即可省略 )。
java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Example {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("lia");
list.add("lae");
list.add("mie");
list.add("xie");
sortList(list);
// 匿名内部类
list.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
// lambda
list.sort((s1, s2) -> s1.compareTo(s2));
list.sort((s1, s2) -> {
return s1.compareTo(s2);
});
// 方法引用
list.sort(String::compareTo); // 升序 [lae, lia, mie, xie]
// 接口static方法
list.sort(Comparator.comparing(s -> s.charAt(0)));
}
}
在Java8之前,我用使用匿名内部类来传递行为。有了lambda,就不需要为只用一次的方法声明定义。
- 可选的参数类型声明:lambda中不需要指定参数类型,因为编译器能够根据上下文进行推断;
- 可选的参数圆括号:只有一个参数时,无需定义圆括号;多个参数时,需要使用圆括号;
- 可选的大括号:当主体只包含了一行语句时,无需使用大括号
- 可选的return:当主体只包含了一行语句时,无需使用return,返回值的类型由编译器推理得出;
- 变量作用域:Lambda表达式可以引用类成员变量和局部变量,但是局部变量必须显式声明为final,或事实上是final的。 注意:要是Lambda表达式的长度多于几行,变得不够一目了然的话,此时应该声明一个有描述性名称的方法,将比使用匿名的Lambda更有表达力。
三、函数式接口
函数接口指的是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。这种接口可以隐式转换为Lambda表达式。
Java 8提供了一个注解@FunctionalInterface
。当你用@FunctionalInterface标注了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
Java 8自带一些常用的函数式接口,在java.util.function包中。
四、方法引用------Lambda的语法糖
语法:目标放在分隔符::前,方法名称放在后面,不需要括号,因为并没有实际调用这个方法。
有3种形式的方法引用:
- 类成员方法的引用
java
Arrays.asList(1, 2, 3, 4).forEach(System.out::println);
- 构造函数引用:使用构造函数名称和关键字new,即ClassName::new
java
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Apple {
// 重量
private int weight;
private String color;
public Apple() {
}
public Apple(int weight) {
this.weight = weight;
}
public Apple(int weight, String color) {
this.weight = weight;
this.color = color;
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public static List<Apple> filterHeavyApples(List<Apple> inventory, Predicate<? super Apple> predicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (predicate.test(apple)) {
result.add(apple);
}
}
return result;
}
}
css
List<Apple> apples = Stream.of(100, 120, 160, 180).map(Apple::new).collect(Collectors.toList());
- 类静态方法的引用
ini
// 筛选重量符合的苹果
List<Apple> heavyApples = filterHeavyApples(apples, Apple::isHeavyApple);
五、Stream流
流,就是从支持数据处理操作的源生成的元素序列 。集合在乎存储、访问元素 ,关注数据;而流的目的在于计算,如过滤、查找、排序等。
5.1流操作的特点
Java 8中提供了java.util.stream库,支持创建流和操作流。流操作有几个重要特点:
- 流水线:很多流操作本身会返回一个流,这样可以将很多操作链接起来,形成一个大的流水线;
- 内部迭代:集合的迭代器是显式遍历,而流的迭代操作在背后进行。
- 延迟和短路:中间操作会返回另一个流 ,除非流水线上触发一个终结操作,否则中间操作不会执行任何处理。
java
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// 流操作的延迟性
public class Example1 {
public static void main(String[] args) {
// 执行后不会有任何输出
Stream<String> limit = Stream.of(1, 2, 3, 4, 5, 6)
.filter(num -> {
System.out.println("filtering " + num);
return num > 3;
})
.map(num -> {
System.out.println("mapping " + num);
return num.toString();
})
.limit(2);
// 有终结操作才会触发中间操作
List<String> list = limit.collect(Collectors.toList());
}
}
5.2创建流
流的来源可以是集合,数组,I/O channel,产生器generator 等。对于集合,有两个方法来生成流:
- stream():为集合创建串行流
- parallelStream():为集合创建并行流
java
// Stream.of
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
// 数组创建流
String[] num = {"a", "b", "c", "d", "e"};
Stream<String> stream = Arrays.stream(num);
// 元素创建流
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
// 集合创建流
List<String> wordList = new ArrayList<>();
// 串行流
Stream<String> stream = wordList.stream();
// 并行流
Stream<String> parallelStream = wordList.parallelStream();
// 创建数值流
Stream<Integer> intStream = IntStream.rangeClosed(2, 100).boxed();
// generate生成流
List<Double> list = Stream.generate(Math::random).limit(5).collect(Collectors.toList());
5.3操作流
流的操作包括筛选、切片、映射、查找、匹配和归约等。
筛选
- filter():获取满足条件的元素
- distinct():对流中元素去重,根据equals()
- limit(n) :返回一个只包含前n个元素的流,可将无限流变成有限流
- skip(n):返回一个扔掉了前n个元素的流;如果流中元素不足n个,则返回一个空流
映射
- map():会被应用到每个元素上,并将其映射成一个新的元素(不改变原元素)
- flatMap:将多个流中内容合并成一个流
查找:allMatch、anyMatch、noneMatch、findFirst和findAny,都是终结操作;
归约:T reduce(T identity, BinaryOperator accumulator);如sum、max、min、avg、count等,最终返回一个结果。
java
// 筛选出大于3的元素
Stream<Integer> filter = Stream.of(1, 2, 3, 3, 4, 4, 5, 6).filter(num -> num > 3);
// 去重,根据Integer.equals()
Stream<Integer> distinct = Stream.of(1, 2, 3, 3, 4, 4, 5, 6).distinct();
// 返回前3个
Stream<Integer> limit = Stream.of(1, 2, 3, 3, 4, 4, 5, 6).limit(3);
// 跳过前3个
Stream<Integer> skip = Stream.of(1, 2, 3, 3, 4, 4, 5, 6).skip(3);
// 将元素取平方
Stream<Integer> map = Stream.of(1, 2, 3, 4, 5, 6).map(num -> num * num);
// 获取第一个大于3的元素
Optional<Integer> findFirst = Stream.of(1, 2, 3, 3, 4, 4, 5, 6).filter(num -> num > 3).findFirst();
// 求和归约
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Integer sum = numbers.stream().reduce(0, Math::addExact);
Integer sum1 = numbers.stream().reduce(0, Integer::sum);
Optional<Integer> sum2 = numbers.stream().reduce(Integer::sum);
5.4流操作的状态
- 无状态:如map或filter等操作,从输入流中获取一个元素,不依赖于其他就能得到0或1个结果,这些操作没有内部状态;
- 有状态:但诸如reduce、sum、max等操作,每处理一个元素,都需要内部状态来累积结果。再比如sort操作,,要求所有元素都放入缓冲区后才能进行排序,存储空间要求是无界的。
5.5数值流------原始类型流
Java 8中提供了IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本,性能更好。
java
// 闭区间[2, 100]
IntStream evenNumbers = IntStream.rangeClosed(2, 100);
// 区间[2, 100)
IntStream evenNumbers1 = IntStream.range(2, 100)
List<Apple> appleList = Arrays.asList(new Apple(125, "green"),
new Apple(125, "green"),
new Apple(120, "green"),
new Apple(150, "red"),
new Apple(133, "red"),
new Apple(147, "red"));
// mapToInt创建数值流,流中元素是int
IntStream intStream = appleList.stream().mapToInt(Apple::getWeight);
// sum时无需拆箱
int sum = intStream.sum();
// sum时需要拆箱
Integer sum = appleList.stream().map(Apple::getWeight).reduce(0, Integer::sum);
// 将数值流转为一般流
Stream<Integer> boxed = intStream.boxed();
5.6并行流
在Java 7之前,并行处理数据集合非常麻烦。
- 第一,你要将数据集合分成若干子部分;
- 第二,你要给每个子部分分配一个独立的线程;
- 第三,你需要在恰当的时候对它们进行同步来避免出现竞争条件,等待所有线程都完成,最后把各部分结果合并起来。
现在,Java 8中通过对集合调用parallelStream(),就能创建一个并行流:把内容分成多个数据块,并用不同的线程分别处理每个数据块。
但是,顺序流调用parallel(),它在内部先是设置了一个boolean标志;对并行流调用sequential()就可以又把它变成顺序流。
并行流内部使用了默认的ForkJoinPool(分支/合并框架)
5.6.1并行流的性能
使用并行流就如下面的代码,分别使用for循环、顺序流、并行流计算1到10000000的和。
java
import java.util.function.Function;
import java.util.stream.LongStream;
import java.util.stream.Stream;
public class SumPerformanceTest {
public static void main(String[] args) {
// 求1到10000000的和
System.out.println("iterativeSum sum done in:" +
testSumPerformance(SumPerformanceTest::iterativeSum, 10000000) + " ms")
System.out.println("Sequential sum done in:" +
testSumPerformance(SumPerformanceTest::sequentialSum, 10000000) + " ms");
System.out.println("parallelSum sum done in:" +
testSumPerformance(SumPerformanceTest::parallelSum, 10000000) + " ms");
System.out.println("parallelSumNew sum done in:" +
testSumPerformance(SumPerformanceTest::parallelSum, 10000000) + " ms");
}
// for循环累加
public static long iterativeSum(long n) {
long result = 0;
for (long i = 1L; i <= n; i++) {
result += i;
}
return result;
}
// 顺序求1~n的和
public static long sequentialSum(long n) {
return Stream.iterate(1L, i -> i + 1).limit(n).reduce(0L, Long::sum);
}
// 并行求1~n的和
public static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);
}
// 并行求1~n的和
public static int parallelSumNew(long n) {
return IntStream.rangeClosed(0, (int) n).parallel().sum();
}
// 计算5次,取最小耗时
public static long testSumPerformance(Function<Long, Long> add, long n) {
long time = Long.MAX_VALUE;
for (int i = 0; i < 5; i++) {
long start = System.nanoTime();
long sum = add.apply(n);
long duration = (System.nanoTime() - start) / 1000000; //毫秒
if (time > duration) {
time = duration;
}
}
return time;
}
}
可以看到,求和的并行版本比顺序版本要慢很多,有这些原因:
- iterate生成装箱对象,求和时必须拆箱成int;
- iterate很难分割成多个独立执行的小块,因此每次执行都要依赖前一次执行的结果;
- 即使使用IntStream数值流时,比使用iterate的更快了,但是还是不及顺序流、for循环速度快。
这个测试提醒我们,并行化并不是没有代价的:对流做递归划分,把每个子流的操作分配到不同线程,在多个内核之间移动数据,最终将各个子流的结果合并。
5.6.2错误使用并行流
java
// 累加器
public class Accumulator {
private volatile long total = 0;
public void add(long i) {
total += i;
}
public long getTotal() {
return total;
}
}
java
import java.util.stream.LongStream;
public class ErrorParallel {
public static void main(String[] args) {
long sum = LongStream.rangeClosed(1, 10000000).sum();
System.out.println("正确结果:" + sum);
for (int i = 0; i < 5; i++) {
System.out.println("并行结果:" + parallelSum(10000000));
}
}
// 并行时结果有误
public static long parallelSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
return accumulator.getTotal();
}
}
示例中并行流计算结果错误的首要原因,就是改变了Accumulator中共享变量的状态(即使用volatile修饰),导致并发安全问题。为此,我们需注意一些并行流的使用准则:
- 首先,使用并行流时,结果正确永远是第一位的;
- 其次,在具体场景下,用适当的基准来检查使用并行流的性能;如果相比于顺序流并没有较大的性能提升,说明该场景并不适合使用并行流计算。
- 第三,自动装箱和拆箱操作会大大降低性能,可使用原始类型流IntStream等;
- 第四,对于较小的数据量,并行处理的好处还抵不上并行化造成的额外开销;
- 第五,limit、findFirst等依赖于元素顺序的操作,在并行流上的性能比顺序流要差;
- 最后,使用并行流前,要考虑流背后的数据结构是否易于拆分;例如,ArrayList的拆分效率比LinkedList高得多。
5.7分支/合并框架
分支/合并框架的目的,是以递归方式将可以并行的任务拆分成更小的子任务,然后将每个子任务的结果合并起来生成整体结果。
那么,如何创建任务和子任务呢?任务ForkJoinTask是Future的实现类,它有两个抽象子类,有结果返回的RecursiveTask<R>
和无结果返回的的RecursiveAction
。我们只需继承这两个类,实现compute()方法,定义将任务拆分成子任务的逻辑,以及无法再拆分时处理单个子任务的逻辑。
scss
// 伪代码
if (任务足够小或不可分) {
顺序计算该任务
} else {
将任务拆分成两个子任务
递归调用complate()方法,拆分每个子任务
等待所有子任务完成
合并每个子任务的结果
}
ForkJoinPool是用来处理任务的线程池,是ExecutorService接口的一个实现,默认最大线程数等于机器的处理器数量(也允许自定义)。
不同之处在于,ForkJoinPool内部采用了工作窃取(work stealing)算法 :每个线程都把分配给它的任务保存到一个双向链式队列中,每完成一个任务,就会从队列头上取出下一个任务开始执行;某些线程的队列为空时,会随机选一个别的线程的队列,从队尾偷走一个任务。
工作窃取算法用于在池中的工作线程之间重新分配和平衡任务,尽可能使所有线程都保持相近的负荷。
示例------数字求和
我们来使用ForkJoin框架计算1到1000000的和,并与顺序计算方式进行比较。
java
import java.util.concurrent.RecursiveTask;
public class ForkJoinSumCalculator extends RecursiveTask<Long> {
/**
* 拆分后的一段数据
*/
private final long[] array;
private final int start;
private final int end;
/**
* 任务拆分阈值
*/
private static final int THRESHOLD = 1000;
public ForkJoinSumCalculator(long[] array) {
this(array, 0, array.length - 1);
}
public ForkJoinSumCalculator(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = start - end + 1;
// 任务足够小时
if (length < THRESHOLD) {
return computeSequentially(array, start, end);
}
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(array, start, start + length / 2);
// 先fork左半部分任务
leftTask.fork();
ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(array, start + length / 2 + 1, end);
Long rightResult = rightTask.compute();
// 在rightTask提交计算后调用,阻塞等待左半部分计算完成
Long leftResult = leftTask.join();
// 合并结果
return leftResult + rightResult;
}
// 顺序求和
private Long computeSequentially(long[] array, int start, int end) {
long sum = 0;
for (int i = start; i <= end; i++) {
sum += array[i];
}
return sum;
}
}
java
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class ForkJoinTest {
/**
* 定义成静态成员,单例即可
*/
private static final ForkJoinPool FORK_JOIN_POOL = new ForkJoinPool();
public static void main(String[] args) {
iterativeSum(10000000);
long[] array = LongStream.rangeClosed(1, 10000000).toArray();
forkJoinSum(array);
}
// 顺序求和
public static void iterativeSum(long n) {
long start = System.nanoTime();
long sum = 0;
for (long i = 1L; i <= n; i++) {
sum += i;
}
long duration = (System.nanoTime() - start) / 1000000; //毫秒
System.out.println("sum: " + sum + ", 耗时: " + duration);
}
// ForkJoin框架求和
public static void forkJoinSum(long[] array) {
long start = System.nanoTime();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(array);
Long sum = FORK_JOIN_POOL.invoke(task);
long duration = (System.nanoTime() - start) / 1000000; //毫秒
System.out.println("sum: " + sum + ", 耗时: " + duration);
}
}
运行结果如下,ForkJoin框架耗时反而比顺序计算更久,因为任务拆分、结果合并会有额外损耗。可见,对于简单的计算任务,并行处理未必比串行有性能优势。
注意:一个任务调用join方法后会阻塞调用方。因此需要在两个子任务的计算都开始后再调用它。
六、接口默认方法
Java中接口是将相关方法按照约定组合到一起的方式。Java 8允许在接口内声明静态方法、默认方法。这种机制可以用于平滑地进行接口的优化和演进。 如果一个类从多个地方(如另一个类或接口)继承了同一签名的方法,那么使用该方法时,各版本的优先级为:
- 本类中的方法优先级最高,高于父类实现或接口中的默认方法
- 子接口中默认方法优先级高于父接口的;
- 如果还是无法判断,就必须通过显式复写和显式调用该方法。
java
public interface A {
default void hello() {
System.out.println("Hello from A");
}
}
public interface B {
default void hello() {
System.out.println("Hello from B");
}
}
// 从A、 B接口继承了同一签名的default方法
public class C implements A, B {
public static void main(String[] args) {
new C().hello();
}
//不重写会报错
@Override
public void hello() {
// 显式调用,将输出:Hello from B
B.super.hello();
System.out.println("Hello from C");
}
}
七、用Optional取代null
null代表了"不存在的值",虽然使用简单便捷,但是有这些问题
- 它是NullPointerException的错误之源;
- 它会使代码膨胀,不得不做null检查;
- 它会提示你Java中指针的存在;
- 它不属于任何类型,可以赋值给任意引用类型,你将无法获知null变量最初的类型。 我们来看个例子:一个人Person可能有汽车Car,汽车可能买了保险Insurance。
java
import java.util.Optional;
public class Person {
private Car car;
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
// 有了Optional后
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
java
import java.util.Optional;
public class Car {
private Insurance insurance;
public Insurance getInsurance() {
return insurance;
}
public void setInsurance(Insurance insurance) {
this.insurance = insurance;
}
// 有了Optional后
public Optional<Insurance> getInsuranceAsOptional() {
return Optional.ofNullable(insurance);
}
}
java
// 保险
public class Insurance {
private String name;
public Insurance(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
当我们想要获取一个人的某辆车的保险名时,可能会有下面这些写法。
java
// 忘记做null检查
public String getCarInsuranceName1(Person person) {
return person.getCar().getInsurance().getName();
}
// 深层质疑
public String getCarInsuranceName2(Person person) {
if (person != null) {
Car car = person.getCar();
if (car != null) {
Insurance insurance = car.getInsurance();
if (insurance != null) {
return insurance.getName();
}
}
}
return "Unknown";
}
// 过多的退出语句
public String getCarInsuranceName3(Person person) {
if (person == null) {
return "Unknown";
}
Car car = person.getCar();
if (car == null) {
return "Unknown";
}
Insurance insurance = car.getInsurance();
if (insurance == null) {
return "Unknown";
}
return insurance.getName();
}
如果使用Java 8的Optional,你将发现代码变得如此简洁。
java
public String getCarInsuranceName4(Person person) {
return person.getCarAsOptional()
.flatMap(Car::getInsuranceAsOptional)
.map(Insurance::getName)
.orElse("Unknown");
}
使用Optional对可能不存在的值建模时,将迫使你注意值缺失问题,最终避免代码中不期而至的空指针异常。
因此,当一个方法可能返回null时,不如将返回值类型定义为Optional,对调用方也是个极好的提示。
可使用静态方法Optional.empty、Optional.of、Optional.ofNullable来创建Optional对象。Optional类中提供了很多方法。感兴趣的小伙伴可以直接阅读源码。