Java 8 In Action

本贴最后更新于 1700 天前,其中的信息可能已经时移世异

函数式编程

函数式编程给我的直观感受:

  • 让方法参数具备行为能力,以使方法能够从容地应对频繁的业务需求变更。(替代接口的匿名实现类的编写)
  • 简化代码的编写,并增强代码的可读性

引言——让方法参数具备行为能力

假设你现在是一个农场主,你采摘了一筐苹果如下:

List<Apple> apples = Arrays.asList(
    new Apple("red", 100),
    new Apple("red", 300),
    new Apple("red", 500),
    new Apple("green", 200),
    new Apple("green", 400),
    new Apple("green", 600),
    new Apple("yellow", 300),
    new Apple("yellow", 400),
    new Apple("yellow", 500)
);

Apple

@Data
@AllArgsConstructor
public class Apple {
    private String color;
    private int weight;
}

现在需要你编写一个方法,挑选出箩筐中颜色为绿色的苹果,于是你轻而易举地写了如下代码:

@Test
public void pickGreenApples() {
    List<Apple> list = new ArrayList<>();
    for (Apple apple : apples) {
        if (Objects.equals(apple.getColor(), "green")) {
            list.add(apple);
        }
    }
    System.out.println(list);
}

[Apple(color=green, weight=200), Apple(color=green, weight=400), Apple(color=green, weight=600)]

如果需要你挑选出红色的呢?你发现可以将按照颜色挑选苹果抽取出来以供复用:

public void pickByColor(List<Apple> apples, String color) {
    for (Apple apple : apples) {
        if (Objects.equals(apple.getColor(), color)) {
            System.out.println(apple);
        }
    }
}

@Test
public void testPickByColor() {
    pickByColor(apples, "red");
}

Apple(color=red, weight=100)
Apple(color=red, weight=300)
Apple(color=red, weight=500)

好了,现在我需要你挑选出重量在 400g 以上,颜色不是绿色的苹果呢?你会发现,根据不同的颜色和重量挑选标准能够组合成若干的挑选策略,那是不是针对每个策略我们都需要编写一个方法呢?这当然不可能,我们也无法事先预知顾客需要什么样的苹果。

现在我们来理一下业务需求,其实也就是我们给顾客一个苹果,由他来判断是否符合他的食用标准,这可以采用策略模式来实现:

  1. 将判断某个苹果是否符合标准这一行为抽象为一个接口:

    public interface AppleJudgementStrategy {
        /**
         * 你给我一个apple,我判断他是否符合挑选出来的标准
         * @param apple the apple in container
         * @return true if apple need be picked
         */
        boolean judge(Apple apple);
    }
    
  2. 挑选苹果的方法根据策略挑选苹果(面向接口编程,灵活性更大)

    public void pickByStrategy(List<Apple> apples,AppleJudgementStrategy strategy) {
        for (Apple apple : apples) {
            if (strategy.judge(apple)) {
                // apple 符合既定的挑选策略
                System.out.println(apple);
            }
        }
    }
    
  3. 业务方法根据实际的业务需求创建具体的挑选策略传给挑选方法

    @Test
    public void testPickByStrategy() {
        // 挑选400g以上且颜色不是绿色的
        pickByStrategy(apples, new AppleJudgementStrategy() {
            @Override
            public boolean judge(Apple apple) {
                return apple.getWeight() >= 400 && !Objects.equals(apple.getColor(), "green");
            }
        });
    }
    
    Apple(color=red, weight=500)
    Apple(color=yellow, weight=400)
    Apple(color=yellow, weight=500)
    

那么以上的代码重构是无需基于 Java 8 的,但其中有一个弊端:策略模式的实现要么需要创建一个新类,要么使用匿名类的方式。在此过程中,new/implements AppleJudgementStrategypublic boolean judge 的大量重复是很冗余的,Java 8 引入的 Lambda 表达式就很好的改善了这一点,如上述代码可简化如下:

@Test
public void testPickByStrategyWithLambda() {
    pickByStrategy(apples, apple -> apple.getWeight() >= 400 && !Objects.equals(apple.getColor(), "green"));
}

Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)

本节通过挑选苹果的例子初步体验了一下 Lambda 表达式的魅力,但其的作用不仅于此,接下来让我们打开函数式编程的大门。

为什么要引入函数式编程

正如上一节说表现的,面对同一业务(挑选苹果)的需求的频繁变更,业务方法仅能接受基本类型、引用类型(对象类型)的参数已经不能满足我们的需求了,我们期望能够通过参数接受某一特定的行为(如判断某个苹果是否应该被挑选出来)以使方法能够“以不变应万变”。

上例中,虽然我们能通过策略模式达到此目的,但如果究其本质,策略接口其实就是对一个函数的封装,而我们业务方法参数接收该对象也仅仅是为了调用该对象实现的接口方法。无论我们是在调用业务方法之前创建一个该接口的实现类然后在调用业务方法传参时 new 该实现类,还是在调用业务方法传参时直接 new 一个匿名类,这些创建类的操作都只是为了遵守“在 Java 中,类是第一公民的事实”(即先有类,后有方法,方法必须封装在类中)。因此,创建类和声明方法的过程显得有些多余,其实业务方法只是想要一个具体的策略而已,如 return apple.getWeight() >= 400(挑选出重量大于 400g 的苹果)。

因此你会看到 Java 8 引入 Lambda 表达式之后,业务调用方可以变更如下:

