Java进阶_09_Java8新特性


本文介绍Java8新特性。

1. 概述

Java8是最大的一个版本改动。具有以下新特性:

  1. 速度更快
  2. 代码更少(增加了新的语法Lambda表达式)
  3. 强大的Stream API
  4. 便于并行(fork join)
  5. 最大化减少空指针异常(Optional容器类)

其中最为核心的为Lambda表达式与StreamAPI。除此之外,还有以下几种新特性。

2. Lambda表达式

2.1 概述

Lambda是一个匿名函数,可以把Lambda表达式理解为是一段可以传递的代码(将代码和数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

匿名函数,即函数没有名字。和接口的匿名实现类类似,没有类名,直接实现接口,并创建对象。

以前的Comparator的匿名实现类,需要实现该接口,实现compare方法。但是通过Lambda表达式,可直接将compar方法的实现代码作为匿名实现类。案例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 传统方式
public void test1(){

// 这就是创建了一个比较器对象。可用于其他函数的参数,传入。
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};

// 将比较器对象作为参数传入
TreeSet<Integer> ts = new TreeSet<>(com);
}

// lambda方式
public void test2(){

// 直接提取核心代码,以lambda方式创建比较器对象。
Comparator<Integer> com = (o1, o2) -> Integer.compare(o1, o2);
TreeSet<Integer> ts = new TreeSet<>(com);
}

通过lambda表达式,可以很简单地匿名实现接口类。这样的话,对于一个接口,如果实现类的具体实现方法都不一样,显然就需要创建多个类,即使这个类就使用一次,也要实现这个类,并且实现方法。

显然这是不合理的,因此可匿名实现接口。但是传统的匿名实现接口,比较繁琐,那些接口名、返回值类型等等,都是比较麻烦的,因此简化之后,就是lambda表达式了。

以下是自己的错误思维:

其实,如果究其本质,lambda其实就是一个函数,并不是什么接口,上面只是从接口的例子来引出lambda的方便。

这个函数是匿名的,只需要将函数需要的参数,和函数体写出来即可。

本质上,Lambda其实就是匿名接口实现类的简化版。但是这个接口必须只有一个抽象方法,这样,才能默认就是实现的这个抽象方法。只有一个抽象方法的接口称为函数式接口。

针对函数体内,匿名内部类的一些特点,比如无法修改函数体内局部变量的值,其实对于Lambda表达式也是同样适用的。

2.2 基础语法

Lambda表达式的操作符为:”->”,这个符号称为箭头操作符或者Lambda操作符。该操作符将Lambda表达式分成了两部分:

  • 左侧:lambda表达式的参数列表,即Lambda参数列表。即函数式接口中抽象方法的形参列表。
  • 右侧:lambda表达式所需要执行的功能,即Lambda体。即函数式接口中抽象方法的实现体。

既然是抽象方法的简化版,显然抽象方法可以有返回值,也可没有返回值,返回值的类型也是多样的。另外,抽象方法的形参列表,可以没有参数,也可以有多个,并且参数的数据类型也是多样的。

那么对应于Lambda表达式,该有哪些具体的类型呢?

这里需要说明一点,对于返回值类型,以及参数列表的类型,其实无需自己规定,Lambda底层会通过类型推断自己识别出来。

