java8新特性介绍

1-初识Lambda表达式与函数式接口

Java8被称作Java史上变化最大的一个版本。其中包含很多重要的新特性,最核心的就是增加了Lambda表达式和Stream API。这两者也可以结合在一起使用。首先来看下什么是Lambda表达式。

Lambda表达式,维基百科上的解释是一种用于表示匿名函数和闭包的运算符,感觉看到这个解释还是觉得很抽象,接下来我们看一个例子

Java8被称作Java史上变化最大的一个版本。其中包含很多重要的新特性,最核心的就是增加了Lambda表达式和Stream API。这两者也可以结合在一起使用。首先来看下什么是Lambda表达式。
Lambda表达式,维基百科上的解释是一种用于表示匿名函数和闭包的运算符,感觉看到这个解释还是觉得很抽象,接下来我们看一个例子

public class SwingTest {
    public static void main(String[] args) {
        JFrame jFrame = new JFrame("My JFrame");
        JButton jButton = new JButton("My JButton");

        jButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {                
                System.out.println("Button Pressed!");
            } 
        }); 
        
        jFrame.add(jButton); jFrame.pack(); 
        jFrame.setVisible(true); 
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
    }
}

这是一段Swing编程中的代码,给Button绑定一个监听事件,当点击Button时会在控制台输出"Button Pressed!"内容。这里使用了创建了一个匿名内部类的实例来绑定到监听器,这也是以往比较常规的代码组织形式。但是仔细看一下我们会发现,实际上我们真正关注的就是一个ActionEvent类型的参数e和向控制台输出的语句System.out.println("Button Pressed!");。
如果将上段程序中以匿名内部类的方式创建接口实例的代码替换成Lambda表达式后,代码如下
public class SwingTest {

public static void main(String[] args) {
    JFrame jFrame = new JFrame("My JFrame");
    JButton jButton = new JButton("My JButton");

    jButton.addActionListener(e -> System.out.println("Button Pressed!"));

    jFrame.add(jButton);
    jFrame.pack();
    jFrame.setVisible(true);
    jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

}
关注最中间部分代码的变化,由原来的6行代码,现在1行就可以实现了。这就是Lambda表达式的一种简单形式。
可以看出Lambda表达式的语法是
(param1,param2,param3) -> {

//todo

}
这里参数的类型程序可以根据上下文进行推断,但是并不是所有的类型都可以推断出来,此时就需要我们显示的声明参数类型,当只有一个参数时小括号可以省略。当todo部分只有一行代码时,外边的大括号可以省略。如我们上面的示例
那么除了代码简洁了,Lambda表达式还给我们带来了什么变化吗?
我们回忆一下,在Java中,我们是否无法将函数作为参数传递给一个方法,也无法声明返回值是一个函数的方法。在Java8之前,答案是肯定的。
那么,在上面的例子中我们居然可以将一段代码逻辑作为参数传递给了监听器,告诉监听器事件触发时你可以这么做,而不再需要以匿名内部类的方式作为参数。这也是Java8带来的另一新特性:函数式编程。
支持函数式编程的语言有很多,在JavaScript中,把函数作为参数传递,或者返回值是一个函数的情况非常常见,JavaScript是一门非常常见的函数式语言。
Lambda为Java添加了缺失的函数式编程的特性,使我们能将函数当做一等公民看待。
在函数式编程语言中,Lambda表达式的类型是函数。而在Java中,Lambda表达式是对象,它们必须依附于一类特别的对象类型——函数式接口(Functional Interface)
接下来我们看下函数式接口的定义:
如果一个接口中,有且只有一个抽象的方法(Object类中的方法不包括在内),那这个接口就可以被看做是函数式接口。

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

来看下Runnable接口的声明,在Java8后,Runnable接口多了一个FunctionalInterface注解,表示该接口是一个函数式接口。但是如果我们不添加FunctionalInterface注解的话,如果接口中有且只有一个抽象方法时,编译器也会把该接口当做函数式接口看待。

@FunctionalInterface
public interface MyInterface {
    void test();
    String toString();
}

MyInterface这也是一个函数式接口,因为toString()是Object类中的方法,只是在这里进行了复写,不会增加接口中抽象方法的数量。
(到这里额外提一下,Java8中,接口里面的方法不仅仅只能有抽象方法,也可以有具体实现了的方法,被称作默认方法(default method),这部分后面会具体介绍)
既然在Java中,Lambda表达式是对象。那么这个对象的类型是什么呢?我们再回顾下SwingTest程序,这里以匿名内部类的方式创建了一个ActionListener接口实例

jButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {                
        System.out.println("Button Pressed!");
    } 
}); 

使用Lambda表达式改进后

jButton.addActionListener(e -> System.out.println("Button Pressed!"));

也就是我们使用Lambda表达式创建了一个ActionListener接口的实例,再看下ActionListener接口的定义

public interface ActionListener extends EventListener {
    /**
     * Invoked when an action occurs.
     */
    public void actionPerformed(ActionEvent e);
}

只有一个抽象方法,虽然没添加FunctionalInterface注解,但是也符合函数式接口的定义,编译器会认为这是一个函数式接口。
所以,使用Lambda表达式可以创建函数式接口的实例。即Lambda表达式返回的是函数式接口类型。
实际上,函数式接口实例的创建可以有三种方式(参考自FunctionalInterface注解说明):
1.Lambda表达式
2.方法引用(后续章节介绍)
3.构造方法引用(后续章节介绍)

小结:本篇我们打开了学习Java8的大门,认识了什么是lambda表达式,了解了函数式接口的定义是什么,并借住几个例子展示了lambda表达式的便捷之处。

2-默认方法和函数式接口实例其它创建方式