// 业务方法
public void pickByStrategy(List<Apple> apples,AppleJudgementStrategy strategy) {
    for (Apple apple : apples) {
        if (strategy.judge(apple)) {
            // apple 符合既定的挑选策略
            System.out.println(apple);
        }
    }
}

// 调用业务
@Test
public void testPickByStrategy() {
    // 挑选400g以上的
    pickByStrategy( apples, (apple) -> { return apple.getWeight() >= 400 } );
}

我们使用一个 Lambda 表达式 (apple) -> { return apple.getWeight() >= 400 } 代替了 AppleJudgementStrategy 实例的创建

什么是 Lambda 表达式

Lambda 表达式可以理解为函数表达式,在上例中指的就是 (apple) -> { return apple.getWeight() >= 400 },表示对接口 AppleJudgementStrategy 函数 boolean judge(Apple apple); 的一个实现。其中 (apple) 表示函数接受一个 Apple 类型的对象;{ return apple.getWeight() >= 400 } 则是函数体,表示传入 Apple 对象的重量大于 400 时返回 true

Lambda 表达式和接口是密切相关的,而且能使用 Lambda 表达式代替其实例的接口必须是只声明了一个抽象方法的接口

  • 先要有一个接口,不能是抽象类
  • 该接口中有且仅有一个不带方法体的抽象方法
  • 该接口中可以有若干个带有方法体的 defaultstatic 方法
  • 可以在接口上标注 @FunctionalInterface 以表示该接口是函数式接口,其实例可以通过 Lambda 表达式创建,并且该注解可以约束该接口满足上述三点规则

注意:Java 8 对接口进行了重新定义,为了获得更好的兼容性,接口中的方法可以有方法体,此时该方法需被标记为 default,即该方法有一个默认的实现,子类可以选择性的重写。不像抽象方法,必须被具体子类重写。

此外接口中还可以定义带有方法体的静态方法,可以通过 接口名.方法名 的形式访问,这一点与类静态方法无异。

上述两点打破了 Java 8 之前对接口的定义:接口中的方法必须都是抽象方法(public abstract)。

也就是说在 AppleJudgementStrategy 添加若干 defaultstatic 方法,都是不影响使用 Lambda 表达式来代替其实例的(在 IDE 中,@FunctionalInterface 注解不会报红表示这是一个合法的函数式接口):

@FunctionalInterface
public interface AppleJudgementStrategy {
    /**
     * 你给我一个apple,我判断他是否符合挑选出来的标准
     * @param apple the apple in container
     * @return true if apple need be picked
     */
    boolean judge(Apple apple);

    default void fun1() {
        System.out.println("这是带有默认实现的方法");
    }

    static void fun2() {
        System.out.println("这是定义在接口中的静态方法");
    }
}

有了函数式接口之后我们就可以在需要该接口实例的地方使用 lambda 表达式了。

实战

下面我们通过实战来巩固 lambda 表达式的运用。

  1. 编写函数式接口:

    @FunctionalInterface
    public interface AccumulatorFunction {
    
        /**
         * 该函数聚合两个整数通过运算生成一个结果
         * @param a 整数a
         * @param b 整数b
         * @return  运算结果
         */
        int accumulate(int a, int b);
    }
    
  2. 编写业务方法,参数接受函数式接口实例,让该参数具备行为能力

    /**
         * 通过既定的计算规则对输入值a和b得出输出结果
         * @param a
         * @param b
         * @param accumulatorFunction
         * @return
         */
    public int compute(int a, int b, AccumulatorFunction accumulatorFunction) {
        return accumulatorFunction.accumulate(a,b);
    }
    
  3. 编写业务调用方,通过 lambda 表达式阐述行为

    @Test
    public void testAccumulatorFunction() {
        int res1 = compute(1, 2, (a, b) -> {		//阐述的行为是求和
            return a + b;
        });
        System.out.println("1加2的结果是:" + res1);
        int res2 = compute(1, 2, (a, b) -> {		//阐述的行为是求乘积
            return a * b;
        });
        System.out.println("1乘2的结果是:" + res2);
    }
    
    12的结果是:3
    12的结果是:2
    

Lambda 表达式的编写规则

通过上一节我们知道 lambda 表达式是和函数式接口密切相关的,在需要传入函数式接口实例时我们可以编写 lambda 表达式,编写时我们要特别关注该接口抽象方法定义的参数列表以及返回值。

假设函数式接口声明如下(其中 R,T1,T2,T3 为基本类型或引用类型):

@FunctionalInterface
public interface MyFunction{
    R fun(T1 t1, T2 t2, T3 t3);
}

那么你的 lambda 表达式就应该编写如下:

(t1, t2, t3) -> {	// 参数名自定义,为a,b,c也可以,但是要知道在方法体中访问时a的类型是T1,b的类型是T2
    // do you service with t1,t2,t3
    return instance_of_R;
}

总结如下,严格意义上的 lambda 表达式需包含:

  • 入参列表,用小括号括起来
  • 箭头,由入参列表指向函数体
  • 函数体,由大括号括起来,其中编写语句,若函数有返回值则还应用 return 语句作为结尾

但是为了书写简洁,上述几点有时不是必须的。

入参列表只有一个参数时可以省略小括号

t1 -> {
    // do your service with t1
    return instance_of_R;
}

其他情况下(包括无参时)都必须有小括号

函数体可以用表达式代替