语法格式有以下几种:

  1. 无参无返回值:

    这里不再自己写接口,线程的接口就是Runnable接口,而且该接口中只有一个抽象方法,显然Runnable接口是函数式接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 传统方式
    Runnable r1 = new Runnable() {
    @Override
    public void run() {
    System.out.println("Hello World");
    }
    };

    // Lambda表达式
    Runnable r2 = () -> System.out.println("Hello World!");

    因此语法格式为:() -> Lambda体

  2. 有一个参数,无返回值:

    语法格式为:(参数) -> Lambda体

    如果只有一个参数,左侧小括号可以不写。

    1
    2
    3
    4
    5
    6
    // 有参,只有一个参数,左侧小括号可以不写。
    Consumer<String> consumer1 = (s) -> System.out.println(s);
    consumer1.accept("hianian");

    Consumer<String> consumer2 = s -> System.out.println(s);
    consumer2.accept("hianian");
  3. 有多个参数,有返回值,并且有多条语句:

    1
    2
    3
    4
    5
    6
    Comparator<Integer> com = (x, y) -> {
    System.out.println("函数式接口");
    return Integer.compare(x, y);
    };

    int result = com.compare(1, 2);

    语法和上面的类似,只不过返回值需要return关键字返回,多条语句则需要用大括号代码括起来。语法:(参数列表) -> {Lambda体}

  4. 有多个参数,有返回值,只有一条语句:

    这种形式,return关键字和大括号都可以省略不写。

    这里有个疑问,如果省略return关键字,那么和没有返回值有什么区别呢?它怎么知道是否有无返回值呢?

    我认为,本质上还是要回归到该接口中原始的方法上,似乎因为是函数式接口,可以唯一定位到那个抽象方法,也就可以知道是否有返回值。

  5. Lambda的参数列表的数据类型可以省略不写,疑问JVM编译器通过上下文推断出数据类型,称为“类型推断”。实际上仍然是函数式接口,可以找到唯一的抽象方法,找到其参数列表类型。

    其他的类型推断有:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 类型推断成功,
    String[] strs = {"asdf", "qwer"};

    // 类型推断失败,因为无法知道推断出strs的类型。
    Strign strs;
    strs = {"qwer"};

    // 类型推断成功,钻石表达式
    // 后面的尖括号中无需写泛型。
    List<String> list = new ArrayList<>();

可以看到,Lambda表达式就是匿名实现类的简化版本,缺点就是代码不能复用。优点就是对于代码量少的实现类,无需新建一个类来显示实现该接口。因此Lambda一般情况下用于代码量少的情况,比如一行代码或者两行代码,或者这段代码出现的概率较低,无需代码复用。

3. 函数式接口

3.1 概述

若接口中只有一个抽象方法,那么这个接口就是函数式接口。可以使用注解@FunctionalInterface修饰,该注解可以检查该接口是否是函数式接口。

注意,这里的接口中的方法指的是自定义的方法,不是继承自Object类的(比如equals),也不是default修饰的。换句话说,继承自Object类中的方法不是抽象方法,default修饰的也不是抽象方法。

注意,接口实际上就是一种特殊的类,因此也是继承自Object。

3.2 四大常用函数式接口

其实上面的例子中可以看出,如果采用Lambda表达式,那么就必须新建一个函数式接口来支持Lambda表达式,显然这对开发者来说是不友好的。因此Java内置了四大常用函数式接口。

函数式接口 参数类型 返回类型 用途
Consumer<T> T void 消费型接口,对类型为T的对象应用操作,包含方法void accept(T t)
Supplier<T> T 供给型接口,返回类型为T的对象,包含方法T get()
Function<T, R> T R 函数型接口,对类型为T的对象应用操作,并返回结果。结果是R类型的对象,包含方法:R apply(T t)
Predicate<T> T boolean 断定型接口,确定类型为T的对象是否满足某约束,并返回boolean值。包含方法boolean test(T t)

这几个常用接口均在java.util.function包下。

  1. Consumer<T>接口,里面的accept方法,没有返回值,有参数T对象,其实就是对T对象参数进行操作,消费这个对象,并不会产出。
  2. Supplier<T>接口,里面的get方法,返回值类型为T,没有参数,其实就是获取T类型的对象。提供这个对象,没有消费支出,但是有产出。
  3. Function<T, R>,里面的apply方法,返回值类型为R,参数为T对象,这个就相当于上面两者的融合。相当于函数,既有输入也有输出、
  4. Predicate<T>,里面的test方法,返回值类型为boolean,参数类型为T,其实就是相当于一个判断函数,对T进行操作,并返回true或者false。相当于对输入的参数条件进行判断。

4. 方法引用与构造器引用

4.1 方法引用