上面我们简单介绍了Java8的Lambda表达式以及函数式接口的概念,接下来我们继续深入Java8函数式编程模型。
public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        list.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });
    }
}

这段程序很简单,首先初始化一个Integer类型的集合然后向控制台输出每个元素。其中我们注意到forEach方法,它就是Java8中新增加的默认方法。

public interface Iterable<T> {
    .
    .省略
    .
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

它被声明在Iterable接口中,并被关键字default修饰。这样任何一个该接口的子类型都可以继承forEach方法的实现,所以List接口因为是Iterable的间接子接口,所以也继承了该默认方法。Java8采用这种巧妙的方式既扩展了接口的功能,又兼容了老版本。

接下来分析下forEach的实现,首先接收了一个Consumer类型的参数action,进行非空判断,然后遍历当前所有元素交由action的accept方法进行处理。那么Consumer又是什么鬼,看源码

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    .
    .省略
    .
}

一个接口,有且仅有一个抽象方法,被@FunctionalInterface修饰,典型的函数式接口。
ok,现在我们知道forEach接收的Consumer类型的参数是一个函数式接口,接口里唯一的accept抽象方法接收一个参数,不返回值。那通过上一篇文章我们知道,创建函数式接口类型的实例其中一种方式是使用Lambda表达式,所以可以将最上面的程序改造一下

public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        //Lambda表达式 接收一个参数 不返回值
        list.forEach(item -> System.out.println(item));
    }
}

该lambda表达式item -> System.out.println(item)接收一个参数 不返回值,符合accept方法定义,编译通过。
也就是说如果使用lambda表达式来创建一个函数式接口实例,那这个lambda表达式的入参和返回必须符合这个函数式接口中唯一的抽象方法的定义。

接下来再对程序进行改造

public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        //方法引用
        list.forEach(System.out::println);
    }
}

看到out后面有两个冒号,反正当时我是凌乱了。。。这个就是函数式接口实例第二种创建方式:方法引用
方法引用的语法是 对象::方法名(只是其中一种)
同样,使用方法引用方式去创建函数式接口实例也必须遵守方法的定义,看下此处println方法源码

public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}

接收一个参数,并不返回值,编译通过。
最后我们来看下创建函数式接口的最后一种,第三种方式:构造方法引用 ,继续改程序

public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        //构造方法引用
        list.forEach(Test1::new);
    }
    
    Test1(Integer i){
        System.out.println(i);
    }
}

构造方法引用的语法是:类名::new
我们给Test1新添加了一个构造方法,该构造方法接收一个参数,不返回值,编译通过。(仅为展示构造方法引用的用法)

结合上一篇文章可以总结一下,创建函数式接口类型的三种方式:
1.lambda表达式
2.方法引用
3.构造方法引用
注意:无论是哪种方式,必须要符合抽象方法的方法定义

小结:本篇我们首先介绍了接口中的默认方法,然后又分别用几段程序代码展示了方法引用和构造方法引用的使用方式,后续还会提供一些示例来加深理解。

3-深入理解函数式编程模型

上面我们总体介绍了创建函数式接口实例的几种方式以及Java8中接口新增的默认方法特性,接下来我们来看下Java8中已经为我们提供的几种典型的函数式接口
先看一个示例
public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest functionTest = new FunctionTest();
        int i2 = functionTest.add2(2);
        int i3 = functionTest.add3(2);
        int i4 = functionTest.add4(2);
    }

    //逻辑提前定义好
    public int add2(int i){
        return i + 2;
    }

    //逻辑提前定义好
    public int add3(int i){
        return i + 3;
    }

    //逻辑提前定义好
    public int add4(int i){
        return i + 4;
    }
}

FunctionTest中定义了三个方法,分别把参数加2,加3,加4然后分别把结果返回,那如果以后还要取到参数的平方或者各种其他的运算,就还需要定义更多的处理方法。接下来看下如果使用Java8提供的Function接口会有哪些改进
首先看下Function接口定义

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    .
    .省略
    .
}

该函数式接口唯一的抽象方法apply接收一个参数,有返回值。看下使用方式

public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest functionTest = new FunctionTest();

        int result2 = functionTest.compute(5, num -> num + 2);
        int result3 = functionTest.compute(5, num -> num + 2);
        int result4 = functionTest.compute(5, num -> num + 2);
        int results = functionTest.compute(5, num -> num * num);

    }

    //调用时传入逻辑
    public int compute(int i, Function<Integer,Integer> function){
        Integer result = function.apply(i);
        return result;
    }
}

我们在FunctionTest中定义了compute方法,方法的第一个参数是要运算的数据,第二个参数是函数式接口Function的实例,当执行compute方法时,会将第一个参数交给第二个参数Function中的apply方法处理,然后返回结果。
这样我们可以将方法定义的更抽象,代码重用性也就越高,每次将要计算的数据和计算逻辑一起作为参数传递给compute方法就可以。是不是有点体验到函数式编程的灵活之处。
注:因为表达式只有一行语句 num -> num + 2 可以省略了return 关键字 如果为了更加直观可以写成 num -> return num + 2。

Supplier接口,默认抽象方法get不接收参数,有返回值

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

类似工厂模式

public class SupplierTest {
    public static void main(String[] args) {
        Supplier<String> supplier = String::new;
        String s = supplier.get();
    }
}

这里使用构造方法引用的方式创建Supplier实例,通过get直接返回String对象

Predicate接口

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    .
    .省略
    .
}

接收一个参数,返回布尔类型,使用方式

public class PredicateTest {
    public static void main(String[] args) {
        Predicate<String> predicate = s -> s.length() > 5;
        System.out.println(predicate.test("hello"));
    }
}

定义了一个接收一个参数返回布尔值的lambda表达式,赋值给predicate,就可以直接对传入参数进行校验

