Lambda 表达式和 Stream 流了解下

188 阅读8分钟

前言

Java SE 8.0 发布于 2014-3。1.8 给我们带来的变革还是挺大的,新增 Api 和语法不仅提升了性能,也改变了我们的编码习惯。最好的地方是提升了开发效率。

interface 支持静态方法和默认方法。

Lambda 表达式让我们基本告别匿名内部类,并且 Lambda 表达式在字节码上的处理带来的性能提升大于使用匿名内部类。

Stream 让我们如同操作 mysql 一样处理数据集

更方便的使用重复注解

新增的日期 Api 用的人都说好。

还有一些新增 api、更智能的类型推断 、数据结构的优化及虚拟机的优化,等等

interface 支持静态方法和默认方法。

Lambda 表达式会和函数式接口挂钩的,所以先看看新的接口怎么玩吧。

public interface InterfaceDemo {
    
    // 定义的方法
    int getAge();

    // 默认方法
    default String getName(){
        return "我叫阿良,我是一名剑客";
    }
    
    // 静态方法
    static String getGender(){
        return "男";
    }

    public static void main(String[] args) {
        // 使用匿名内部类
        InterfaceDemo interfaceDemo =new InterfaceDemo() {
            @Override
            public int getAge() {
                return 0;
            }
        };
        System.out.println(interfaceDemo.getAge());
        System.out.println(interfaceDemo.getName());
        System.out.println(InterfaceDemo.getGender());
        
        // 使用 Lambda 表达式
        InterfaceDemo interfaceDemo1 = ()-> 10;
        System.out.println(interfaceDemo1.getAge());
        System.out.println(interfaceDemo1.getName());
    }
}
  • 函数式接口

    • 仅有一个抽象方法
    • 可以有多个默认方法和静态方法

为了在编译阶段就能检测接口是否符合函数式接口定义,可以增加注解 @FunctionalInterface。如果出现多个抽象方法,语法错误

@FunctionalInterface
public interface InterfaceDemo {
    
    int getAge();
    
    default String getName(){
        return "我叫阿良,我是一名剑客";
    }
    default String getName1(){
        return "我叫阿良,我是一名剑客";
    }

    static String getGender(){
        return "男";
    }
    static String getGender1(){
        return "男";
    }
}

Lambda 表达式

其实语法很简单

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);
}

public class Demo {

    @Test
    public void run1() {
        // Lambda 表达式
        
        // (t1)->{
        //    return t1.getId();
        // };
        Function<UserDTO,Integer> function = (t1)->{
            return t1.getId();
        };
    }
}

使用的时候不必过意追求 lambda 语法糖的使用,先用最基本语法的就行。孰能生巧,慢慢的别的形式的语法糖你也会了。

Stream

写代码的时候呢,我们可能会遇到这样的需求

  • 筛选出年龄大于 20 的用户
  • 将数据集分组,男的一组,女的一组
  • 将数据集分组,找出男组中年龄最大用户,女组年龄最大用户
  • 对数据集进行分组求和,分组求平局值
  • 对数据集排序
  • 对数据集转化为别的类型
  • 对数据集断言,年龄有没有大于 25 的
  • 对数据集进行去重
  • 等等

以上操作是不是和操作 mysql 差不多,Stream 的引入将简化我们的操作,并行 Stream 还可以对大数据集分片计算,最终合并,提高效率。听听是不是就觉得有点意思。

创建 Stream 流

  • List.Stream 创建流
    @Test
    public void run6() {
        final List<String> strings = Arrays.asList("1", "2", "3", "4");
        // 比较常见的一种
        final Stream<String> stream = strings.stream();
    }
  • Stream.of 创建 Stream 流
    @Test
    public void run1() {
        final Stream<String> stream = Stream.of("陈平安", "宁姚");
    }
  • Stream.iterate 生成 Stream 流
// 流是无限的,所以要结合 limit(n) 来限制个数,否则生成 Long.MAX_VALUE 个
Stream.iterate(2, n -> n + 2).limit(5)
  • Stream.generate 也是无限流
// 生成随机数,流是无限的,所以要结合 limit(n) 来限制个数,否则生成 Long.MAX_VALUE 个
Stream.generate(() -> Math.random()).limit(5);