Lambda体中的代码就是普通的Java代码,如果这段代码的功能已经由其他方法实现了,可以直接调用该方法。换句话说,Lambda体就是一行方法调用,显然本质上,直接调用该方法就能完成任务。

这里引用其他的方法称为方法引用。方法引用是Lambda表达式的另一种表现形式。

具体的语法格式主要有以下三种:

  1. 对象::实例方法名

    注意,方法名无需加括号,案例如下所示,两种方式是等价的。注意,这里因为是直接省略了左侧形参和右侧方法调用时实参,所以,函数式接口中的抽象方法和方法引用的方法的形参类型、返回值类型必须要保持一致。

    1
    2
    3
    4
    5
    Consumer<String> con = (x) -> System.out.println(x);

    PrintStream ps = System.out;
    Consumer<String> con2 = ps::println;
    Consumer<String> con3 = System.out::println;

    这其实就相当于将直接将参数传给了方法引用。

  2. 类::静态方法名

    这种方式和上面的类似,案例如下所示:

    1
    2
    3
    4
    5
    Comparator<Integer> com1 = (x, y) -> Integer.compare(x, y);

    // 这里的比较器的具体实现代码就是Integer类中的静态方法,
    // 所以可以直接方法引用。
    Comparator<Integer> com2 = Integer::compare;
  3. 类::实例方法名

    1
    2
    3
    4
    5
    6
    7
    // 情景:传入两个参数,返回一个结果
    // 这里是test(x,y)方法的代码实现,因为只有一行,所以可直接写。右侧是Lambda体。
    BiPredicate<String, String> bp = (x, y) -> x.equals(y);

    // 注意,这里是简化后的,采用:类名::实例方法 来简化
    // 这种简化方式有要求:即实例方法的调用是:第一个参数.实例方法(第二个参数),此时就可以采用如下方式简化。
    BiPredicate<String, String> bp2 = String::equals;

4.2 构造器引用

和方法引用类似,构造器引用的返回值以及参数列表要和函数式接口方法的返回值、参数列表一一对应。

构造器引用的语法如下所示:

  1. 类名::new

    代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Supplier<String> sup = () -> new String();
    // 因为Supplier接口中的方法是无参的,所以调用的就是String中无参构造方法
    Supplier<String> sup2 = String::new;
    System.out.println(sup.get());
    System.out.println(sup2.get());

    Function<String, String> func = (x) -> new String(x);
    // 因为Function接口中的方法是有参的,所以调用的就是String中有参构造方法
    Function<String, String> func2 = String::new;
    System.out.println(func.apply("asdf"));
    System.out.println(func2.apply("qwer"));

    如果是数组的话,则类名后面加中括号即可。

5. Stream API

5.1 Stream概述

Stream是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API对集合数据进行操作,就类似使用SQL执行的数据库查询。也可以使用StreamAPI来执行并行操作。简而言之,Stream API提供了一种高效且易于使用的处理数据的方式。

注意,这里的Stream流和IO流是不一样的。但核心目的仍然是传输数据。

换句话说,这里的Stream流其实就是:

  1. 对某个数据转换成流,【不影响原始数据源】
  2. 以流的形式展开一系列操作,
  3. 对上面的操作结果,产生一个新的流。

流(Stream)是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

集合讲的是数据,而流讲的是计算。

注意

  1. Stream自己不会存储元素。
  2. Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream
  3. Stream操作是延迟执行的,这意味着他们会等到需要结果的时候才执行。

整个Stream流的操作有三个步骤:

  1. 创建Stream流

    一个数据源(如:集合、数组),获取一个流

  2. 中间操作

    一个中间操作链,对数据源的数据进行处理

  3. 终止操作(终端操作)

    一个终止操作,执行中间操作链,并产生结果

5.2 创建Stream