再看一个Predicate例子

public class PredicateTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        PredicateTest predicateTest = new PredicateTest();
        List<Integer> result = predicateTest.conditionFilter(list, integer -> integer > 5);
        result.forEach(System.out::println);
    }

    public List<Integer> conditionFilter(List<Integer> list, Predicate<Integer> predicate){
        return list.stream().filter(predicate).collect(Collectors.toList());
    }
}

这段程序的逻辑是找到集合里大于5的数据,打印到控制台。我们具体分析一下conditionFilter方法,第一个参数是待遍历的集合,第二个参数是Predicate类型的实例,还记得Predicate接口中的抽象方法定义吗,接收一个参数返回布尔类型。list.stream()是创建了一个针对此集合的Stream对象(先简单认识一下),然后调用Stream的filter方法对结果进行过滤,过滤的条件就是Predicate接口的实现,最后collect(Collectors.toList());是将过滤后的结果收集起来放置到一个新的集合中并返回。
其中涉及到Srteam api的知识点我们会在后续章节中详细介绍,现在只关心Predicate的使用就可以。

接下来调用conditionFilter方法

List<Integer> result = predicateTest.conditionFilter(list, integer -> integer > 5);

list是待遍历的集合
integer -> integer > 5 lambda表达式是对Predicate接口的具体实现

result.forEach(System.out::println);

最后,方法引用实例化一个Consumer对象,把结果输出到控制台。

可以看出,lambda表达式和stream api结合起来使用让代码更简洁更加语义化,即使之前没接触过stream api也能大概看出conditionFilter方法的功能。

小结:本篇继续介绍Java8函数式编程模型,介绍了几个内置的函数式接口使用方式,并在最后一个示例引出stream api知识点,后续会对stream api进行详细的介绍。

4-stream api和函数式编程简单应用

之前我们介绍了几个Java8内置的函数式接口的特点和使用方式,并在最后引出了stream api的知识点,接下来我们开始学习Java8中的stream api。
先假设一个简单的需求,存在一个字符串集合,我们想把所有长度大于5的字符串转换成大写输出到控制台,之前我们可能会直接这么做
public class Test3 {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("hello","world","helloworld");

        for (int i = 0; i < list.size(); i++) {
            if(list.get(i).length() > 5){
                System.out.println(list.get(i).toUpperCase());
            }
        }
    }
}

现在换成使用stream api看下

public class Test3 {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("hello","world","helloworld");
        list.stream().filter(s -> s.length() > 5).map(s -> s.toUpperCase()).forEach(System.out::println);
    }
}

1行代码直接搞定,而且这种链式编程风格从语义上看逻辑很清晰。
stream方法先构造了一个该集合的Stream对象,filter方法取出长度大于5的字符串,map方法将所有字符串转大写,forEach输出到控制台。我们可以对照Stream接口的源码片段看下这几个stream api的定义

public interface Stream<T> extends BaseStream<T, Stream<T>> {
    
    Stream<T> filter(Predicate<? super T> predicate);
    
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    
    void forEach(Consumer<? super T> action);
    .
    .省略
    .
}

filter方法,接收一个Predicate函数式接口类型作为参数,并返回一个Stream对象,从上一篇我们知道可以由一个接收一个参数返回布尔类型的lambda表达式来创建Predicate函数式接口实例,所以看到filter接收的参数是s -> s.length() > 5

map方法,接收Function函数式接口类型,接收一个参数,有返回值s -> s.toUpperCase() 正是做了这件事情

forEach方法,接收Consumer函数式接口类型,接收一个参数,不返回值 这里使用方法引用的其中一种形式System.out::println来创建了Consumer实例。

所以通过上面的例子可以看出函数式编程和stream api结合的非常紧密。大家应该也注意到了在介绍每个方法时,我们提到了有中间操作和终止操作,终止操作意味着我们需要一个结果了,当程序遇到终止操作时才会真正执行。中间操作是指在终止操作之前所有的方法,这些方法以方法链的形式组织在一起处理一些列逻辑,如果只有中间操作而没有终止操作的话即使运行程序,代码也不会执行的

实际上map方法中可以使用另一种方法引用的形式来处理,类方法引用。语法:类名::方法名

public class Test3 {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("hello","world","helloworld");
        list.stream().filter(s -> s.length() > 5).map(String::toUpperCase).forEach(System.out::println);
    }
}

之前介绍的System.out::println这种属于对象方法引用,类方法引用的应用也是很广泛的。只是理解起来需要花费些时间,map方法接收一个Function函数式接口的实现,那就肯定需要一个输入并且有一个输出,但是我们看下toUpperCase方法的定义

public String toUpperCase() {
    return toUpperCase(Locale.getDefault());
}

有返回值,但是没有入参,乍一看也不符合Function接口中apply方法的定义啊。这也是类方法引用的特点,虽然toUpperCase没有明确的入参,因为此时toUpperCase的输入是调用它的那个对象,编译器会把调用toUpperCase方法的那个对象当做参数,也就是lambda表达式s -> s.toUpperCase()中的s参数。所以也满足一个输入一个输出的定义

小结:本篇简单介绍了函数式编程与stream api应用及类方法引用的使用,lambda表达式让老版本的代码更简洁,方法引用让lambda表达式更简洁,实际上就是lambda表达式的一种语法糖。

5-Function函数式接口进阶与默认方法详解

以上我们快速的借助示例演示了stream api的简单应用,体会到了使用stream api对集合处理的便捷和其与函数式接口密不可分的关系,所以为了更高效的使用stream api,有必要更熟练的掌握函数式接口。Java8中内置了大量的函数式接口,接下来我们选择一些比较常用的一起学习下。

