【Java SE】二十二、Java 8 新特性

Java 8 是 oracle 公司于 2014 年 3 月发布,可以看成是自 Java 5 以来最具革命性的版本。Java 8 为 Java 语言、编译器、类库、开发工具与JVM带来了大量新特性,是目前最常用的 Java 版本。

函数式接口

只包含一个抽象方法的接口,称为函数式接口。所有以前用匿名实现类表示的现在都可以用 Lambda 表达式来写。

你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。我们可以在一个接口,上使用 @Functionallnterface 注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明, 说明这个接口是一个函数式接口。在 java.util.function 包下定义了 Java 8的丰富的函数式接口。

Java 内置四大核心函数式接口,如下表:

函数式接口 参数类型 返回类型 用途
Consumer<T> T void 消费型接口,对类型为 T 的对象应用操作,包含方法:
void accept(T t)
Supplier<T> null 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)

其他接口:

函数式接口 参数类型 返回类型 用途
BiFunction<T, U, R> T, U R 对类型为 T, U 参数应用操作,返回 R 类型的结果。包含方法:
R apply(T t, U u)
UnaryOperator<T> T T 对类型 T 的对象进行一元运算,并返回 T 类型的结果。包含方法为:
T apply(T t)
BinaryOperator<T> T, T T 对类型 T 的对象进行二元运算,并返回 T 类型的结果。包含方法为:
T apply(Tt1, T t2)
BiConsumer<T, U> T, U void 对类型为T, U参数应用操作。包含方法:
void accept(T t, U u)
BiPredicate<T, U> T, U boolean 略。包含方法为:
void accept(Tt, U u)
TolntFunction<T>
ToLongFunction<T>
ToDoubleFunction<T>
T int
long
double
分别计算 int、long、double 值的函数
IntFunction<R>
LongFunction<R>
DoubleFunction<R>
int
long
double
R 参数分别为 int、long、double 类型的函数

## Lambda 表达式

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递) ,使用它可以写出更简洁、更灵活的代码

示例如下:

//匿名方法
Comparator<Integer> com1 = new Comparator<Integer>() { // Comparator其实就是一个函数式接口
    @Override
    public int compare(Integer o1, Integer o2) {
    	return Integer.compare(o1,o2);
    }
};
System.out.println(com1.compare(12,21));

//Lambda表达式
Comparator<Integer> com2 = (o1,o2) -> Integer.compare(o1,o2);
System.out.println(com2.compare(32,21));

//方法引用
Comparator<Integer> com3 = Integer :: compare;
System.out.println(com3.compare(32,21));

从上面的实例,我们可以看到 Lambda 表达式如下:

(o1,o2) -> Integer.compare(o1,o2)
  • 中间: Lambda 操作符或箭头操作符
  • 左边: Lambda 形参列表 (其实就是按口中的抽象方法的形参列表)
  • 右边: Lambda 体 (其实就是重写的抽象方法的方法体)

总共分 6 种语法格式,如下:

//无参,有返回值
Runnabler1 = () -> {System.out.println("Hello Lambda!");};
//一个参数,但是没有返回值。
Consumer<String> con = (String str) -> {System.out.println(str);};
//数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
Consumer<String> con = (str) -> {System.out.printn(str);};
//若只需一个参数,则参数的小括号可以省略
Consumer<String> con = str -> {System.out.printn(str);};
//需要两个或以上的参数,多条执行语句,并且可以有返回值
Comparator<Integer> com = (x,y) -> {
    System.out.printn("实现函数式接口方法! ");
    return Integer.compare(x,y);
};
//只有一条语句,若有return与大括号,则都可以省略
Comparator<Integer> com = (x,y) -> Integer.compare(x,y); // {return Integer.compare(x,y)}

注:Lambda 表达式的本质是作为函数式接口的实例

方法引用和构造器引用

方法引用可以看做是 Lambda 表达式深层次的表达。换句话说,方法引用就是 Lambda 表达式,也是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是 Lambda 表达式的一个语法糖。

当要传递给 Lambda 体的操作,已经有实现的方法了,可以使用方法引用。

注意:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致 (仅针对于前两种情况)

//下面的例子皆省略调用方法
public void test1() {
    //情况一: 对象::实例方法
    //Consumer中的void accept(T t) & PrintStream中的void println(T t)
    Consumer<string> con1 = str -> System.out.println(str); //lambda表达式
    System.out.print1n("****************************");
    PrintStream ps = System.out;
    Consumer<String> con2 = ps::println; //方法引用
    
    //情况二: 类::静态方法
    //Function中的R apply(T t) & Math中的Long round(Double d)
    Function<Double,Long> func1 = d -> Math.round(d);
	System.out.printn("****************************");
	Function<Double,Long> func2 = Math::round;
    
	//情况三:类::实例方法(难点)
    //Comparator中的int comapre(T t1,T t2) & String中的int t1.compareTo(t2)
	Comparator<string> com1 = (s1,s2) -> s1.compareTo(s2);
	System.out.printn("****************************");
    Comparator<string> com2 = String::compareTo;
	//BiPredicate中的boolean test(T t1, T t2) & String中的boolean t1.equals(t2)
	BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
	System.out.printn("****************************");
	BiPredicate<String,string> pre2 = String::equals;
	//Function中的apply(T t) & 自定义类(Person)中的String getName();
	Function<Person,String> func1= e -> e.getName();
    System.out.printn("****************************");
    Function<Employee,String> func2 = Employee::getName;
}