Stream是一个接口,全限定类名为:java.util.stream.Stream

  1. 通过Collection系列集合提供的stream()或parallelStream()获取到Stream流对象

    1
    2
    3
    4
    List<Integer> list = new ArrayList<>();

    Stream<Integer> stream = list.stream();
    Stream<Integer> parallel = list.parallelStream();
  2. 通过Arrays中的静态方法stream方法将数组转换成stream流对象。

    1
    2
    Integer[] array = new Integer[6];
    Stream<Integer> stream1 = Arrays.stream(array);
  3. 通过Stream类中的静态方法of()方法

    1
    Stream<String> aa = Stream.of("aa", "bvv", "qwer");
  4. 创建无限流

    迭代器形式创建无限流,即迭代器可以无限产生元素,本质上是从迭代器中产生流。

    1
    2
    3
    Stream<Integer> iterate = Stream.iterate(0, (x) -> x + 2);
    // 中间操作(限制10个)加终止操作
    iterate.limit(10).forEach(System.out::println);

    生成器形式创建无限流,生成器也可无限产生元素,本质上是从生成器中产生流。

    1
    2
    3
    Stream<Double> generate = Stream.generate(Math::random);
    // 中间操作(限制10个)加终止操作
    generate.limit(10).forEach(System.out::println);

5.3 中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”换句话说,这些中间操作,并不会马上执行,而是在终止操作时一次性全部执行。因此说,如果单纯的中间操作,是不会有任何外观上的结果的。

对于Stream,中间操作有如下几种:

注意,这些操作都是实例方法,并不是静态方法。

5.3.1 筛选与切片

方法 描述
filter(Predicate p) 接收Lambda,从流中排除某些元素【保留filter条件元素】。返回一个新的流。
distinct() 筛选,通过流所生成元素的hashCode()和equals()去除重复元素。返回一个新的流。
limit(long maxSize) 截断流,使其元素不超过给定数量。返回一个新的流。
skip(long n) 跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)是互补的。

案例如下所示:

1
2
3
// 按照某种形式来迭代产生数据
Stream<Integer> iterate = Stream.iterate(0, (x) -> x + 1);
iterate.filter((x) -> x % 5 == 0).limit(10).skip(8).forEach(System.out::println);

上面的filter和limit就是一系列的中间操作,先过滤出整除5的元素,然后取前10个,之后,再跳过这10个中的前8个,最后输出终止操作。

5.3.2 映射

方法 描述
map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的DoubleStream。
mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的IntStream。
mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的LongStream。
flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

感觉map映射就是遍历数组or集合中的每个元素,对其进行相同操作。

Function f,这个方法是返回一个流对象,而flatMap则是将若干个流对象,串联整合成一个流对象。

5.3.3 排序

方法 描述
sorted() 产生一个新流,其中按自然顺序排序(按照字典排序内置的Comparable)
sorted(Comparator comp) 产生一个新流,其中按比较器顺序排序(自定义规则Comparator)

5.4 终止操作

注意,一个流,只要终止操作了,那么就会被关闭,后续不能再被使用该流。

5.4.1 查找与匹配

方法 描述
allMatch 检查是否匹配所有元素
anyMatch 检查是否至少匹配一个元素
noneMatch 检查是否没有匹配所有元素
findFirst 返回第一个元素
findAny 返回当前流中的任意元素
count 返回流中元素的总个数
max 返回流中的最大值
min 返回流中的最小值

上述的匹配方法,返回结果都是布尔值。下面的5个方法返回值类型均为Optional容器类型,可避免空指针异常。

5.4.2 规约

方法 描述
reduce(T identity, BinaryOperator) 将流中元素按照BinaryOperator规则进行规约运算
reduce(BinaryOperator) 将流中元素按照BinaryOperator规则进行规约运算

规约就是对流中所有元素进行计算,比如累加累减等等。identity是起始值,即将这个值开始规约,后面依次是流中的元素,所以结果必不为空。如果没有identity,那么结果就可能为空,所以为Optional类型。

备注:map和reduce操作非常注明,二者的连接通常被称为map-reduce模式,因Google用它来进行网格搜索而出名。

5.4.3 收集