Function接口
在之前的文章中,我们简单介绍过Function接口中apply方法的应用,除了apply这个抽象方法,Function接口中还内置了两个比较常用的默认方法(接口中增加的有具体实现的方法,扩展了接口功能,子类默认会继承该实现),看下Function接口源码

/**
 * Represents a function that accepts one argument and produces a result.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object)}.
 *
 * @param <T> the type of the input to the function
 * @param <R> the type of the result of the function
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    /**
     * @return a composed function that first applies the {@code before}
     * function and then applies this function
     */
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    /**
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     */
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    /**
     * 省略
     */
}

Function接口定义中有两个泛型,按着接口文档说明第一个泛型是输入类型,第二泛型是结果类型。
compose方法接收一个Function参数before,该方法说明是返回一个组合的函数,首先会应用before,然后应用当前对象,换句话说就是先执行before对象的apply,再执行当前对象的apply,将两个执行逻辑串起来。
andThen方法接收一个Function参数after,与compose方法相反,它是先执行当前对象的apply方法,再执行after对象的方法。看一个应用示例

public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest functionTest = new FunctionTest();
        System.out.println(functionTest.compute1(5,i -> i * 2,i -> i * i));//50
        System.out.println(functionTest.compute2(5,i -> i * 2,i -> i * i));//100
    }

    public int compute1(int i, Function<Integer,Integer> after,Function<Integer,Integer> before){
        return after.compose(before).apply(i);
    }

    public int compute2(int i, Function<Integer,Integer> before,Function<Integer,Integer> after){
        return before.andThen(after).apply(i);
    }
}

定义了compute1和compute2两个方法,compute1方法第一个参数是要计算的数据,第二个参数是后执行的函数,第一个是先执行的函数,因为输入输出都是数字类型,所以泛型都指定为Integer类型,通过after.compose(before);将两个函数串联起来然后执行组合后的Funtion方法apply(i)。当调用compute1(5,i -> i 2,i -> i i)时,先平方再乘以2所以结果是50。而compute2方法对两个Function的调用正好相反,所以结果是100。

BiFunction接口
接下来继续看下另一个很常用的函数式接口BiFunction


/**
 * This is the two-arity specialization of {@link Function}.
 * @param <T> the type of the first argument to the function
 * @param <U> the type of the second argument to the function
 * @param <R> the type of the result of the function
 *
 * @see Function
 * @since 1.8
 */
@FunctionalInterface
public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     */
    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

实际上就是可以有两个参数的Function,同样前两个泛型代表着入参的类型,第三个代表结果类型。

public class BiFunctionTest {
    public static void main(String[] args) {
        BiFunctionTest2 biFunctionTest2 = new BiFunctionTest2();
        System.out.println(biFunctionTest2.compute(4,5,(a,b) -> a * b,a -> a * 2));
    }

    public int compute(int a, int b, BiFunction<Integer,Integer,Integer> biFunction,
                       Function<Integer,Integer> function){
        return biFunction.andThen(function).apply(a,b);
    }
}

看下compute方法,前两个参数是待计算数据,第三个是一个BiFunction,因为入参和结果都是数组所以三个泛型都定义为Integer。最后一个参数是Function。计算逻辑是先执行BiFunction然后将结果传给Funciton在计算最后返回结果,所以使用了andThen方法。我们想一下,BiFunction的andThen方法为什么接收的是Function类型的参数而不是BiFunction,答案很简单,因为BiFunction的apply方法接收两个参数,但是任何一个方法不可能有两个返回值,所以也没办法放在BiFunction前面执行,这也是为什么BiFunction没有compose方法的原因。

6-Predicate接口详解

我们学习了Function和BiFunction函数式接口,本篇继续了解下其他常用的函数式接口。
先来看下Predicate
Predicate函数式接口的主要作用就是提供一个test方法,接受一个参数返回一个布尔类型,Predicate在stream api中进行一些判断的时候非常常用。
@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}

使用泛型T指定传入的参数类型,我们通过一个根据不同条件取出不同数据的例子来看下Predicate具体应用

public class PredicateTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        PredicateTest predicateTest = new PredicateTest();
        //输出大于5的数字
        List<Integer> result = predicateTest.conditionFilter(list, integer -> integer > 5);
        result.forEach(System.out::println);
        System.out.println("-------");
        //输出大于等于5的数字
        result = predicateTest.conditionFilter(list, integer -> integer >= 5);
        result.forEach(System.out::println);
        System.out.println("-------");
        //输出小于8的数字
        result = predicateTest.conditionFilter(list, integer -> integer < 8);
        result.forEach(System.out::println);
        System.out.println("-------");
        //输出所有数字
        result = predicateTest.conditionFilter(list, integer -> true);
        result.forEach(System.out::println);
        System.out.println("-------");
    }
    //高度抽象的方法定义,复用性高
    public List<Integer> conditionFilter(List<Integer> list, Predicate<Integer> predicate){
        return list.stream().filter(predicate).collect(Collectors.toList());
    }
}

我们只定义了一个conditionFilter方法,stream()会将当前list作为源创建一个Stream对象,collect(Collectors.toList())是将最终的结果封装在ArrayList中(这部分会在后续stream学习中详细介绍,这里只关注filter即可),filter方法接收一个Predicate类型参数用于对目标集合进行过滤。里面并没有任何具体的逻辑,提供了一种更高层次的抽象化,我们可以把要处理的数据和具体的逻辑通过参数传递给conditionFilter即可。理解了这种设计思想后,再看上面的例子就很容易理解,本身逻辑并不复杂,分别取出小于5、大于等于5、小于8的元素,最后一个总是返回true的条件意味着打印出集合中所有元素。
除此之外,Predicate还提供了另外三个默认方法和一个静态方法

default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) && other.test(t);
}