当函数体只有一条语句时,如 return t1+t2+t3(假设 t1,t2,t3,R 都是 int 型),那么方法体可以省略大括号和 return

(t1,t2,t3) -> t1+t2+t3

只要函数体包含了语句(必须以分号结尾),那么函数体就需要加上大括号

function 包 & 方法推导

在 Java 8 中,为我们新增了一个 java.util.function 包,其中定义的就全部都是函数式接口,其中最为主要的接口如下:

  • Consumer,接受一个参数,没有返回值。代表了消费型函数,函数调用消费你传入的参数,但不给你返回任何信息
  • Supplier,不接收参数,返回一个对象。代表了生产型函数,通过函数调用能够获取特定的对象
  • Predicate,接收一个对象,返回一个布尔值。代表了断言型函数,对接收的对象断言是否符合某种标注。(我们上面定义的 AppleJudgementFunction 就属于这种函数。
  • Function,接收一个参数,返回函数处理结果。代表了输入-输出型函数。

该包下的其他所有接口都是基于以上接口在参数接受个数(如 BiConsumer 消费两个参数)、和参数接收类型(如 IntConsumer 仅用于消费 int 型参数)上做了一个具体化。

Consumer In Action

对每个苹果进行“消费”操作:

public void consumerApples(List<Apple> apples, Consumer<Apple> consumer) {
    for (Apple apple : apples) {
        consumer.accept(apple);
    }
}

如“消费”操作就是将传入的苹果打印一下:

@Test
public void testConsumer() {
    consumerApples(apples, apple -> System.out.println(apple));
}

Apple(color=red, weight=100)
Apple(color=red, weight=300)
Apple(color=red, weight=500)
Apple(color=green, weight=200)
Apple(color=green, weight=400)
Apple(color=green, weight=600)
Apple(color=yellow, weight=300)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)

如果你使用的 IDE 是 IDEA,那么它会提示你 System.out.println(apple) 可以 Lambda can be replaced with method inference(即该 lambda 表达式可以使用方法推导),于是我们使用快捷键 alt + Enter 寻求代码优化提示,代表被替换成了 apple -> System.out::println

这里引申出了 lambda 的一个新用法:方法推导。当我们在编写 lambda 时,如果方法体只是一个表达式,并且该表达式调用的方法行为与此处对应的函数接口的行为一致时,可以使用方法推导(类名::方法名对象名::方法名)。

因为这里我们需要一个 Consumer,而 println 的定义与 Consumer.accept 的函数行为是一致的(接受一个对象,无返回值):

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

因此 IDEA 提示我们此处可以使用方法推导 apple -> System.out::println 代替方法调用 apple -> System.out.println(apple)。如此,前者看起来更具可读性:使用 println 行为消费这个 apple

Supplier In Action

Supplier 像是一个工厂方法,你可以通过它来获取对象实例。

如,通过 Supplier 你给我一个苹果,我来打印它的信息

public void printApple(Supplier<Apple> appleSupplier) {
    Apple apple = appleSupplier.get();
    System.out.println(apple);
}

@Test
public void testSupplier() {
    printApple(() -> {
        return new Apple("red", 666);
    });
}

Apple(color=red, weight=666)

如果 Apple 提供无参构造方法,那么这里可以使用构造函数的方法推导(无参构造函数不接收参数,但返回一个对象,和 Supplier.get 的函数类型一致):

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Apple {
    private String color;
    private int weight;
}

@Test
public void testSupplier() {
    //        printApple(() -> {
    //            return new Apple("red", 666);
    //        });
    printApple(Apple::new);
}

Apple(color=null, weight=0)

Predicate In Action

Predicate 则是用来断言对象,即按照某种策略判定入参对象是否符合标准的。

为此我们可以将先前写的挑选苹果的业务方法中的 AppleJudgementStrategy 换成 Predicate,作用是一样的,以后用到策略模式的地方直接使用 Predicate<T> 就行了

//    public void pickByStrategy(List<Apple> apples, AppleJudgementStrategy strategy) {
//        for (Apple apple : apples) {
//            if (strategy.judge(apple)) {
//                // apple 符合既定的挑选策略
//                System.out.println(apple);
//            }
//        }
//    }
public void pickByStrategy(List<Apple> apples, Predicate<Apple> applePredicate) {
    for (Apple apple : apples) {
        if (applePredicate.test(apple)) {
            // apple 符合既定的挑选策略
            System.out.println(apple);
        }
    }
}

@Test
public void testPickByStrategyWithLambda() {
    pickByStrategy(apples, apple -> 
                   apple.getWeight() >= 400 && !Objects.equals(apple.getColor(),"green"));
}

Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)

Function In Action

Function 就不用说了,代表了最普通的函数描述:有输入,有输出。

我们此前编写的 AccumulatorFunction 就属于一个 BiFunction,让我们来使用 BiFucntion 对其进行改造:

//    public int compute(int a, int b, AccumulatorFunction accumulatorFunction) {
//        return accumulatorFunction.accumulate(a,b);
//    }

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

@Test
public void testAccumulatorFunction() {
    int res1 = compute(1, 2, (a, b) -> {
        return a + b;
    });
    System.out.println("1加2的结果是:" + res1);
    int res2 = compute(1, 2, (a, b) -> {
        return a * b;
    });
    System.out.println("1乘2的结果是:" + res2);
}

12的结果是:3
12的结果是:2

Stream——更优雅的集合操作工具类