Stream.filter 对数据集进行过滤

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO implements Serializable {

    private static final long serialVersionUID = 7240658208223103787L;

    private Integer id;

    private String username;

    private Integer age;

    private Gender gender;
}
   private List<UserDTO> userDTOS;

    @Before
    public void before() {
        userDTOS = new ArrayList<>(16);
        userDTOS.add(UserDTO.builder().age(18).id(1).username("陈平安").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(19).id(2).username("宁姚").gender(Gender.FEMALE).build());
        userDTOS.add(UserDTO.builder().age(21).id(4).username("周米粒").gender(Gender.FEMALE).build());
        userDTOS.add(UserDTO.builder().age(23).id(6).username("李宝瓶").gender(Gender.FEMALE).build());
        userDTOS.add(UserDTO.builder().age(22).id(5).username("阮秀").gender(Gender.FEMALE).build());
        userDTOS.add(UserDTO.builder().age(20).id(3).username("崔东山").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(30).id(7).username("齐静春").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(32).id(9).username("阿良").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(31).id(8).username("陈清都").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(33).id(10).username("左右").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(41).id(12).username("崔诚").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(42).id(13).username("李希圣").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(34).id(11).username("裴钱").gender(Gender.FEMALE).build());
        userDTOS.add(UserDTO.builder().age(44).id(15).username("崔瀺").gender(Gender.MALE).build());
        userDTOS.add(UserDTO.builder().age(43).id(14).username("文圣老爷").gender(Gender.MALE).build());
    }

以上是每个 api 会用到的一些数据

筛选出年龄大于 21 ,小于 30 的用户


List<UserDTO> collect = userDTOS.stream()
            .filter(userDTO -> userDTO.getAge() < 30)
            .filter(userDTO -> userDTO.getAge() > 21)
            .collect(Collectors.toList());

上述写法尽管没有错误,但是可以优化 filter ,减少迭代次数

Predicate<UserDTO> predicate= userDTO -> userDTO.getAge() < 30;
// and(与),or(或),negate(非)
Predicate<UserDTO> and = predicate.and(userDTO -> userDTO.getAge() > 21);
final List<UserDTO> collect = userDTOS.stream().filter(and).collect(Collectors.toList());
System.out.println(collect);

上述只是举个例子,也可以在一个断言中写所有的逻辑

Stream.map

// 将数据集中的每个项转换为另一个对象, UserDTO->UserBO

final List<UserBO> collect = userDTOS.stream()
.map(userDTO -> UserBO.builder().age(userDTO.getAge()).name(userDTO.getUsername()).build())
.collect(Collectors.toList());

Stream.sorted

将数据集中排序

    @Test
    public void sorted() {
        final List<UserDTO> collect = userDTOS.stream()
        // 根据 id 排序(升序),然后 reversed 取反,最终也就是降序
        .sorted(Comparator.comparing(UserDTO::getId).reversed())
        .collect(Collectors.toList());
    }

Stream.distinct 对数据集去重,根据 equals 去重,所以对象需要重写 equals方法

final Stream<String> a1 = Stream.of("a1", "a2", "a1", "a2", "a3", "a4");
a1.distinct().collect(Collectors.toList());

peek 对数据集处理,区别 forEach 就是他返回新的 Stream

        System.out.println(Stream.of("one", "two", "three", "four")
                .filter(e -> e.length() > 3)
                .peek(e -> System.out.println("Filtered value: " + e))
                .map(String::toUpperCase)
                .peek(e -> System.out.println("Mapped value: " + e))
                .collect(Collectors.toList()));

打印的是

Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
[THREE, FOUR]

Stream 操作中,有些 api 可以返回新的 Stream 例如 filter,peek,map, 这些中间操作(返回新的 Stream 流)的api会串起来执行,等到最终操作一起运算返回结果。

可以中间看看 reduce 和 collect

Stream.reduce

    @Test
    public void run1() {
        final Stream<String> a1 = Stream.of("a1", "a3", "a5");
        // t1 为每次累计的结果,第一次即为 aaa
        final String aaa = a1.reduce("aaa", (t1, t2) -> {
            System.out.println(t1);
            System.out.println(t2);

            StringJoiner stringJoiner = new StringJoiner("--");
            stringJoiner.add(t1).add(t2);
            return stringJoiner.toString();
        });
        // aaa--a1--a3--a5
        System.out.println(aaa);
    }