default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
}

default Predicate<T> negate() {
    return (t) -> !test(t);
}

static <T> Predicate<T> isEqual(Object targetRef) {
    return (null == targetRef)
            ? Objects::isNull
            : object -> targetRef.equals(object);
}

and方法接收一个Predicate类型,也就是将传入的条件和当前条件以并且的关系过滤数据。or方法同样接收一个Predicate类型,将传入的条件和当前的条件以或者的关系过滤数据。negate就是将当前条件取反。看下具体使用方式

public List<Integer> conditionFilterNegate(List<Integer> list, Predicate<Integer> predicate){
    return list.stream().filter(predicate.negate()).collect(Collectors.toList());
}

public List<Integer> conditionFilterAnd(List<Integer> list, Predicate<Integer> predicate,Predicate<Integer> predicate2){
    return list.stream().filter(predicate.and(predicate2)).collect(Collectors.toList());
}

public List<Integer> conditionFilterOr(List<Integer> list, Predicate<Integer> predicate,Predicate<Integer> predicate2){
    return list.stream().filter(predicate.or(predicate2)).collect(Collectors.toList());
}

//大于5并且是偶数
result = predicateTest.conditionFilterAnd(list, integer -> integer > 5, integer1 -> integer1 % 2 == 0);
result.forEach(System.out::println);//6 8 10
System.out.println("-------");

//大于5或者是偶数
result = predicateTest.conditionFilterOr(list, integer -> integer > 5, integer1 -> integer1 % 2 == 0);
result.forEach(System.out::println);//2 4 6 8 9 10
System.out.println("-------");

//条件取反
result = predicateTest.conditionFilterNegate(list,integer2 -> integer2 > 5);
result.forEach(System.out::println);// 1 2 3 4 5
System.out.println("-------");

我们分别借助Predicate的三个默认方法定义了conditionFilterAnd、conditionFilterOr和conditionFilterNegate方法。然后再下方调用这三个方法,根据传入的判断条件观察输出结果。

最后再来看一下Predicate接口中的唯一一个静态方法,Java8中接口中除了增加了默认方法也可以定义静态方法。

/**
 * Returns a predicate that tests if two arguments are equal according
 * to {@link Objects#equals(Object, Object)}.
 *
 * @param <T> the type of arguments to the predicate
 * @param targetRef the object reference with which to compare for equality,
 *               which may be {@code null}
 * @return a predicate that tests if two arguments are equal according
 * to {@link Objects#equals(Object, Object)}
 */
static <T> Predicate<T> isEqual(Object targetRef) {
    return (null == targetRef)
            ? Objects::isNull
            : object -> targetRef.equals(object);
}

isEqual方法返回类型也是Predicate,也就是说通过isEqual方法得到的也是一个用来进行条件判断的函数式接口实例。而返回的这个函数式接口实例是通过传入的targetRef的equals方法进行判断的。我们看一下具体用法

System.out.println(Predicate.isEqual("test").test("test"));//true

这里会用第一个"test"的equals方法判断与第二个"test"是否相等,结果true。

7-Optional类详解

前面我们详细介绍了Predicate函数式接口中主要的一些方法使用,本篇介绍的Optional虽然并不是一个函数式接口,但是也是一个极其重要的类。

Optional并不是我们之前介绍的一系列函数式接口,它是一个class,主要作用就是解决Java中的NPE(NullPointerException)。空指针异常在程序运行中出现的频率非常大,我们经常遇到需要在逻辑处理前判断一个对象是否为null的情况。

if(null != person){
    Address address = person.getAddress();
    if(null != address){
        ......
    }
}

实际开发中我们经常会按上面的方式进行非空判断,接下来看下使用Optional类如何避免空指针问题

String str = "hello";
Optional<String> optional = Optional.ofNullable(str);
optional.ifPresent(s -> System.out.println(s));//value为hello,正常输出

首先,ofNullable方法接收一个可能为null的参数,将参数的值赋给Optional类中的成员变量value,ifPresent方法接收一个Consumer类型函数式接口实例,再将成员变量value交给Consumer的accept方法处理前,会校验成员变量value是否为null,如果value是null,则什么也不会执行,避免了空指针问题。下方是ifPresent源码

/**
 * If a value is present, invoke the specified consumer with the value,
 * otherwise do nothing.
 *
 * @param consumer block to be executed if a value is present
 * @throws NullPointerException if value is present and {@code consumer} is
 * null
 */
public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

如果传入的内容是空,则什么也不会执行,也不会有空指针异常

String str = null;
Optional<String> optional = Optional.ofNullable(str);
optional.ifPresent(s -> System.out.println(s));//不会输出任何内容

如果为空时想返回一个默认值

String str = null;
Optional<String> optional = Optional.ofNullable(str);
System.out.println(optional.orElseGet(() -> "welcome"));

orElseGet方法接收一个Supplier,还记得前面介绍的Supplier么,不接受参数通过get方法直接返回结果,类似工厂模式,上面代码就是针对传入的str变量,如果不为null那正常输出,如果为null,那返回一个默认值"welcome"

orElseGet方法源码

/**
 * Return the value if present, otherwise invoke {@code other} and return
 * the result of that invocation.
 *
 * @param other a {@code Supplier} whose result is returned if no value
 * is present
 * @return the value if present otherwise the result of {@code other.get()}
 * @throws NullPointerException if value is not present and {@code other} is
 * null
 */
public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

8-方法引用详解

