Java8特性学习

Java8相关知识点学习

  • Lambda 表达式
  • 方法引用
  • 默认方法
  • 函数接口
  • Function
  • Stream
  • Optional API
  • Date Time API

Lambda 表达式

Lambda 表达式也可称为闭包。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)

语法

1
2
3
(parameters) -> expression  

(parameters) ->{ statements; }

特性

  • 类型声明(可选):可以不需要声明参数类型,编译器会识别参数值。
  • 参数圆括号(可选):在单个参数时可以不使用括号,多个参数时必须使用。
  • 大括号和return关键字(可选):如果只有一个表达式,则可以省略大括号和return关键字,编译器会自动的返回值;相对的,在使用大括号的情况下,则必须指明返回值。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//第一种,传统匿名Compartor接口排序
Collections.sort(peopleList, new Comparator<People>() {
@Override
public int compare(People o1, People o2) {
return o1.getAge().compareTo(o2.getAge());
}
});

//第二种,使用Lambda表达式来代替匿名接口方法
//1.声明式,不使用大括号,只可以写单条语句
Collections.sort(peopleList,(People a,People b)->a.getAge().compareTo(b.getAge()));

//2.不声明式,使用大括号,可以写多条语句
Collections.sort(peopleList,(a,b)->{
System.out.print("——————————————");
return a.getAge().compareTo(b.getAge());
});

//第三种,使用Lambda表达式调用类的静态方法
Collections.sort(peopleList,(a,b)->People.sortByName(a,b));

//第四种,使用Lambda表达式调用类的实例方法
Collections.sort(peopleList,(a,b)->new People().sortByAge(a,b));

方法引用

在一些Lambda中可能只是单纯的调用方法这种,情况下就可以使用方法引用的方式来提高可读性。

种类

  • 类静态方法引用

    1
    Class::staticMethodName
  • 某个对象的方法引用

    1
    instance::instanceMethodName
  • 特定类的任意对象的方法引用:

    1
    Class::method
  • 构造方法引用:

    1
    Class::new

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void testMethodReference() {
//第一种,引用类的静态方法
Collections.sort(peopleList, People::sortByName);
System.out.println("引用类的静态方法:" + peopleList);

//第二种,引用类的实例方法
Collections.sort(peopleList, new People()::sortByAge);
System.out.println("引用类的实例方法:" + peopleList);

//第三种,特定类的方法调用()
Integer[] a = new Integer[]{3, 1, 2, 4, 6, 5};
Arrays.sort(a, Integer::compare);
System.out.println("特定类的方法引用:" + Arrays.toString(a));

//第四种,引用类的构造器
Car car = Car.create(Car::new);
System.out.println("引用类的构造器:" + car);
}
1
2
3
public static Car create(Supplier<Car> supplier){
return supplier.get();
}

默认方法

在Java8之前的时代,为已存在接口增加一个通用的实现是十分困难的,接口一旦发布之后就等于定型,如果这时在接口内增加一个方法,那么就会破坏所有实现接口的对象

默认方法(defalut)

1
2
3
4
5
public interface vehicle {
default void print(){
System.out.println("我是默认方法");
}
}

静态方法(static)

1
2
3
4
5
public interface vehicle {
static void test() {
System.out.println("这是是测试方法!!!");
}
}

注:静态方法与默认方法均可以有多个,默认方法可以被覆盖。

函数接口

“函数接口(functional interface)”,就是除去默认方法以及继承的抽象方法,只有显式声明一个抽象方法的接口。它使用@FunctionalInterface注解在类上进行标注,也可以省略,Java会自动识别。

常用函数接口

java.util.function.Predicate

该接口包含方法boolean test(T t),一般用于条件的检测,内部包含三个默认方法:and、or、negate、,即与或非,用于各式的条件判断。例:

1
2
3
4
Predicate<Integer> predicate = x -> x > 3;
predicate.test(10);//true
predicate.negate().test(10);//false
predicate.or(x -> x < 1).and(x -> x > -1).negate().test(-1);//true

注意:在这里与或非的判断顺序是从左到右的,调用的顺序会影响结果。

java.util.Comparator

Comparator是Java中的经典接口,在排序中较为常用。Java8在此之上添加了一些新的默认方法,来丰富该接口的功能。例:

Integer[] a = new Integer[]{3, 1, 2, 4, 6, 5};
Comparator comparator = Integer::compare;

Arrays.sort(a, comparator);
System.out.println(“升序:” + Arrays.toString(a));