构造器实质上是没有返回值的方法,所以引用相似,如下

//构造器引用需要类有相应参数的构造器
public void test2() {
    //Supplier中的T get()
    Supplier<Employee> sup1 = () -> new Employee();
    System.out.printn("****************************");
    Supplier<Employee> sup2 = Employee::new; //构造器引用

	//Function中的R apply(T t)
	Function<Integer, Person> func1 = id -> new Person(id);
	System.out.println("****************************");
	Function<Integer, Person> func2 = Employee::new;
	
    //BiFunction中的R apply(T t, U u)
	BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
    System.out.println("****************************");
	BiFunction<Integer,String,Employee> func2 = Employee::new;

    //Function中的R apply(T t),可以把数组看作一个特殊的类
	Function<Integer ,String[]> func1 = length -> new String[length]; //这是个lambda表达式应用于数组的例子
	String[] arr1 = func1.apply(5); //创建一个长度为5的数组
	System.out.println("****************************");
    Function<Integer,String[]> func2 = String[]::new; //数组引用
}

Stream API

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

注意:

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

Stream 的实例化

//1.通过集合
List<String> str = new ArrayList<>();
// default Stream<E> stream(): 返回一个顺序流
Stream<String> stream = str.stream();
// default Stream<E> parallelStream() :返回- -个并行流
Stream<String> parallelStream = str.parallelStream();

//2.通过数组
int[] arr = new int[]{1,2,3,4,5,6};
//调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
IntStream stream = Arrays.stream(arr); // 还有long和double类型的Stream
Stream<String> stream1 = Arrays.stream(new String[10]); // 也可以用泛型

//3.通过Stream的of()
Stream<Integer> stream = Stream.of(1,2,3,4,5);

//4.创造无限流: Stream.iterate()和Stream.generate(),不加限制则会一直运行下去
//迭代: public static<T> Stream<T> iterate(final T seed, final Unaryoperator<T> f)
Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println); //遍历前10个偶数
//生成: public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10).forEach(System.out::println);

注:其实有点像 Python 里的生成器

Stream 的中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值“。

筛选与切片

主要有四个方法,接下来我将会使用一个存有 Employee 类的集合来举例,如下:

List<Employee> list = EmployeeData.getEmployees(); //获取集合

//1.filter(Predicate p) - 过滤,接收 Lambda,从流中排除某些元素
Stream<Employee> stream = list.stream(); //获取stream实例
stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println); //筛出工资大于7000的员工

//2.limit(n) - 载断流,便其元素不超过给定数
list.stream().limit(3).forEach(System.out::println); 

//3.skip(n) -跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)互补
list.stream().skip(3).forEach(System.out::println);

//4.distinct() - 筛选,通过流所生成元素的hashCode()和equals()去除重复元素
list.stream().distinct().forEach(System.out::println);

注:Stream 的执行流程是单向不可逆的。想要从头开始,需在执行终止操作(迭代)后,重新实例化

映射

有 2 个主要方法,共 5 个,如下:

//1.map(Function f) - 接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);

//2.flatMap(Function f) - 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
//比较复杂,不演示

其余方法如下:

方法 说明
mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
mapTolnt(TolntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream
mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream

排序

只有 1 个重载方法,如下:

//sorted() - 自然排序 | sorted(Comparator com) - 定制排序
List<Integer> list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7);
list.stream().sorted().forEach(System.out::println);
List<String> list2 = Arrays.asList("abc", "ddjc", "pa", "cca");
list2.stream().sorted((x,y) -> String.compare(x,y)).forEach(System.out::println);

Stream 的终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,如: List,Integer, 还是 void。流进行了终止操作后,不能再次使用。

匹配与查找

我们依然以上面的 Employee 类为例:

List<Employee> employees = EmployeeData.getEmployees();

//allMatch(Predicate p) - 检查是否匹配所有元素
boolean allMatch = employees.stream().allMatch(e -> e.getAge() > 18); //是否所有的员工的年龄都大于18
System.out.println(allMatch);

//anyMatch(Predicate p) - 检查是否至少匹配一个元素,同上不演示
//noneMatch(Predicate p) - 检查是否没有匹配的元素,同上不演示

//findFirst - 返回第一个元素
Optional<Employee> employee = employees.stream().findFirst();
System.out.println(emp1oyee);

//findAny - 返回当前流中的任意元素,返回值依然是Optional,只不过要用,用法同上不演示
Optional<Employee> employee1 = employees.parallelStream().findAny();
System.out.println(employee1);

//count - 返回流中元素的总个数,返回值是long,用法简单不演示

//max(Comparator c) - 返回流中最大值
Stream<Double> salaryStream = employees.stream().map(e -> e.getSalary());
Optional<Double> maxSalary = salaryStream.max(Double::compare); //返回最高工资
System.out.print1n(maxsalary);