上面我们详细介绍了Optional类用来避免空指针问题,本篇我们全面了解一下Java8中的方法引用特性。
方法引用是lambda表达式的一种特殊形式,如果正好有某个方法满足一个lambda表达式的形式,那就可以将这个lambda表达式用方法引用的方式表示,但是如果这个lambda表达式的比较复杂就不能用方法引用进行替换。实际上方法引用是lambda表达式的一种语法糖。
在介绍方法引用使用方式之前,先将方法引用分下类
方法引用共分为四类:
1.类名::静态方法名
2.对象::实例方法名
3.类名::实例方法名
4.类名::new

首先来看下第一种 类名::静态方法名 为了演示我们自定义了一个Student类

public class Student {
    private String name;
    private int score;

    public Student(){

    }

    public Student(String name,int score){
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public static int compareStudentByScore(Student student1,Student student2){
        return student1.getScore() - student2.getScore();
    }
}

Student类有两个属性name和score并提供了初始化name和score的构造方法,并且在最下方提供了两个静态方法分别按score和name进行比较先后顺序。
接下来的需求是,按着分数由小到大排列并输出,在使用方法引用前,我们先使用lambda表达式的方式进行处理

Student student1 = new Student("zhangsan",60);
Student student2 = new Student("lisi",70);
Student student3 = new Student("wangwu",80);
Student student4 = new Student("zhaoliu",90);
List<Student> students = Arrays.asList(student1,student2,student3,student4);

students.sort((o1, o2) -> o1.getScore() - o2.getScore());
students.forEach(student -> System.out.println(student.getScore()));

sort方法接收一个Comparator函数式接口,接口中唯一的抽象方法compare接收两个参数返回一个int类型值,下方是Comparator接口定义

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

我们再看下Student类中定义的compareStudentByScore静态方法

public static int compareStudentByScore(Student student1,Student student2){
    return student1.getScore() - student2.getScore();
}

同样是接收两个参数返回一个int类型值,而且是对Student对象的分数进行比较,所以我们这里就可以 使用类名::静态方法名 方法引用替换lambda表达式

students.sort(Student::compareStudentByScore);
students.forEach(student -> System.out.println(student.getScore()));

第二种 对象::实例方法名
我们再自定义一个用于比较Student元素的类

public class StudentComparator {
    public int compareStudentByScore(Student student1,Student student2){
        return student2.getScore() - student1.getScore();
    }
}

StudentComparator中定义了一个非静态的,实例方法compareStudentByScore,同样该方法的定义满足Comparator接口的compare方法定义,所以这里可以直接使用 对象::实例方法名 的方式使用方法引用来替换lambda表达式

StudentComparator studentComparator = new StudentComparator();
students.sort(studentComparator::compareStudentByScore);
students.forEach(student -> System.out.println(student.getScore()));

第三种 类名::实例方法名
这种方法引用的方式较之前两种稍微有一些不好理解,因为无论是通过类名调用静态方法还是通过对象调用实例方法这都是符合Java的语法,使用起来也比较清晰明了。那我们带着这个疑问来了解一下这个比较特殊的方法引用。
现在再看一下Student类中静态方法的定义

public static int compareStudentByScore(Student student1,Student student2){
    return student1.getScore() - student2.getScore();
}

虽然这个方法在语法上没有任何问题,可以作为一个工具正常使用,但是有没有觉得其在设计上是不合适的或者是错误的。这样的方法定义放在任何一个类中都可以正常使用,而不只是从属于Student这个类,那如果要定义一个只能从属于Student类的比较方法下面这个实例方法更合适一些

public int compareByScore(Student student){
    return this.getScore() - student.getScore();
}

接收一个Student对象和当前调用该方法的Student对象的分数进行比较即可。现在我们就可以使用 类名::实例方法名 这种方式的方法引用替换lambda表达式了

students.sort(Student::compareByScore);
students.forEach(student -> System.out.println(student.getScore()));

这里非常奇怪,sort方法接收的lambda表达式不应该是两个参数么,为什么这个实例方法只有一个参数也满足了lambda表达式的定义(想想这个方法是谁来调用的)。这就是 类名::实例方法名 这种方法引用的特殊之处:当使用 类名::实例方法名 方法引用时,一定是lambda表达式所接收的第一个参数来调用实例方法,如果lambda表达式接收多个参数,其余的参数作为方法的参数传递进去
结合本例来看,最初的lambda表达式是这样的

students.sort((o1, o2) -> o1.getScore() - o2.getScore());

那使用 类名::实例方法名 方法引用时,一定是o1来调用了compareByScore实例方法,并将o2作为参数传递进来进行比较。是不是就符合了compareByScore的方法定义。

第四种 类名::new
也称构造方法引用,和前两种类似只要符合lambda表达式的定义即可,回想下Supplier函数式接口的get方法,不接收参数有返回值,正好符合无参构造方法的定义
@FunctionalInterface
public interface Supplier<T> {

/**
 * Gets a result.
 *
 * @return a result
 */
T get();

}

Supplier<Student> supplier = Student::new;

上面就是使用了Student类构造方法引用创建了supplier实例,以后通过supplier.get()就可以获取一个Student类型的对象,前提是Student类中存在无参构造方法。

小结:本篇全面介绍了方法引用的四种使用方式,且每种方式都有对应一个示例来帮助大家理解。当我们使用lambda表达式进行函数式编程时,如果某个方法正好满足lambda的定义,也满足实际需求的逻辑,就可以使用方法引用的方式来替换lambda表达式。接下来我们将真正开始学习stream api,并结合前面学习的内容体验stream api的强大之处。

9-Stream介绍与操作方式详解

前面我们系统学了方法引用的几种类型及应用场景,本篇开始我们正式学习Stream。
Java8中的Stream与lambda表达式可以说是相伴相生的,通过Stream我们可以更好的更为流畅更为语义化的操作集合。Stream api都位于java.util.stream包中。其中就包含了最核心的Stream接口,一个Stream实例可以串行或者并行操作一组元素序列,官方文档中给出了一个示例
 * <pre>{@code
 *     int sum = widgets.stream()//创建一个流
 *                      .filter(w -> w.getColor() == RED)//取出颜色是红色的元素
 *                      .mapToInt(w -> w.getWeight())//返回每个红色元素的重量
 *                      .sum();//重量求和
 * }</pre>

Java8中,所有的流操作会被组合到一个 stream pipeline中,这点类似linux中的pipeline概念,将多个简单操作连接在一起组成一个功能强大的操作。一个 stream pileline首先会有一个数据源,这个数据源可能是数组、集合、生成器函数或是IO通道,流操作过程中并不会修改源中的数据;然后还有零个或多个中间操作,每个中间操作会将接收到的流转换成另一个流(比如filter);最后还有一个终止操作,会生成一个最终结果(比如sum)。流是一种惰性操作,所有对源数据的计算只在终止操作被初始化的时候才会执行。

总结一下流操作由3部分组成
1.源
2.零个或多个中间操作
3.终止操作 (到这一步才会执行整个stream pipeline计算)

创建流的几种方式

//第一种 通过Stream接口的of静态方法创建一个流
Stream<String> stream = Stream.of("hello", "world", "helloworld");
//第二种 通过Arrays类的stream方法,实际上第一种of方法底层也是调用的Arrays.stream(values);
String[] array = new String[]{"hello","world","helloworld"};
Stream<String> stream3 = Arrays.stream(array);
//第三种 通过集合的stream方法,该方法是Collection接口的默认方法,所有集合都继承了该方法
Stream<String> stream2 = Arrays.asList("hello","world","helloworld").stream();

接下来我们看一个简单的需求:将流中字符全部转成大写返回一个新的集合

List<String> list = Arrays.asList("hello", "world", "helloworld");
List<String> collect = list.stream().map(s -> s.toUpperCase()).collect(Collectors.toList());

这里我们使用了Stream的map方法,map方法接收一个Function函数式接口实例,这里的map和Hadoop中的map概念完全一致,对每个元素进行映射处理。然后传入lambda表达式将每个元素转换大写,通过collect方法将结果收集到ArrayList中。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);//map函数定义