方法 描述
collect(Collector collector) 将流转换为其他方式,接收一个Collector接口实现,用于流汇总
collect(Supplier supplier, BiConsumer consumer) 将流按照指定的收集器方式进行收集

Collectors提供了很多静态方法,返回Collector接口实现类。

1
2
3
4
5
List<Integer> collect = iterate.limit(10).collect(Collectors.toList());
collect.forEach(System.out::println);

// 或者以对象形式指定转换的集合类型
HashSet<Integer> hs = iterate.limit(10).collect(Collectors.toCollection(HashSet::new);

注意,Collectors中提供了很多收集工具方法。除了上面的转换成list的收集器,也可以分组、多级分组、分区、最大值、最小值等等。

5.5 并行流与顺序流

在这里先了解一下Java7中的Fork/Join框架:就是在必要的情况下,将一个大人物,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join汇总。重点在于分割成若干个小任务,然后将这些小任务压入到每个线程的队列中,但是为了避免,某些线程效率较高,执行速度较快,导致很快空闲;而有些线程则效率低,处于繁忙状态,即实时资源分配不均。所以采用“工作窃取模式”,速度较快的线程会从速度较慢的线程中的任务队列中偷取任务,自己执行。

相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上。在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程就会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了线程的等待时间,提高了性能。

并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。Java8中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API可以声明性地通过parallel()与sequential()在并行流与顺序流之间进行切换。换句话说,这里的并行流实际上也就是fork/join的实现。因为fork/join框架手动开发比较困难。

顺序流就是stream,可以直接获取parallelStream(),或者parallel()将顺序流转换成并行流。这样,后续针对该流的操作,底层默认使用多线程了。但是一定是大数量下,如果是小数据量,因为多线程分配任务实际上还会有额外开销的,这里的开销还不足以弥补小数量下多线程的提升效果。

5.6 Optional容器类(了解)

Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在,原来用null表示一个值不存在,现在Optional可以更好的表达这个概念。并且可以避免空指针异常。常用方法如下:

方法 描述
Optional.of(T t) 创建一个Optional实例
Optional.empty() 创建一个空的Optional实例
Optional.ofNullable(T t) 若t不为null,则创建Optional实例,否则创建空实例
isPresent() 判断是否包含值
orElse(T t) 如果调用对象包含之,返回该值,否则返回t
orElseGet(Supplier s) 如果调用对象包含值,返回该值,否则返回s获取的值
map(Function f) 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()
flatMap(Function mapper) 与map类似,要求返回值必须是Optional

本质上说,Optional类就是一个容器,这样,即使对象为空,这样就相当于Optional容器里面是空的,但是还是有这个容器对象的。

6. 接口中的默认方法与静态方法

Java8开始允许接口中有默认实现的方法。即default关键字修饰的,称为默认方法。注意,该方法不是抽象方法。那么此时,如果某个类有和接口重名的方法(默认方法),并且测试类即继承了这个类,也实现了这个接口,此时有如下原则:

  • 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
  • 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么必须子类必须实现(重写)该方法来解决冲突

另外,也允许有静态方法。

7. 新时间日期API

  1. Date
  2. Calendar
  3. TimeZone
  4. SimpleDateFormat(java.text)

上面四个时间相关类都是线程不安全的,而且也都不在一个包下,而且用法非常不友好。Java8新增的时间类如下:

  1. java.time
    1. LocalDate
    2. LocalTime
    3. LocalDateTime
    4. Instant
    5. ZoneOffset
  2. java.time.chrono
  3. java.time.format
    1. DateTimeFormat
  4. java.time.temporal
    1. TemporalAdjuster
    2. TemporalAdjusters
  5. java.time.zone

上面的一套API解决了线程安全问题。

8. 其他新特性

Java8对注解处理提供了两点改进:可重复的注解以及可用于类型的注解。

可重复注解就是可重复添加同名的注解,只不过value值不一样。【注意,注解定义的时候要加repeatable】

类型注解指的是注解修饰的类型,以前只有TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE。Java8新增了TYPE_PARAMETER,表示类型,比如修饰String等。

9. 总结


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录