Arrays.sort(a,comparator.reversed());
System.out.println(“降序:”+Arrays.toString(a));
结果

升序:[1, 2, 3, 4, 5, 6]
降序:[6, 5, 4, 3, 2, 1]

java.util.function.Supplier

该类只包含方法:
T get();
Supplier接口是在1.8中新出现的函数接口,用于支持函数式编程。它用于返回一个任意泛型的实例对象,与工厂的功能类似。

java.util.function.Consumer

该接口表示一个接受单个输入参数并且没有返回值的操作。不像其他函数式接口,Consumer接口期望执行修改内容的操作。例如 ,我们需要一个批量修改People的方法,利用Predicate和Consumer就可以这么写
在People内增加updateMany方法:

1
2
3
4
5
6
7
8
public static List updateMany(List<People> peopleList, Predicate<People> predicate, Consumer<People> consumer) {
for (int i = 0; i < peopleList.size(); i++) {
if (predicate.test(peopleList.get(i))) {
consumer.accept(peopleList.get(i));
}
}
return peopleList;
}

调用:

1
2
3
4
5
//批量修改,将age<18的对象的age改为18
People.updateMany(peopleList,
p -> p.getAge() < 18,
p -> p.setAge(18));
System.out.println("修改后的结果:" + peopleList);

通过这种方式,可以将内部的判断逻辑与修改代码放至外部调用,而将for、if等语句封装至内部,提高代码的可读性。

其他的还有一些函数接口,如Runnable,InvocationHandler等,在这里就不阐述了。有兴趣的大家可以自行查询资料。Stream、Function、Optional也是函数接口,将在下面进行详细介绍。

Stream

Java8中提供了Stream API,即流式处理。可以通过将List、Set、Array等对象转换成流进行操作。Stream内的流操作分为两种:中间操作和最终操作,中间操作会返回一个全新的Stream对象,意味着你的操作不会影响最初的流;最终操作会将流进行转换或者操作,返回非Stream的对象。

中间操作

  • distinct
    java Stream distinct();
    去除Stream中重复的对象,并返回一个流。(使用对象的equals方法)

  • skip
    java Stream skip(long n);
    跳过Stream中的前n个对象,将其他对象返回一个Stream。如果n超过了Stream中对象的个数,则会返回一个空的Stream。

  • limit
    java Stream limit(long maxSize);
    截取Stream的前maxSize个对象,并形成一个新Stream。

  • filter
    java Stream filter(Predicate<? super T> predicate);
    根据给定的predicate来过滤对象,返回满足条件的对象构成的Stream。

  • map
    Stream map(Function<? super T, ? extends R> mapper);
    通过给定的mapper,将T类型的流转换为R类型的Stream。

  • flatMap
    java Stream flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
    flatMap也是将Stream进行转换,flatMap与map的区别在于 flatMap是将一个Stream中的每个值都转成一个个Stream,然后再将这些流扁平化成为一个Stream。
    例(转自:Java8 新特性之流式数据处理):
    假设我们有一个字符串数组String[] strs = {“java8”, “is”, “easy”, “to”, “use”};,我们希望输出构成这一数组的所有非重复字符,那么我们可能首先会想到如下实现:
    java List<String[]> distinctStrs = Arrays.stream(strs) .map(str -> str.split(“”)) // 映射成为Stream<String[]> .distinct() .collect(Collectors.toList());
    在执行map操作以后,我们得到是一个包含多个字符串(构成一个字符串的字符数组)的流,此时执行distinct操作是基于在这些字符串数组之间的对比,所以达不到我们希望的目的,此时的输出为:
    [j, a, v, a, 8] [i, s] [e, a, s, y] [t, o] [u, s, e]
    distinct只有对于一个包含多个字符的流进行操作才能达到我们的目的,即对Stream进行操作。此时flatMap就可以达到我们的目的:
    java List distinctStrs = Arrays.stream(strs) .map(str -> str.split(“”)) // 映射成为Stream<String[]> .flatMap(Arrays::stream) // 扁平化为Stream .distinct() .collect(Collectors.toList());
    flatMap将由map映射得到的Stream<String[]>,转换成由各个字符串数组映射成的流Stream,再将这些小的流扁平化成为一个由所有字符串构成的大流Steam,从而能够达到我们的目的。

  • sorted
    java Stream sorted(); Stream sorted(Comparator<? super T> comparator);
    sorted方法可以对Stream进行排序。排序的对象必须实现Comparable,如果没实现会抛出ClassCastException;不提供comparator时,则会调用compareTo方法。

  • peek
    java Stream peek(Consumer<? super T> action);
    对流中的每个对象执行提供的action操作。
    在Stack中,peek用于查看一个对象。在流中也是一样,用于在流循环时,根据给定的action进行查看对象。虽然可以进行元素修改操作,但不建议。

  • 综合例:
    java Integer[] a = new Integer[]{3, 1, 2, 5, 11, 4, 6, 5, 3, 1}; List aList = Arrays.stream(a) .distinct() .skip(1) .filter((e) -> e < 6) .peek(e -> System.out.println(“循环1次”)) .limit(4) .sorted() .collect(Collectors.toList()); System.out.println(aList);
    输出:
    循环1次 循环1次 循环1次 循环1次 [1, 2, 4, 5]

