Java8学习笔记

Java8是2014年发布的,至今也已经有快三年的时间了,之前虽然有学习过,但是学的比较零散,不成系统,而且也没有覆盖到Java8所有的特性。 由于公司已经使用了JDK1.8,所以工作中能使用Java8的机会还是很多的,因此决定来系统地学习一下Java8的新特性,这是对我最近学习Java8的一些记录, 以备在有些细节记不太清的时候可以查询。

Java8

先来一个概览,上图是我整理的Java8中的新特性,总的来看,大致上可以分成这么几个大块。

函数式接口

所谓的函数式接口就是只有一个抽象方法的接口,注意这里说的是抽象方法,因为Java8中加入了默认方法的特性,但是函数式接口是不关心接口中有没有默认方法的。 一般函数式接口可以使用@FunctionalInterface注解的形式来标注表示这是一个函数式接口,该注解标注与否对函数式接口没有实际的影响, 不过一般还是推荐使用该注解,就像使用@Override注解一样。JDK1.8中提供了一些函数式接口如下:

函数式接口 函数描述符 原始类型特化
Predicate<T> T -> boolean IntPredicate, LongPredicate, DoublePredicate
Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Function<T,R> T -> R IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>
Supplier<T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator<T> T -> T IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator<T> (T,T) -> T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate<L,R> (L,R) -> boolean
BiConsumer<T,U> (T,U) -> void ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U) -> R ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>

上表中的原始类型特化指的是为了消除自动装箱和拆箱的性能开销,JDK1.8提供的针对基本类型的函数式接口。

Lambda表达式和方法引用

有了函数式接口之后,就可以使用Lambda表达式和方法引用了。其实函数式接口的表中的函数描述符就是Lambda表达式,在函数式接口中Lambda表达式相当于匿名内部类的效果。 举个简单的例子:

public class TestLambda {

    public static void execute(Runnable runnable) {
        runnable.run();
    }

    public static void main(String[] args) {
        //Java8之前
        execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("run");
            }
        });

        //使用Lambda表达式
        execute(() -> System.out.println("run"));
    }
}

可以看到,相比于使用匿名内部类的方式,Lambda表达式可以使用更少的代码但是有更清晰的表述。注意,Lambda表达式也不是完全等价于匿名内部类的, 两者的不同点在于this的指向和本地变量的屏蔽上。

Lambda表达式还可以复合,把几个Lambda表达式串起来使用:

Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150).or(a -> “green”.equals(a.getColor()));

上面这行代码把两个Lambda表达式串了起来,含义是选择重量大于150或者绿色的苹果。

方法引用可以看作Lambda表达式的更简洁的一种表达形式,使用::操作符,方法引用主要有三类:

  1. 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt);
  2. 指向任意类型实例方法的方法引用(例如String的length方法,写作String::length);
  3. 指向现有对象的实例方法的方法引用(例如假设你有一个本地变量localVariable用于存放Variable类型的对象,它支持实例方法getValue,那么可以写成localVariable::getValue)。

举个方法引用的简单的例子:

Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);

//使用方法引用
Function<String, Integer> stringToInteger = Integer::parseInt;

方法引用中还有一种特殊的形式,构造函数引用,假设一个类有一个默认的构造函数,那么使用方法引用的形式为:

Supplier<SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.get();

//等价于

Supplier<SomeClass> c1 = () -> new SomeClass();
SomeClass s1 = c1.get();

如果是构造函数有一个参数的情况:

Function<Integer, SomeClass> c1 = SomeClass::new;
SomeClass s1 = c1.apply(100);

//等价于

Function<Integer, SomeClass> c1 = i -> new SomeClass(i);
SomeClass s1 = c1.apply(100);

Stream

Stream可以分成串行流和并行流,并行流是基于Java7中提供的ForkJoinPool来进行任务的调度,达到并行的处理的目的。 集合是我们平时在进行Java编程时非常常用的API,使用Stream可以帮助更好的来操作集合。Stream提供了非常丰富的操作,包括筛选、切片、映射、查找、匹配、归约等等, 这些操作又可以分为中间操作和终端操作,中间操作会返回一个流,因此我们可以使用多个中间操作来作链式的调用,当使用了终端操作之后,那么这个流就被认为是被消费了, 每个流只能有一个终端操作。

//筛选后收集到一个List中
List<Apple> vegetarianMenu = apples.stream().filter(Apple::isRed).collect(Collectors.toList());

//筛选加去重
List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);

以上都是一些简单的例子,Stream提供的API非常丰富,可以很好的满足我们的需求。

操作 类型 返回类型 使用的类型/函数式接口 函数描述符
filter 中间 Stream<T> Predicate<T> T -> boolean
distinct 中间 Stream<T>
skip 中间 Stream<T> long
limit 中间 Stream<T> long
map 中间 Stream<R> Function<T,R> T -> R
flatMap 中间 Stream<R> Function<T, Stream<R>> T -> Stream<R>
sorted 中间 Stream<R> 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

与函数式接口类似,Stream也提供了原始类型特化的流,比如说IntStream等:

//maoToInt转化为一个IntStream
int count = list.stream().mapToInt(list::getNumber).sum();

并行流与串行流的区别就在于将stream改成parallelStream,并行流会将流的操作拆分,放到线程池中去执行,但是并不是说使用并行流的性能一定好于串行的流, 恰恰相反,可能大多数时候使用串行流会有更好的性能,这是因为将任务提交到线程池,执行完之后再合并,这些本身都是有不小的开销的。关于并行流其实还有非常多的细节, 这里做一个抛砖引玉,有兴趣的同学可以在网上自行查找一些资料来学习。

默认方法

默认方法出现的原因是为了对原有接口的扩展,有了默认方法之后就不怕因改动原有的接口而对已经使用这些接口的程序造成的代码不兼容的影响。 在Java8中也对一些接口增加了一些默认方法,比如Map接口等等。一般来说,使用默认方法的场景有两个:可选方法和行为的多继承。

默认方法的使用相对来说比较简单,唯一要注意的点是如何处理默认方法的冲突。关于如何处理默认方法的冲突可以参考以下三条规则:

  1. 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
  2. 如果无法依据第一条规则进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口。即如果B继承了A,那么B就比A更具体。
  3. 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。那么如何显式地指定呢:
public class C implements B, A {

    public void hello() {
        B