引言——Stream 初体验

现在有一个菜品类定义如下:

public class Dish {

    public enum Type {MEAT, FISH, OTHER}

    private final String name;			//菜品名称
    private final boolean vegetarian;	//是否是素食
    private final int calories;			//提供的卡路里
    private final Type type;			//菜品类型

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }

    @Override
    public String toString() {
        return name;
    }
}

给你一份包含若干菜品的菜单:

List<Dish> menu = Arrays.asList(
    new Dish("pork", false, 800, Dish.Type.MEAT),
    new Dish("beef", false, 700, Dish.Type.MEAT),
    new Dish("chicken", false, 400, Dish.Type.MEAT),
    new Dish("french fries", true, 530, Dish.Type.OTHER),
    new Dish("rice", true, 350, Dish.Type.OTHER),
    new Dish("season fruit", true, 120, Dish.Type.OTHER),
    new Dish("pizza", true, 550, Dish.Type.OTHER),
    new Dish("prawns", false, 300, Dish.Type.FISH),
    new Dish("salmon", false, 450, Dish.Type.FISH));

现在要你以卡路里升序的方法打印卡路里在 400 以下的菜品名称清单,在 Java 8 之前,你可能需要这样做:

@Test
public void beforeJava8() {
    // 1. filter which calories is lower than 400 and collect them
    List<Dish> filterMenu = new ArrayList<>();
    for (Dish dish : menu) {
        if (dish.getCalories() < 400) {
            filterMenu.add(dish);
        }
    }
    // 2. sort by calories ascending
    Collections.sort(filterMenu, new Comparator<Dish>() {
        @Override
        public int compare(Dish o1, Dish o2) {
            return o1.getCalories() - o2.getCalories();
        }
    });
    // 3. map Dish to Dish.getName and collect them
    List<String> nameList = new ArrayList<>();
    for (Dish dish : filterMenu) {
        nameList.add(dish.getName());
    }
    // print name list
    System.out.println(nameList);
}

[season fruit, prawns, rice]

在 Java 8 之后,通过 Stream 只需简洁明了的一行代码就能搞定:

@Test
public void userJava8() {
    List<String> nameList = menu.stream()
        // 1. filter which calories is lower than 400
        .filter(dish -> dish.getCalories() < 400)
        // 2. sort by calories ascending
        .sorted(Comparator.comparing(Dish::getCalories))
        // 3. map Dish to Dish.getName
        .map(Dish::getName)
        // 4. collect
        .collect(Collectors.toList());
    System.out.println(nameList);
}

[season fruit, prawns, rice]

Stream 的强大才刚刚开始……

Stream 到底是什么

《Java 8 In Action》给出的解释如下:

定义

  • Sequence of elements——Stream 是一个元素序列,跟集合一样,管理着若干类型相同的对象元素
  • Source——Stream 无法凭空产生,它从一个数据提供源而来,如集合、数组、I/O 资源。值得一提的是,Stream 中元素组织的顺序将遵从这些元素在数据提供源中组织的顺序,而不会打乱这些元素的既有顺序。
  • Data processing operations——Stream 提供类似数据库访问一样的操作,并且只需传入 lambda 表达式即可按照既定的行为操纵数据,如 filter(过滤)map(映射)reduce(聚合)find(查找)match(是否有数据匹配)sort(排序) 等。Stream 操作还可以被指定为串行执行或并行执行以充分利用多 CPU 核心。

特性

  • Pipelining——大多数 Stream 操作返回的是当前 Stream 对象本身,因此可以链式调用,形成一个数据处理的管道,但是也有一些 terminal 操作会终结该管道,如调用 Streamcollect 方法后表示该数据处理流的终结,返回最终的数据集合
  • Internal iteration——Stream 将元素序列的迭代都隐藏了,我们只需提供数据处理流中的这一个阶段到下一个阶段的处理行为(通过调用 Stream 方法传入 lambda 表达式)。

上一节菜品的例子中数据处理流可描述如下(其中的 limit 表示取前 n 个元素):

Stream 的并发

Stream 内部集成了 Java 7 提供的 ForkJoin 框架,当我们通过调用它的 parallel 开启并行执行开关时,Stream 会将数据序列的处理通过 ForkJoinPool 进行并行化执行(不仅仅是开启多个线程,底层还会根据你 CPU 核心数量将子任务分配到不同的核心执行):