最终操作

聚合

  • max & min
    Optional min(Comparator<? super T> comparator);
    Optional max(Comparator<? super T> comparator);
    根据给定的comparator返回Stream中的max或min。

  • count
    long count();
    返回Stream中对象的个数。

匹配

  • anyMatch & allMatch & noneMatch
    boolean anyMatch(Predicate<? super T> predicate);
    boolean allMatch(Predicate<? super T> predicate);
    boolean noneMatch(Predicate<? super T> predicate);
    根据给定的predicate判断Stream是否匹配条件。
  • collect
    <R, A> R collect(Collector<? super T, A, R> collector);
    根据给定的collector对Stream中的元素进行操作,返回复杂数据结构的对象。用于将Stream中的对象转换成我们想要的结构,如list、map、set等。
    前例中就使用collect(Collectors.toList())将Stream中的对象转换成List。
  • reduce
    Optional reduce(BinaryOperator accumulator);
    T reduce(T identity, BinaryOperator accumulator);
    如果我们不知希望单纯的返回List这样的类型,而是希望将整个Stream经过一些操作后,规约成一个对象返回,就可以用到规约操作。reduce方法有两个参数,其中accumulator代表着规约的操作,即用何种的方式进行参数化处理;identity则是accumulator的标识值(具体用处暂不明)。
    例:求和
    java Integer[] a = new Integer[]{3, 1, 2, 5, 11, 4, 6, 5, 3, 1}; int sum = Arrays.stream(a) .distinct() .filter((e) -> e < 6) .reduce(0, (x, y) -> x + y);//或.reduce(0, Integer::sum); System.out.println(sum);//15
  • toArray
    Object[] toArray();
    将Stream中的对象返回成一个Object数组。
  • forEach
    void forEach(Consumer<? super T> action);
    顾名思义,对Stream中每个元素进行action操作,与peek类似,但forEach是一个最终操作,一般在结束时查看对象使用。
  • findFirst & findAny
    Optional findFirst();
    Optional findAny();
    findFirst可以返回Stream中第一个对象,并将它封装在Optional中。
    findAny则不是返回第一个对象,而是任意一个对象。在顺序Stream中findFirst和findAny的结果是一致的,但在并行Stream中,findFirst存在着限制,故在并行Stream中需要使用findAny(findAny源码注释中写的是some element?)。同样将对象封装在Optional中。

Function

Java8提供的java.util.function包的核心函数接口有4个:

  • 函数型T ->R,完成参数类型T向结果类型R的转换和数据处理。核心函数接口Function
  • 判断型T ->boolean,核心函数接口Predicate
  • 消费型T ->void,核心函数接口Consumer
  • 供给型void->T,核心函数接口Supplier

主要方法

name type description
Consumer Consumer< T > 接收T对象,不返回值
Predicate Predicate< T > 接收T对象并返回boolean
Function Function< T, R > 接收T对象,返回R对象
Supplier Supplier< T > 提供T对象(例如工厂),不接收值
UnaryOperator UnaryOperator< T > 接收T对象,返回T对象
BiConsumer BiConsumer<T, U> 接收T对象和U对象,不返回值
BiPredicate BiPredicate<T, U> 接收T对象和U对象,返回boolean
BiFunction BiFunction<T, U, R> 接收T对象和U对象,返回R对象
BinaryOperator BinaryOperator< T > 接收两个T对象,返回T对象