那如果我们想把结果放到Set中或者替他的集合容器,也可以这样

list.stream().map(s -> s.toUpperCase()).collect(Collectors.toSet());//放到Set中

或者更为通用的

list.stream().map(s -> s.toUpperCase()).collect(Collectors.toCollection(TreeSet::new));//自定义容器类型

我们可以自己制定结果容器的类型Collectors的toCollection接受一个Supplier函数式接口类型参数,可以直接使用构造方法引用的方式。

Stream中除了map方法对元素进行映射外,还有一个flatMap方法

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

flatMap从方法命名上可以解释为扁平的map

图片描述
map方法是将一个容器里的元素映射到另一个容器中。
图片描述
flatMap方法,可以将多个容器的元素全部映射到一个容器中,即为扁平的map。
看一个求每个元素平方的例子

Stream<List<Integer>> listStream =
                Stream.of(Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6));
List<Integer> collect1 = listStream.flatMap(theList -> theList.stream()).
                map(integer -> integer * integer).collect(Collectors.toList());

首先我们创建了一个Stream对象,Stream中的每个元素都是容器List<Integer>类型,并使用三个容器list初始化这个Stream对象,然后使用flatMap方法将每个容器中的元素映射到一个容器中,这时flatMap接收的参数Funciton的泛型T就是List<Integer>类型,返回类型就是T对应的Stream。最后再对这个容器使用map方法求出买个元素的平方。

然后介绍一个用于获取统计信息的方法

//同时获取最大 最小 平均值等信息
List<Integer> list1 = Arrays.asList(1, 3, 5, 7, 9, 11);
IntSummaryStatistics statistics = list1.stream().filter(integer -> integer > 2).mapToInt(i -> i * 2).skip(2).limit(2).summaryStatistics();
System.out.println(statistics.getMax());//18
System.out.println(statistics.getMin());//14
System.out.println(statistics.getAverage());//16

将list1中的数据取出大于2的,每个数进行平方计算,skip(2)忽略前两个,limit(2)再取出前两个,summaryStatistics对取出的这两个数计算统计数据。mapToInt接收一个ToIntFunction类型,也就是接收一个参数返回值是int类型。

接下来看一下Stream中的一个静态方法,generate方法

/**
 * Returns an infinite sequential unordered stream where each element is
 * generated by the provided {@code Supplier}.  This is suitable for
 * generating constant streams, streams of random elements, etc.
 *
 * @param <T> the type of stream elements
 * @param s the {@code Supplier} of generated elements
 * @return a new infinite sequential unordered {@code Stream}
 */
public static<T> Stream<T> generate(Supplier<T> s) {
    Objects.requireNonNull(s);
    return StreamSupport.stream(
            new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}

generate接收一个Supplier,适合生成连续不断的流或者一个全部是随机数的流

Stream.generate(UUID.randomUUID()::toString).findFirst().ifPresent(System.out::println);

使用UUID.randomUUID()::toString 方法引用的方式创建了Supplier,然后取出第一个元素,这里的findFirst返回的是 Optional,因为流中有可能没有元素,为了避免空指针,在使用前 ifPresent 进行是否存在的判断。

最后再学习一下另一个静态方法,iterate

/**
 * Returns an infinite sequential ordered {@code Stream} produced by iterative
 * application of a function {@code f} to an initial element {@code seed},
 * producing a {@code Stream} consisting of {@code seed}, {@code f(seed)},
 * {@code f(f(seed))}, etc.
 *
 * <p>The first element (position {@code 0}) in the {@code Stream} will be
 * the provided {@code seed}.  For {@code n > 0}, the element at position
 * {@code n}, will be the result of applying the function {@code f} to the
 * element at position {@code n - 1}.
 *
 * @param <T> the type of stream elements
 * @param seed the initial element
 * @param f a function to be applied to to the previous element to produce
 *          a new element
 * @return a new sequential {@code Stream}
 */
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
    Objects.requireNonNull(f);
    final Iterator<T> iterator = new Iterator<T>() {
        @SuppressWarnings("unchecked")
        T t = (T) Streams.NONE;

        @Override
        public boolean hasNext() {
            return true;
        }

        @Override
        public T next() {
            return t = (t == Streams.NONE) ? seed : f.apply(t);
        }
    };
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
            iterator,
            Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
}