@Test
public void userJava8() {
    List<String> nameList = menu.stream().parallel()
        .filter(dish -> {
            System.out.println(Thread.currentThread().getName());
            return dish.getCalories() < 400;
        })
        .sorted(Comparator.comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(Collectors.toList());
    System.out.println(nameList);
}

ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-5
ForkJoinPool.commonPool-worker-3
main
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-1
[season fruit, prawns, rice]

否则在当前线程串行化执行:

@Test
public void userJava8() {
    List<String> nameList = menu.stream()
        .filter(dish -> {
            System.out.println(Thread.currentThread().getName());
            return dish.getCalories() < 400;
        })
        .sorted(Comparator.comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(Collectors.toList());
    System.out.println(nameList);
}

main
main
main
main
main
main
main
main
main
[season fruit, prawns, rice]

Stream 实例化

上文曾提到,Stream 是无法凭空而来的,需要一个数据提供源,而我们最常见的就是数组和集合了。

作为一个接口,我们是无法通过 new Stream 获取其实例的,获取其实例的常用方法有以下几种

Collection#stream

通过 Collection 接口中的 stream() 方法,从一个集合实例中创建一个 Stream,对同一个集合对象调用多次 stream() 会产生不同的 Stream 实例。

开篇挑选苹果的例子中,apples.stream 就是调用了此方法

Arrays.stream(T[] arr)

  • 通过 Arrays.stream(),由一个数组(引用类型或基本类型组)创建一个 Stream

    @Test
    public void testCreateStream() {
        Arrays.stream(new Object[]{"hello",21,99.9,true}).forEach(System.out::println);
    }
    
    hello
    21
    99.9
    true
    

Stream.of()

  • 通过 Stream.of(T t) 可以创建一个仅包含一个元素的 Stream
  • 通过 Stream.of(T... t) 可以从一个变长参列(实际上是一个数组)创建一个 Stream

range/rangeClosed & generate

使用 IntStream/LongStream/DoubleStream 中的方法

IntStream 中的 range/rangeClosed (int, int) 可以创建一个包含指定开区间/闭区间中所有元素的 StreamLongStream 也一样,但 DoubleStream 因为区间中的浮点数有无数个因此没有此方法

IntStream.rangeClosed(0, 4).forEach(i -> {
    new Thread(() -> {
        System.out.println("I am a thread, my name is " + Thread.currentThread().getName());
    }, "t-" + i).start();
});

I am a thread, my name is t-0
I am a thread, my name is t-1
I am a thread, my name is t-2
I am a thread, my name is t-3
I am a thread, my name is t-4

generate(Supplier s),三者都有此方法,通过一个生产策略来创建一个无穷大的 Stream,如果在此 Stream 上进行操作那么每处理完一个元素都会通过调用 s.get 获取下一个要处理的元素。

final int a = 0;
IntStream.generate(() -> a).forEach(System.out::println);

你会发现上述程序会一直打印 0

通过 generate 创建的 stream,如果在之上进行 operation,那么该 processing 会一直进行下去只有遇到异常时才会停止

IntStream.generate(i::getAndIncrement).forEach(e -> {
    System.out.println(e);
    if (e == 5) {
        throw new RuntimeException();
    }
});

0
1
2
3
4
5

java.lang.RuntimeException

Stream Operation 详解

Stream 的数据操纵方法分为 terminalnon-terminalnon-terminal 操作之后可以继续链式调用其他 operation,形成一个流式管道,而 terminal 操作则会终止数据流的移动。Stream 中方法返回 Stream(实际返回的是当前 Stream 实例本身)的都是 non-terminal option

filter——过滤

调用 Streamfilter 并传入一个 Predicate 可以对元素序列进行过滤,丢弃不符合条件(是否符合条件根据你传入的 Predicate 进行判断)的元素

// print the even number between 1 and 10
IntStream.rangeClosed(1, 10).filter(i -> i % 2 == 0).forEach(System.out::println);

2
4
6
8
10

distinct——去重

@Test
public void testDistinct() {
    Stream.of("a", "b", "a", "d", "g", "b").distinct().forEach(System.out::println);
}

a
b
d
g

distinct 会根据 equals 方法对“相等”的对象进行去重。

skip——去除前 n 个元素

相当于 SQLlimit <offset,rows> 语句中的 offset

public void testSkip() {
    IntStream.rangeClosed(1,10).skip(6).forEach(System.out::println);
}

7
8
9
10

limit——取前 n 个元素,丢弃剩余的元素

相当于 SQLlimit <offset,rows> 语句中的 rows

// 丢弃前6个之后Stream只剩下7~10了,再截取前2个,Stream就只有7和8了
IntStream.rangeClosed(1, 10).skip(6).limit(2).forEach(System.out::println);

7
8

map——映射

map 方法接收一个 Function(接收一个对象,返回一个对象),返回什么对象、需不需要借助传入的对象信息就是映射的逻辑,根据你的所需你可以将 Stream 中的所有元素统一换一个类型。

// 从一份菜单(菜品集合)中提取菜品名称清单
List<String> nameList = menu.stream().map(dish -> dish.getName()).collect(Collectors.toList());
System.out.println(menu);

[pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]

flatMap——扁平化

flatMap 可以将你的元素进一步细粒度化,如从某个文件读取文件内容后根据换行符分割创建一个以“行”为元素的 Stream,如果你想进一步将“行”按照“空格”分割得到以“字词”为元素的 Stream,那么你就可以使用 flatMap

你需要传入一个 Function<T,Stream>:传入一个元素,返回一个包含由该元素分割形成若干细粒度元素的 Stream

Stream.of("hello", "world").
    flatMap(str -> Stream.of(str.split(""))).
    forEach(System.out::println);

h
e
l
l
o
w
o
r
l
d

sorted & Comparator.comparing

sort 需要你传入一个定义了排序规则的 Comparator,它相当于一个 BiPredicate

// 将菜品按照卡路里升序排序
menu.stream().
    sorted((dish1,dish2)->dish1.getCalories()-dish2.getCalories())
    .map(Dish::getCalories)
    .forEach(System.out::println);

120
300
350
400
450
530
550
700
800

你会发现 IDEA 提示你 (dish1,dish2)->dish1.getCalories()-dish2.getCalories() 可以简化为 Comparator.comparingInt(Dish::getCalories),使用 comparing 你可以直接传入排序字段如 getColaries,它会自动帮我们封装成一个升序的 BiPredicate,如果你需要降序则再链式调用 reversed 即可:

menu.stream().
    sorted(Comparator.comparingInt(Dish::getCalories).reversed())
    .map(Dish::getCalories)
    .forEach(System.out::println);

800
700
550
530
450
400
350
300
120

count——返回当前 Stream 中的元素个数

match——判断 Stream 中是否有符合条件的元素

这是一个 terminal option,当 Stream 调用 match 之后会返回一个布尔值,match 会根据你传入的 Predicate 返回 true or false,表示当前 Stream 中的所有元素是否存在至少一个匹配(anyMatch)、是否全部匹配(allMatch)、是否全都不匹配(noneMatch)。

这几仅以 anyMatch 的使用示例:

// 菜单中是否有卡路里小于100的菜品
boolean res = menu.stream().anyMatch(dish -> dish.getCalories() < 100);
System.out.println(res);

false

find

这也是一个 terminal operation。通常用于在对 Stream 进行一系列处理之后剩下的元素中取出一个进行消费。

  • findAny,通常用于并行处理 Stream 时,获取最先走完整个数据处理流程的元素。

    比如对于一个 id,可能会开几个线程并行地调接口、查数据库、查缓存、查 ES 的方式获取商品数据,但只要有其中一个方式成功返回数据那么就直接消费这个数据,其他方式不予等待。

    static Random random = new Random();
    
    public Object queryById(Integer id){
        // 随机睡眠0~6秒,模仿从数据库或者调接口获取数据的过程
        try {
            TimeUnit.MILLISECONDS.sleep(random.nextInt(6000));
            String data = UUID.randomUUID().toString();
            System.out.println("get data -> " + data + "[id=" + id + "]");
            return data;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return e;
        }
    
    }
    
    @Test
    public void testFindAny() throws InterruptedException {
        Optional<Object> dataOptional = Stream.of(1, 2, 3, 4, 5)
            // 对于每个id并行获取对应的数据
            .parallel()
            .map(id -> {
                Object res = queryById(id);
                if ( res instanceof Throwable) {
                    throw new RuntimeException();
                }
                return res;
            })
            // 有一个拿到了就直接用,后面到的不管了
            .findAny();
        dataOptional.ifPresent(data -> System.out.println("consume data : " + data));
        Thread.currentThread().join();
    }
    
    get data -> 6722c684-61a6-4065-9472-a58a08bbc9d0[id=1]spend time : 442 ms
    get data -> 1975f02b-4e54-48f5-9dd0-51bf2789218e[id=2]spend time : 1820 ms
    get data -> fd4bacb1-a34d-450d-8ebd-5f390167f5f8[id=4]spend time : 4585 ms
    get data -> b2336c45-c1f9-4cd3-b076-83433fdaf543[id=3]spend time : 4772 ms
    get data -> fcc25929-7765-467a-bf36-b85afab5efe6[id=5]spend time : 5575 ms
    consume data : 6722c684-61a6-4065-9472-a58a08bbc9d0
    

    上面的输出中 consume data 始终都是 spend time 最短的数据

  • findFirst 则必须等到 Stream 中元素组织顺序(初始时是数据提供源的元素顺序,如果你调用了 sorted 那么该顺序就会变)的第一个元素处理完前面的流程然后消费它:

    Optional<Object> dataOptional = Stream.of(1, 2, 3, 4, 5)
        // 对于每个id并行获取对应的数据
        .parallel()
        .map(id -> {
            Object res = queryById(id);
            if ( res instanceof Throwable) {
                throw new RuntimeException();
            }
            return res;
        })
        // 先拿到的先用,后面到的不管了
        .findFirst();
    dataOptional.ifPresent(data -> System.out.println("consume data : " + data));
    Thread.currentThread().join();
    
    get data -> d6ac2dd6-66b6-461c-91c4-fa2f13326210[id=4]spend time : 1271 ms
    get data -> 560f5c0e-c2ac-4030-becc-1ebe51ebedcb[id=5]spend time : 2343 ms
    get data -> 11925a9f-03e8-4136-8411-81994445167e[id=1]spend time : 2825 ms
    get data -> 0ecfa02e-3903-4d73-a18b-eb9ac833a899[id=2]spend time : 3270 ms
    get data -> 930d7091-cfa6-4561-a400-8d2e268aaa83[id=3]spend time : 5166 ms
    consume data : 11925a9f-03e8-4136-8411-81994445167e
    

    consume data 始终都是调用 findFirst 时在 Stream 排在第一位的元素。

reduce——聚合计算

reduce 是对当前 Stream 中的元素进行求和、求乘积、求最大/小值等需要遍历所有元素得出一个结果的聚合操作。它有如下重载方法:

  • Optional<T> reduce(BinaryOperator<T> accumulator);

    通过 accumulatorStream 中的元素做聚合操作,返回一个包装了操作结果的 Optional,通过该 Optional 可以拿到该结果以及判断该结果是否存在(如果 Stream 没有元素,那么自然也就没有聚合结果了)

    accumulator 是一个 BiFunction,相当于你在遍历时访问相邻的两个元素得出一个结果,这样 Stream 就能依据此逻辑遍历所有元素得到最终结果

    Optional<Dish> dishOptional = menu.stream()
        .filter(dish -> dish.getCalories() > 600)
        .reduce((d1, d2) -> d1.getCalories() < d2.getCalories() ? d1 : d2);
    if (dishOptional.isPresent()) {
        Dish dish = dishOptional.get();
        System.out.println("大于600卡路里的菜品中,卡路里最小的是:" + dish);
    } else {
        System.out.println("没有大于600卡路里的菜品");
    }
    
    大于600卡路里的菜品中,卡路里最小的是:beef
    
    OptionalInt reduce = IntStream.rangeClosed(1, 10)
                    .reduce((i, j) -> i + j);	// -> method inference: Integer::sum
            reduce.ifPresent(System.out::println);
    55
    

    方法逻辑的伪代码如下:

    if(stream is emtpy){
        return optional(null)
    }else{
        result = new Element()
        foreach element in stream
            result = accumulator.apply(element, result);
        return optional(result)
    }
    
  • T reduce(T identity, BinaryOperator<T> accumulator);

    此方法增加了一个 identity,它的作用是如果调用 reduce 时当前 Stream 中没有元素了,也应该返回一个 identity 作为默认的初始结果。否则,调用空的 Optionalget 会报错:

    OptionalInt reduce = IntStream.rangeClosed(1, 10)
        .filter(i -> i > 10)
        .reduce(Integer::sum);
    System.out.println(reduce.isPresent());
    System.out.println(reduce.getAsInt());
    
    
    false
    java.util.NoSuchElementException: No value present
    

    如果加了 identityreduce 无论如何都将返回一个明确的结果(如果有元素就返回聚合后的结果,否则返回 identity),而不是一个结果未知的 Optional

    int res = IntStream.rangeClosed(1, 10)
    	.filter(i -> i > 10)
    	.reduce(0, Integer::sum);
    System.out.println(res);
    
    0
    

    值得注意的是,identity 的值不应该随便给出,给出的规则应该符合:如果 Stream 中有元素,那么对于任意元素 element,给定的 identity 应该满足 accumulator.apply(element, result),否则会出现如下令人困惑的现象:

    int res = IntStream.rangeClosed(1, 10).reduce(1, (i, j) -> i * j);
    System.out.println(res);	//3628800
    
    int res = IntStream.rangeClosed(1, 10).reduce(0, (i, j) -> i * j);
    System.out.println(res);	//0
    

    上面给定的 identity1accumulator 逻辑为累乘时,满足对于任意元素 e 都有 e * 1 == e,因此不会影响聚合逻辑;而将 identity 换成 0,无论 Stream 中有什么元素,聚合结果都为 0。这是由 reduce(identity,accumulator) 的方法逻辑导致的,其伪代码如下:

    if(stream is emtpy){
        return optional(identity)
    }else{
        result = identity
        foreach element in stream
            result = accumulator.apply(element, result);
        return optional(result)
    }
    

    可以发现,首先将聚合结果 result 置为 identity,然后将每个元素累乘到 result 中(result = element * result),由于任何数乘零都得零,因此聚合结果始终返回 0 了。

  • <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

    此方法又多了一个 combiner,这个 combiner 是为了支持并行的,如果是在串行状态下调用那么传入的 combiner 不会起到任何作用:

    String reduce = Stream.of("h", "e", "l", "l", "o").reduce("", (a, b) -> a + b);
    System.out.println(reduce);
    
    hello
    

    但如果是串行状态下调用,那么 combiner 会根据 ForkJoin 机制和 Stream 使用的 Splierator 在子任务回溯合并时对合并的两个子结果做一些处理:

    String result = Stream.of("h", "e", "l", "l", "o")
        .parallel()
        .reduce("", (a, b) -> a + b, (a, b) -> a + b + "=");
    System.out.println(result);		//he=llo===
    

    我们可以在 combiner 中查看当前合并的两个子结果:

    String result = Stream.of("h", "e", "l", "l", "o")
        .parallel()
        .reduce("", (a, b) -> a + b, (a, b) -> {
            System.out.println("combine " + a + " and " + b + ", then append '='");
            return a + b + "=";
        });
    System.out.println("the final result is :" + result);
    
    
    combine l and o, then append '='
    combine l and lo=, then append '='
    combine h and e, then append '='
    combine he= and llo==, then append '='
    the final result is he=llo===
    

collect——收集元素到集合、分组、统计等

collect 需要传入一个 CollectorCollectors 为我们封装了很多 Collector 的默认实现。

例如

  • collect(Collectors.toList()),可以将元素收集到一个 List 中并返回,类似的有 toSet
  • collect(Collectors.joining()) 能将 Stream 中的字符串元素拼接起来并返回
  • collect(Collectors.groupBy()) 能够实现分组
  • ...其中的大多数 API 都有着类 SQL 的外貌,你可以很容易理解它

Stream In Action

下面通过一个业务员 Trader 和交易 Transaction 的例子来巩固 Stream 的运用。

两个类定义如下:

public class Trader{
    private final String name;
    private final String city;
    public Trader(String n, String c){
        this.name = n;
        this.city = c;
    }
    public String getName(){
        return this.name;
    }
    public String getCity(){
        return this.city;
    }
    public String toString(){
        return "Trader:"+this.name + " in " + this.city;
    }
}

public class Transaction{

    private final Trader trader;
    private final int year;
    private final int value;
    public Transaction(Trader trader, int year, int value){
        this.trader = trader;
        this.year = year;
        this.value = value;
    }
    public Trader getTrader(){
        return this.trader;
    }
    public int getYear(){
        return this.year;
    }
    public int getValue(){
        return this.value;
    }
    @Override
    public String toString(){
        return "{" + this.trader + ", " +
                "year: "+this.year+", " +
                "value:" + this.value +"}";
    }
}

需求如下:

/**
* 1. Find all transactions in the year 2011 and sort them by value (small to high).
* 2. What are all the unique cities where the traders work?
* 3. Find all traders from Cambridge and sort them by name.
* 4. Return a string of all traders’ names sorted alphabetically.
* 5. Are any traders based in Milan?
* 6. Print all transactions’ values from the traders living in Cambridge.
* 7. What’s the highest value of all the transactions?
* 8. Find the transaction with the smallest value.
*/

代码示例:

//1. Find all transactions in the year 2011 and sort them by value (small to high).
List<Transaction> transactions1 = transactions.stream()
    .filter(transaction -> transaction.getYear() == 2011)
    .sorted(Comparator.comparing(Transaction::getValue))
    .collect(Collectors.toList());
System.out.println(transactions1);

//2. What are all the unique cities where the traders work?
String value = transactions.stream()
    .map(transaction -> transaction.getTrader().getCity())
    .distinct()
    .reduce("", (c1, c2) -> c1 + " " + c2);
System.out.println(value);

//3. Find all traders from Cambridge and sort them by name.
transactions.stream()
    .filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity()))
    .map(Transaction::getTrader)
    .sorted(Comparator.comparing(Trader::getName))
    .forEach(System.out::println);