Consumer

  • 作用
    消费某个对象

  • 使用场景
    Iterable接口的forEach方法需要传入Consumer,大部分集合类都实现了该接口,用于返回Iterator对象进行迭代。

  • 设计思想
    开发者调用ArrayList.forEach时,一般希望自定义遍历的消费逻辑,比如:输出日志或者运算处理等。
    处理逻辑留给使用者,使用灵活多变。
    多变的逻辑能够封装成一个类(实现Consumer接口),将逻辑提取出来。

Predicate

  • 作用
    判断对象是否符合某个条件

  • 使用场景
    ArrayList的removeIf(Predicate):删除符合条件的元素
    如果条件硬编码在ArrayList中,它将提供无数的实现,但是如果让调用者传入条件,这样ArrayList就可以从复杂和无法猜测的业务中解放出来。

  • 设计思想
    提取条件,让条件从处理逻辑脱离出来,解耦合

Function

  • 作用
    实现一个”一元函数“,即传入一个值经过函数的计算返回另一个值。
  • 使用场景
    V HashMap.computeIfAbsent(K , Function<K, V>) // 简化代码,如果指定的键尚未与值关联或与null关联,使用函数返回值替换。
    Stream map(Function<? super T, ? extends R> mapper); // 转换流
  • 设计思想
    一元函数的思想,将转换逻辑提取出来,解耦合

Supplier

  • 作用
    创建一个对象(工厂类)
  • 使用场景
    Optional.orElseGet(Supplier<? extends T>):当this对象为null,就通过传入supplier创建一个T返回。
  • 设计思想
    封装工厂创建对象的逻辑

UnaryOperator

  • 作用
    UnaryOperator继承了Function,与Function作用相同
    不过UnaryOperator,限定了传入类型和返回类型必需相同
  • 使用场景
    List.replaceAll(UnaryOperator) // 该列表的所有元素替换为运算结算元素
    Stream.iterate(T,UnaryOperator) // 重复对seed调用UnaryOperator来生成元素
  • 设计思想
    一元函数的思想,将同类转换逻辑提取出来,解耦合

参考:

Optional API

有点类似RxJava中的条件判断数据转换语句

  • of
    为非null的值创建一个Optional。
    of方法通过工厂方法创建Optional类。需要注意的是,创建对象时传入的参数不能为null。如果传入参数为null,则抛出NullPointerException 。

    1
    2
    3
    4
    //正常返回Optional对象
    Optional<String> name = Optional.of("TEST");
    //传入参数为null,抛出NullPointerException
    Optional<String> name = Optional.of(null);
  • ofNullable
    为指定的值创建一个Optional,如果指定的值为null,则返回一个空的Optional。

    1
    Optional<String> name = Optional.ofNullable(null);
  • isPresent
    如果值存在返回true,否则返回false。

  • get
    如果Optional有值则将其返回,否则抛出NoSuchElementException。

  • ifPresent
    如果Optional实例有值则为其调用consumer,否则不做处理

  • orElse
    如果有值则将其返回,否则返回指定的其它值。

  • orElseGet
    orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的字符串作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值

  • orElseThrow
    如果有值则将其返回,否则抛出supplier接口创建的异常。
    在orElseGet方法中,我们传入一个Supplier接口。然而,在orElseThrow中我们可以传入一个lambda表达式或方法,如果值不存在来抛出异常。

  • map
    如果有值,则对其执行调用mapping函数得到返回值。如果返回值不为null,则创建包含mapping返回值的Optional作为map方法返回值,否则返回空Optional。
    map方法用来对Optional实例的值执行一系列操作。通过一组实现了Function接口的lambda表达式传入操作。

  • flatMap
    如果有值,为其执行mapping函数返回Optional类型返回值,否则返回空Optional。
    flatMap方法与map方法类似,区别在于mapping函数的返回值不同。map方法的mapping函数返回值可以是任何类型T,而flatMap方法的mapping函数必须是Optional。

  • filter
    如果有值并且满足断言条件返回包含该值的Optional,否则返回空Optional。

Date Time API

JAVA8引入了java.time包,一个新的日期时间API,处理了以下缺点

  • 线程安全 - java.util.Date不是线程安全的,因此开发者必须在使用日期处理并发性问题。新的日期时间API是不可变的,并且没有setter方法。
  • 设计问题 - 默认的开始日期为1900年,月的开始月份为0而不是1,没有统一。不直接使用方法操作日期。新的API提供了这样操作实用方法。
  • 时区处理困难 - 开发人员必须编写大量的代码来处理时区的问题。新的API设计开发为这些特定领域提供了帮助。