Lambda与Stream✨让代码简洁高效的七大原则
在现代Java编程实践中,Lambda表达式和Stream API已成为提高代码可读性和执行效率的重要工具
本文基于 Effective Java Lambda与Stream章节汇总出7条相关原则(文末附案例链接)
Lambda优于匿名内部类
JDK8中只存在一个抽象方法的接口称为函数接口,并使用注解@FunctionalInterface
标识
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); //... }
在此之前,在方法中实现这种接口通常会使用匿名内部类
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6); list.sort(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); } }); //[6, 5, 4, 3, 2, 1] System.out.println(list);
JDK 8时可以使用Lambda表达式来实现函数接口
list.sort((o1, o2) -> o2.compareTo(o1));
对于这种简单易懂的函数接口使用Lambda表达式更容易实现、简洁 Lambda表达式优于匿名内部类
善用方法引用
JDK 8 还提供方法引用,一般情况下方法引用会比Lambda表达式还简洁
比如上面那个倒转排序可以使用方法引用来实现
list.sort((o1, o2) -> o2.compareTo(o1)); //方法引用 list.sort(Comparator.reverseOrder());
实际上会调用其内部的方法
public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() { return Collections.reverseOrder(); }
但是在某些场景下,使用方法引用反而会太过简洁导致看不懂,因此哪种方式易懂就优先使用哪种方式
坚持使用标准函数接口
JDK 8 java.util.function提供几十种标准函数接口,其只需要记住基础函数接口,其他都是变体
Predicate 谓词 返回布尔类型 判断给定输入参数是否满足某种条件
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
// 创建一个谓词,检查数字是否是偶数 Predicate<Integer> isEven = n -> n % 2 == 0; // 使用谓词过滤列表 numbers.stream() .filter(isEven) .forEach(System.out::println);
Supplier 不需要传参提供一个结果
@FunctionalInterface public interface Supplier<T> { T get(); }
// 创建一个Supplier,每次调用get方法都会生成一个新的随机数 Supplier<Integer> randomIntSupplier = () -> (int) (Math.random() * 100); // 获取并打印5个随机数 for (int i = 0; i < 5; i++) { System.out.println(randomIntSupplier.get()); }
Consumer 消费 传入参数不返回
@FunctionalInterface public interface Consumer<T> { void accept(T t); }
List<String> greetings = Arrays.asList("Hello", "World", "!"); // 创建一个consumer,用于打印接收到的消息 Consumer<String> printer = message -> System.out.println(message); // 对列表中的每个元素应用consumer greetings.forEach(printer);
Function 函数 传入T类型响应另一个R类型
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
List<String> names = Arrays.asList("John Doe", "Jane Smith", "Alice Johnson"); // 创建一个Function,将人名转换为姓氏 Function<String, String> getLastName = name -> name.split(" ")[1]; // 将所有名字转换为姓氏 List<String> lastNames = names.stream() .map(getLastName) .collect(Collectors.toList()); // 输出:[Doe, Smith, Johnson] System.out.println(lastNames);
当我们设计时优先使用标准函数接口,标准函数接口无法满足我们的需求时再自定义函数接口
(记得使用注解@FunctionalInterface)
谨慎使用Stream
JDK8提供流式处理,先将集合转换为流,再通过多重管道对流进行处理,最后调用终止操作结束
Stream还是链式编程,给编写代码带来极大简便
并不是所有处理都使用Stream会更好,如果业务中流程复杂、滥用Stream会降低代码的可读性、维护性
这种情况下Stream配合for循环迭代可能会更好
最好避免使用Stream来处理char类型,chars返回的实际上是IntStream
//3375633756303402151831471311692515133756 "菜菜的后端私房菜".chars().forEach(System.out::print); //菜菜的后端私房菜 "菜菜的后端私房菜".chars().forEach(x -> System.out.print((char) x));
优先选择Stream中无副作用的函数
副作用的函数指的是处理数据的同时改变原集合,比如 foreach
无副作用的函数则在处理过程中不影响原集合,比如filter、map、sorted
List<String> list = Arrays.asList("aaa", "b", "cc"); List<Integer> lengths = list.stream() .map(String::length) .sorted() .collect(Collectors.toList());
使用完无副作用函数后再使用收集器转换为理想的容器
Stream优先用Collection为返回类型
使用Stream处理时,有些情况后续需要使用迭代,有些情况后续需要使用Stream
为了兼容多种情况,返回时应该优先使用Collection类型
比如Collection、Set、List等,它们都能转换为Stream或迭代器
List<String> lists = Arrays.asList(" 菜菜", " caicai ", "小菜 ", "", " "); Stream<String> stream = lists.stream() .filter(s -> !Objects.equals("", s.trim())) .map(s -> s.trim()); //Collection List Set Collection<String> collect = stream.collect(Collectors.toCollection(ArrayList::new)); //获取迭代 Iterator<String> iterator = collect.iterator(); //获取Stream Stream<String> collectionStream = collect.stream(); while (iterator.hasNext()) { System.out.println(iterator.next()); } collectionStream.forEach(System.out::println);
谨慎使用Stream并行
多线程并行能够提高处理程序的速度,同时不熟悉并行时误操作也会带来数据一致性问题
并行最好使用在互不干扰的情况,避免出现数据不一致
比如数组长度为100,使用十个线程,每个线程负责处理十个长度的区间,并行处理时互不影响
比如ArrayList、HashMap等都是直接/间接基于数组实现的,使用并行加快速度
使用parallel()
开启并行
static long piParallel(long n) { return LongStream.rangeClosed(2, n) .parallel() .mapToObj(BigInteger::valueOf) .filter(i -> i.isProbablePrime(50)) .count(); }
测试并行提升速度
long start = System.currentTimeMillis(); piParallel(10_000_000); // 2150 System.out.println(System.currentTimeMillis() - start); start = System.currentTimeMillis(); pi(10_000_000); // 16363 System.out.println(System.currentTimeMillis() - start);
总结
函数接口只存在一个抽象方法,并用注解@FunctionalInterface标识,可以使用Lambda表达式实现
简单易懂的函数接口使用Lambda实现简洁,优于匿名内部类
方法引用比Lambda更简洁,但某些情况下太简介会降低可读性,哪种方式更易提示代码可读性选择哪种
java.util.function提供标准函数接口,当设计组件时优先选择标准函数接口,不满足需求再自定义
Stream流式处理能够给编写代码带来极大简便,但业务代码流程复杂,滥用Stream会降低代码可读性、维护性,最好结合Stream和迭代的方式写出可读性、可维护性高的代码
避免使用Stream处理char类型,会转化为Int类型处理
在Stream中优先使用不影响原集合的方法,如filter、map、sorted等,等处理完数据后再通过收集器转化为对应容器
在某些场景下,后续需要使用Stream或迭代,Collection都兼容,优先返回Collection、List、Set
并行能够加快程序运行速度,当可能带来线程不安全的一致性问题
使用并行最好互不干扰,比如数组实现的容器(ArrayList、HashMap),线程各自负责自己的区间
最后(不要白嫖,一键三连求求拉~)
本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