//4. Return a string of all traders’ names sorted alphabetically.
transactions.stream()
    .map(transaction -> transaction.getTrader().getName())
    .distinct()
    .sorted()
    .forEach(System.out::println);

//5. Are any traders based in Milan?
boolean res = transactions.stream()
    .anyMatch(transaction -> "Milan".equals(transaction.getTrader().getCity()));
System.out.println(res);

//6. Print all transactions’ values from the traders living in Cambridge.
transactions.stream()
    .filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity()))
    .map(Transaction::getValue)
    .forEach(System.out::println);

//7. What’s the highest value of all the transactions?
Optional<Integer> integerOptional = transactions.stream()
    .map(Transaction::getValue)
    .reduce(Integer::max);
System.out.println(integerOptional.get());

//8. Find the transaction with the smallest value.
integerOptional = transactions.stream()
    .map(Transaction::getValue)
    .reduce(Integer::min);
System.out.println(integerOptional.get());

Optional——规避空指针异常

其实上节介绍 reduce 的使用时,就有 Optional 的身影,如果你没有给出 identity,那么 reduce 会给你返回一个 Optional,如此 Optional 的身份(拿到这个类的实例,你会立马条件反射:要在访问对象之前判断一下 Optional 包装的对象是否为 null)会提醒你进行非空判断,不至于你拿着 reduce 返回的 null 去使用从而导致空指针异常。