iterate方法有两个参数,第一个是seed也可以称作种子,第二个是一个UnaryOperator,UnaryOperator实际上是Function的一个子接口,和Funciton区别就是参数和返回类型都是同一种类型

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

}

iterate方法第一次生成的元素是UnaryOperator对seed执行apply后的返回值,之后所有生成的元素都是UnaryOperator对上一个apply的返回值再执行apply,不断循环。
f(f(f(f(f(f(n))))))......

//从1开始,每个元素比前一个元素大2,最多生成10个元素
Stream.iterate(1,item -> item + 2).limit(10).forEach(System.out::println);

我们在使用stream api时也要注意一些陷阱,比如下面这个例子

//Stream陷阱 distinct()会一直等待产生的结果去重,将distinct()和limit(6)调换位置,先限制结果集再去重就可以了
IntStream.iterate(0,i -> (i + 1) % 2).distinct().limit(6).forEach(System.out::println);

如果distinct()一直等待那程序会一直执行不断生成数据,所以需要先限制结果集再去进行去重操作就可以了。

10-Stream分组与分区详解

第九章我们介绍了Strem的概念与实际的一些操作,本篇我们继续来学习Stream的另一个重要操作,分组与分区。
我们在上一篇介绍Stream的操作时,会经常使用到Collectors这个类,这个类实际上是一个封装了很多常用的汇聚操作的一个工厂类。我们之前用到过
//将结果汇聚到ArrayList中
Collectors.toList();
//将结果汇聚到HashSet中
Collectors.toSet();

以及更为通用的

//将结果汇聚到一个指定类型的集合中
Collectors.toCollection(Supplier<C> collectionFactory);

Stream分组

在实际开发中,对于将一个集合的内容进行分组或分区这种需求也非常常见,所以我们继续学习下Collectors类中的groupingBy和partitioningBy方法。

public static Collector groupingBy(Function<? super T, ? extends K> classifier){
    //...
}

groupingBy接收一个Function类型的变量classifier,classifier被称作分类器,收集器会按着classifier作为key对集合元素进行分组,然后返回Collector收集器对象,假如现在有一个实体Student

public class Student {
    private String name;
    private int score;
    private int age;

    public Student(String name,int score,int age){
        this.name = name;
        this.score = score;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

我们现在按Student的name进行分组,如果使用sql来表示就是select * from student group by name; 再看下使用Stream的方式

Map<String, List<Student>> collect = students.stream().collect(Collectors.groupingBy(Student::getName));

这里我们使用方法引用(类名::实例方法名)替代lambda表达式(s -> s.getName())的方式来指定classifier分类器,使集合按Student的name来分组。
注意到分组后的返回类型是Map<String, List<Student>>,结果集中会将name作为key,对应的Student集合作为value返回。
那如果按name分组后,想求出每组学生的数量,就需要借助groupingBy另一个重载的方法

public static Collector groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream){
    //...
}

第二个参数downstream还是一个收集器Collector对象,也就是说我们可以先将classifier作为key进行分组,然后将分组后的结果交给downstream收集器再进行处理

//按name分组 得出每组的学生数量 使用重载的groupingBy方法,第二个参数是分组后的操作
Map<String, Long> collect1 = students.stream().collect(Collectors.groupingBy(Student::getName, Collectors.counting()));

Collectors类这里也帮我们封装好了用于统计数量的counting()方法,这里先了解一下counting()就是将收集器中元素求总数即可,后续我们会再深入源码学习。

我们还可以对分组后的数据求平均值

Map<String, Double> collect2 = students.stream().collect(Collectors.groupingBy(Student::getName, Collectors.averagingDouble(Student::getScore)));

averagingDouble方法接收一个ToDoubleFunction参数

@FunctionalInterface
public interface ToDoubleFunction<T> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    double applyAsDouble(T value);
}

ToDoubleFunction实际上也是Function系列函数式接口中的其中一个特例,接收一个参数,返回Double类型(这里是接收一个Student返回score)。因为分组后的集合中每个元素是Student类型的,所以我们无法直接对Student进行求平均值

//伪代码
Collectors.averagingDouble(Student))

所以需要将Student转成score再求平均值,Collectors.averagingDouble(Student::getScore))。

Stream分区

针对上面的Student,我们现在再加一个需求,分别统计一下及格和不及格的学生(分数是否>=60)
这时候符合Stream分区的概念了,Stream分区会将集合中的元素按条件分成两部分结果,key是Boolean类型,value是结果集,满足条件的key是true,我们看下示例。

Map<Boolean, List<Student>> collect3 = students.stream().collect(Collectors.partitioningBy(student -> student.getScore() >= 60));
System.out.println(collect3.get(true));//输出及格的Student
System.out.println(collect3.get(false));//输出不及格的Student

partitioningBy方法接收一个Predicate作为分区判断的依据,满足条件的元素放在key为true的集合中,反之放在key为false的集合中

//partitioningBy方法
public static Collector partitioningBy(Predicate<? super T> predicate) {
    return partitioningBy(predicate, toList());
}


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值