下面这个操作在并行流中会遇到,并行流实际是用的 ForkJoinPool ,并行流会使用这个线程池中的线程对数据集分片进行计算,然后再将分片结果合并

    @Test
    public void run2() {
        final Stream<Integer> limit = Stream.iterate(1, UnaryOperator.identity()).limit(100000);
        // BinaryOperator 合并操作,只在并行流中生效
        System.out.println(limit.parallel().reduce(2, (t1, t2) -> t1 + t2, BinaryOperator.maxBy((t1, t2) ->
                {
                    System.out.println(t2);
                    System.out.println(t1);
                    return t2 - t1;
                }
        )));
    }

这个操作可以看出来,使用 ForkJoin 进行数据处理,对数据进行分片处理,然后合并

    @Test
    public void run6() {
        final Stream<Integer> limit = Stream.iterate(1, UnaryOperator.identity()).limit(100);
        final Integer reduce = limit.parallel().reduce(0, (t1, t2) -> {
            System.out.println(t1);
            System.out.println(t2);
            return t1 + t2;
        });
        System.out.println(reduce);
    }

下面就是简单的对结果进行累加,累积的结果是第一个参数 left(left,right)

@Test
public void run6() {
    final Stream<Integer> limit = Stream.iterate(1, UnaryOperator.identity()).limit(100);
    final Integer reduce = limit.reduce(0, (t1, t2) -> {
        System.out.println(t1);
        System.out.println(t2);
        return t1 + t2;
    });
    System.out.println(reduce);
}

collect

  • 求平均值
userDTOS.stream().collect(Collectors.averagingInt(UserDTO::getId));
userDTOS.stream().collect(Collectors.averagingLong(UserDTO::getId));
userDTOS.stream().collect(Collectors.averagingDouble(UserDTO::getId));
  • 求其中最大值的项

Optional 也要了解下,这个挺好用的

@Test
public void max() {
    System.out.println(userDTOS.stream().max(Comparator.comparingInt(UserDTO::getId)));
    System.out.println(userDTOS.stream().collect(Collectors.maxBy(Comparator.comparingInt(UserDTO::getId))));
    System.out.println(userDTOS.stream().collect(Collectors.maxBy(Comparator.comparing(UserDTO::getId))));
    System.out.println(userDTOS.stream().collect(Collectors.maxBy((t1, t2) -> t1.getId() - t2.getId())));
}

先来分析下 <R, A> R collect(Collector<? super T, A, R> collector),有的时候看 Api 也能看昏头,因为你不知道返回值是什么,都是泛型。缩小范围,看 R ,既 Collector 中第三个泛型,看这个基本就知道最终操作是啥了

  • 有的时候我们会对 collector 操作的结果再做处理,可以按照下面这样处理
@Test
public void collectingAndThen() {
    // 先找出年龄最大的项,t2 为 Optional,然后对这个进行操作
    String collect = userDTOS.stream().collect(
            Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(UserDTO::getId)), t2 -> {
                return t2.get().getId() + "-最大 id";
            }));
    System.out.println(collect);

}
  • 使用 collect 进行计数
    @Test
    public void counting() {
        Long collect = userDTOS.stream().collect(Collectors.counting());
        System.out.println(collect);
        // 不适用 collect 进行计数
        System.out.println(userDTOS.stream().count());
    }
  • 使用 collect 进行分组

当对并行流分组是,使用 groupingByConcurrent 用法和 groupingBy 一样,只是返回的 Map 为 ConcurrentMap

    @Test
    public void groupingBy1() {
        Map<Gender, List<UserDTO>> collect = userDTOS.stream()
        // 依据性别分组
        .collect(Collectors.groupingBy(t1 -> t1.getGender()));
    }

有的时候呢,我们对年龄分组之后,还想再按照年龄继续分组

    @Test
    public void groupingBy2() {
        final Map<Gender, Map<Integer, List<UserDTO>>> collect =
        userDTOS.parallelStream()
        .collect(Collectors.groupingBy(t1 -> t1.getGender(), Collectors.groupingBy(t2 -> t2.getAge())));
        System.out.println(collect);
    }
  • 有的时候呢,我们只想知道帅哥中最大年龄,美女中最大年龄