这种将非空判断交给 API 的机制,能够让我们不必每次拿到对象的时候都要为其是否为空而提心吊胆,又或十分敏感地每拿到一个对象都进行一下 if (obj != null)

有了 Optional 以后,避免空指针的两个点转变如下:

  • 方法返回值
    • 之前,尽量返回非 null 的对象,如空字符串 "",空数组等
    • 返回 Optional,若有结果则返回 Optional.ofNullable(obj),若想返回 null 则用 Optional.empty()
  • 使用方法返回值
    • 之前,调用方法拿到返回值,在使用之前要记得非空判断
    • 之后,调用方法拿到的都是 Optional,在使用之前它会提醒我们使用 isPresentifPresent 规避空指针

说白了,之前需要我们人为的记住非空判断,但引入 Optional 后,非空判断流程交给 API 了,卸去了我们对非空判断的关注点,规范了流程开发。

Optional 的创建、使用、非空判断

相关源码如下:

private static final Optional<?> EMPTY = new Optional<>();
private final T value;

private Optional() {
    this.value = null;
}

private Optional(T value) {
    this.value = Objects.requireNonNull(value);
}

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}
public T orElse(T other) {
    return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

public boolean isPresent() {
    return value != null;
}

public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

你会发现其实他的源码很简单,就是将我们要使用的对象包了一层

  • 通过 of(非空对象)ofNullable(可为空对象) 来创建 Optional 实例

  • 通过 isPresent 可以判断内部对象是否为 null

  • 通过 get 可以获取内部对象,如果内部对象为 null 则会抛异常,因此通常在调用 get 前要使用 isPresent 判断一下

    if(optional.isPresent()){
        obj = optional.get();
    }
    
  • orElse,如果不为 null 则返回,否则

    • orElse(T t),返回你传入的对象 t
    • orElseGet(Supplier<T> s),调用 s.get 获取一个对象
    • orElseGet(Supplier<Throwable> e)
  • 通过 ifPresent(consumer),可以整合判断和对象访问,如果对象不为 null,那就用传入的 consumer 消费它

    appleOptional.ifPresent(System.out.println(apple.getName()))
    

此外,Optional 中还提供了 filtermap 两个从 Stream 中借鉴的方法,作用类似,可自行查看源码。

其实我还想说 CompletableFutrueLocalDate/LocalTime/LocalDateTime...

参考资料

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3167 引用 • 8207 回帖 • 1 关注

相关帖子

2 回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • Tidhy

    赞一个,挺详细的

  • 其他回帖
  • PeterChu

    lombok 几个基本注解的使用 @Data@AllArgsConstructor@NoArgsConstructor@Builder