//min(Comparator c) - 返回流中最小值,同上不演示

//forEach(Consumer c) - 内部迭代,之前演示过了

注:集合也有一个 forEach 方法,两个方法的对象不同

归约

//reduce(T identity, BinaryOperator) - 可以将流中元素反复结合起来, 得到一个值。返回T
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum = list.stream().reduce(0, Integer::sum); //计算1-1日的自然数的和
System.out.println(sum);

//reduce(BinaryOperator) - 可以将流中元素反复结合起来, 得到一个值。返回Optional<T>
List<Employee> employees = EmployeeData.getEmployees();
Stream<Double> salaryStream = employees.stream().map(Employee::getSalary);
Optional<Double> sumMoney = salaryStream.reduce(Double::sum); //求公司所有员工工资的总和,(d1,d2) -> d1 + d2也行
System.out.println(sumMoney);

收集

Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。另外,Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例。

//collect(Collector c) - 将流转换为其他形式,接收-个Collector接口的实现,用于给Stream中元素做汇总的方法
//查找工资大于6e的员工,结果返回为一个List或Set
List<Employee> employees = EmployeeData.getEmployees();
List<Employee> collect = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.tolist());
employeeList.forEach(System.out::println); //这是集合的迭代方法

Collector 接口中的方法如下表:

方法 返回类型 作用
toList() List<T> 把流中元素收集到 List
toSet() Set<T> 把流中元素收集到 Set
toCollection(ArrayList::new) Collection<T> 把流中元素收集到创建的集合
counting() Long 计算流中元素的个数
summingInt(Employee::getSalary) Integer 对流中元素的整数属性求和
averagingInt(Employee::getSalary) Double 计算流中元素 Integer 属性的平均值
summarizingInt(Employee::getSalary) IntSummaryStatistics 收集流中 Integer 属性的统计值,如:平均值
joining() String 连接流中每个字符串
maxBy(comparingInt(Employee::getSalary)) Optional<T> 根据比较器选择最大值
minBy(comparingInt(Employee::getSalary)) Optional<T> 根据比较器选择最小值
reducing(0, Employee::getSalary,Integer::sum) 归约产生的类型 从一个作为累加器的初始值开始利用BinaryOperator 与流中元素逐个结合,从而归约成单个值
collectingAndThen(Collectors.toList(),List::size) 转换函数近回的类型 包裹另一个收集器,对其结果转换函数
groupingBy(Employee::getStatus) Map<K,List<T>> 根据某属性值对流分组,属性为 K,结果为 V
partitioningBy(Employee::getManage) Map<Boolean,List<T>> 根据 true 或 false 进行分区

Optional 类

Optional<T>类是一个容器类(类似于包装类),它可以保存类型 T 的值,代表这个值存在。或者仅保存 null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。Optional 类的 Javadoc 描述如下:这是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回 true,调用 get() 方法 会返回该对象。

Optional 提供很多有用的方法,这样我们就不用显式进行空值检测。

创建 Optional 类对象的方法:

  • Optional.of(T t):创建一个 Optional 实例,T 必须非空;

  • Optional.empty():创建一个空的 Optional 实例

  • Optional.ofNullable(T t):t 可以为 null

判断 Optional 容器中是否包含对象:

  • boolean isPresent():判断是否包含对象

  • void ifPresent(Consumer<? super T> consumer):如果有值,就执行 Consumer 接口的实现代码,并且该值会作为参数传给它。

获取 Optional 容器的对象:

  • T get():如果调用对象包含值,返回该值,否则抛异常

  • T orElse(T other):如果有值则将其返回,否则返回指定的 other 对象

  • T orElseGet(Supplier<? extends T> other):如果有值则将其返回,否则返回由Supplier接口实现提供的对象

  • T orElse Throw(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常。

示例如下:

// 构建一个空的Optional对象
Optional<String> empty = Optional.empty();
System.out.println(empty); // Optional.empty

// 构建一个非空的Optional对象
Optional<String> hello = Optional.of("Hello");
System.out.println(hello); // Optional[Hello]

// 构建一个可能为空的Optional对象
String name = null;
Optional<String> optionalName = Optional.ofNullable(name);
System.out.println(optionalName); // Optional.empty

// 获取Optional对象里的值
String value = hello.get();
System.out.println(value); // Hello

// 如果为空,返回默认值
String defaultValue = optionalName.orElse("World");
System.out.println(defaultValue); // World

// 如果为空,返回函数结果
String supplierValue = optionalName.orElseGet(() -> "Java");
System.out.println(supplierValue); // Java

// 如果为空,抛出异常
try {
   String exceptionValue = optionalName.orElseThrow(() -> new RuntimeException("No value"));
   System.out.println(exceptionValue);
} catch (RuntimeException e) {
   e.printStackTrace(); 
   /*
   java.lang.RuntimeException: No value
     at com.example.OptionalDemo.lambda$main$0(OptionalDemo.java:30)
     at java.base/java.util.Optional.orElseThrow(Optional.java:408)
     at com.example.OptionalDemo.main(OptionalDemo.java:30)
   */
}