@Test
public void groupingBy3() {
    final Map<Gender, Optional<Integer>> collect = userDTOS.stream()
            .collect(Collectors.groupingBy(UserDTO::getGender,
Collectors.mapping(UserDTO::getAge, Collectors.maxBy(Comparator.comparingInt(t1->t1.intValue())))));
    System.out.println(collect);
}
  • 我现在只想知道帅哥中最大年龄的是谁,最小年龄的是谁
   @Test
    public void groupingBy4() {
        final Map<Gender, Optional<UserDTO>> collect = userDTOS.stream()
.collect(Collectors.groupingBy(UserDTO::getGender,
        Collectors.mapping(Function.identity(), Collectors.maxBy(Comparator.comparingInt(UserDTO::getAge)))));
        System.out.println(collect);
    }

看了之后是不是感觉 Stream 是不是很强大啊

  • 可能会对字符串进行拼接的操作
    @Test
    public void joining2() {
        final Stream<String> a1 = Stream.of("a1", "a2", "a3", "a4", "a5");
        final String collect = a1.collect(Collectors.joining("-"));
        // a1-a2-a3-a4-a5
        System.out.println(collect);
    }
    @Test
    public void joining1() {
        final Stream<String> a1 = Stream.of("a1", "a2", "a3", "a4", "a5");
        final String collect = a1.collect(Collectors.joining());
        // a1a2a3a4a5
        System.out.println(collect);
    }
    @Test
    public void joining3() {
        final Stream<String> a1 = Stream.of("a1", "a2", "a3", "a4", "a5");
        final String collect = a1.collect(Collectors.joining("-", "前缀", "后缀"));
        // 前缀a1-a2-a3-a4-a5后缀
        System.out.println(collect);
    }

上述操作都是基本操作,有的时候我们需要对一个 bean 中的某个字段进行拼接,这种骚操作 collect 也能完成

    @Test
    public void run4() {
        final String collect = userDTOS.stream().collect(
        Collectors.mapping(UserDTO::getUsername, Collectors.joining("-")));
        System.out.println(collect);
    }

看到这里,你可能都忍不住要敲代码试试了, Stream 怎么是无比强大

  • 找到 Stream 中的最大或最小项
    @Test
    public void maxBy() {
        final Optional<UserDTO> collect = userDTOS.stream().collect(
        Collectors.maxBy(Comparator.comparing(UserDTO::getAge)));
        System.out.println(collect.get());
    }

    @Test
    public void minBy() {
        final Optional<UserDTO> collect = userDTOS.stream().collect(
        Collectors.minBy(Comparator.comparing(UserDTO::getAge)));
        System.out.println(collect.get());
    }
  • partitioningBy,类似分组,只是 key 是 boolean
    @Test
    public void partitioningBy() {
        final Map<Boolean, List<UserDTO>> collect = userDTOS.stream().collect(Collectors.partitioningBy(userDTO -> userDTO.getGender().equals(Gender.MALE)));
    }

    @Test
    public void partitioningBy2() {
        final Map<Boolean, Optional<UserDTO>> collect = userDTOS.stream().collect(Collectors.partitioningBy(userDTO -> userDTO.getGender().equals(Gender.MALE), Collectors.maxBy(Comparator.comparing(UserDTO::getAge))));
        
        final Map<Boolean, Optional<UserDTO>> collect2 =
        userDTOS.stream().collect(
        Collectors.partitioningBy(userDTO -> userDTO.getGender().equals(Gender.MALE), Collectors.maxBy(Comparator.comparingInt(UserDTO::getAge))));

    }
  • 有的时候我们还会对数据进行求和

平均数,最大值,最小值 都给你计算了,舒服

  @Test
    public void run6() {
        IntSummaryStatistics collect = userDTOS.stream().collect(
        Collectors.summarizingInt(UserDTO::getId));
        final double average = collect.getAverage();
        System.out.println(average);
        System.out.println(collect.getCount());
        System.out.println(collect.getMax());
        System.out.println(collect.getMin());
        System.out.println(collect.getSum());
    }
  • 求数据集中年龄最大的项,Collectors.reducing
    @Test
    public void reduce() {
        final Optional<UserDTO> collect = userDTOS.stream().collect(
        Collectors.reducing(BinaryOperator.maxBy(Comparator.comparingInt(UserDTO::getId))));
        System.out.println(collect.get());
    }

至此 Stream 流大致过完,墙裂推荐,花一周时间好好研究下,能提升开发效率,