Lambda 表达式
众所周知的 Lambda 表达式。它是推动 Java 8 发布的最重要新特性。是继泛型(Generics
)和注解(Annotation
)以来最大的变化。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程。
Lambda 表达式是一个匿名函数,java 8 允许把函数作为参数传递进方法中。
语法格式
(parameters) -> expression 或 (parameters) ->{ statements; }Copy to clipboardErrorCopied
Lambda 实战
我们用常用的实例来感受 Lambda 带来的便利
替代匿名内部类
过去给方法传动态参数的唯一方法是使用内部类。比如
1.Runnable
接口
new Thread(new Runnable() { @Override public void run() { System.out.println("The runable now is using!"); } }).start(); //用lambda new Thread(() -> System.out.println("It's a lambda function!")).start();Copy to clipboardErrorCopied
2.Comparator
接口
List<Integer> strings = Arrays.asList(1, 2, 3); Collections.sort(strings, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2;} }); //Lambda Collections.sort(strings, (Integer o1, Integer o2) -> o1 - o2); //分解开 Comparator<Integer> comperator = (Integer o1, Integer o2) -> o1 - o2; Collections.sort(strings, comperator);Copy to clipboardErrorCopied
3.Listener
接口
JButton button = new JButton(); button.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { e.getItem(); } }); //lambda button.addItemListener(e -> e.getItem());Copy to clipboardErrorCopied
4.自定义接口
上面的 3 个例子是我们在开发过程中最常见的,从中也能体会到 Lambda 带来的便捷与清爽。它只保留实际用到的代码,把无用代码全部省略。那它对接口有没有要求呢?我们发现这些匿名内部类只重写了接口的一个方法,当然也只有一个方法须要重写。这就是我们上文提到的函数式接口,也就是说只要方法的参数是函数式接口都可以用 Lambda 表达式。
@FunctionalInterface public interface Comparator<T>{} @FunctionalInterface public interface Runnable{}Copy to clipboardErrorCopied
我们自定义一个函数式接口
@FunctionalInterface public interface LambdaInterface { void f(); } //使用 public class LambdaClass { public static void forEg() { lambdaInterfaceDemo(()-> System.out.println("自定义函数式接口")); } //函数式接口参数 static void lambdaInterfaceDemo(LambdaInterface i){ System.out.println(i); } }Copy to clipboardErrorCopied
集合迭代
void lamndaFor() { List<String> strings = Arrays.asList("1", "2", "3"); //传统foreach for (String s : strings) { System.out.println(s); } //Lambda foreach strings.forEach((s) -> System.out.println(s)); //or strings.forEach(System.out::println); //map Map<Integer, String> map = new HashMap<>(); map.forEach((k,v)->System.out.println(v)); }Copy to clipboardErrorCopied
方法的引用
Java 8 允许使用 ::
关键字来传递方法或者构造函数引用,无论如何,表达式返回的类型必须是 functional-interface。
public class LambdaClassSuper { LambdaInterface sf(){ return null; } } public class LambdaClass extends LambdaClassSuper { public static LambdaInterface staticF() { return null; } public LambdaInterface f() { return null; } void show() { //1.调用静态函数,返回类型必须是functional-interface LambdaInterface t = LambdaClass::staticF; //2.实例方法调用 LambdaClass lambdaClass = new LambdaClass(); LambdaInterface lambdaInterface = lambdaClass::f; //3.超类上的方法调用 LambdaInterface superf = super::sf; //4. 构造方法调用 LambdaInterface tt = LambdaClassSuper::new; } }Copy to clipboardErrorCopied
访问变量
int i = 0; Collections.sort(strings, (Integer o1, Integer o2) -> o1 - i); //i =3;Copy to clipboardErrorCopied
lambda 表达式可以引用外边变量,但是该变量默认拥有 final 属性,不能被修改,如果修改,编译时就报错。
Stream
java 新增了 java.util.stream
包,它和之前的流大同小异。之前接触最多的是资源流,比如java.io.FileInputStream
,通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何CRUD。
Stream
依然不存储数据,不同的是它可以检索(Retrieve)和逻辑处理集合数据、包括筛选、排序、统计、计数等。可以想象成是 Sql 语句。
它的源数据可以是 Collection
、Array
等。由于它的方法参数都是函数式接口类型,所以一般和 Lambda 配合使用。
流类型
- stream 串行流
- parallelStream 并行流,可多线程执行
常用方法
接下来我们看java.util.stream.Stream
常用方法
/** * 返回一个串行流 */ default Stream<E> stream() /** * 返回一个并行流 */ default Stream<E> parallelStream() /** * 返回T的流 */ public static<T> Stream<T> of(T t) /** * 返回其元素是指定值的顺序流。 */ public static<T> Stream<T> of(T... values) { return Arrays.stream(values); } /** * 过滤,返回由与给定predicate匹配的该流的元素组成的流 */ Stream<T> filter(Predicate<? super T> predicate); /** * 此流的所有元素是否与提供的predicate匹配。 */ boolean allMatch(Predicate<? super T> predicate) /** * 此流任意元素是否有与提供的predicate匹配。 */ boolean anyMatch(Predicate<? super T> predicate); /** * 返回一个 Stream的构建器。 */ public static<T> Builder<T> builder(); /** * 使用 Collector对此流的元素进行归纳 */ <R, A> R collect(Collector<? super T, A, R> collector); /** * 返回此流中的元素数。 */ long count(); /** * 返回由该流的不同元素(根据 Object.equals(Object) )组成的流。 */ Stream<T> distinct(); /** * 遍历 */ void forEach(Consumer<? super T> action); /** * 用于获取指定数量的流,截短长度不能超过 maxSize 。 */ Stream<T> limit(long maxSize); /** * 用于映射每个元素到对应的结果 */ <R> Stream<R> map(Function<? super T, ? extends R> mapper); /** * 根据提供的 Comparator进行排序。 */ Stream<T> sorted(Comparator<? super T> comparator); /** * 在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。 */ Stream<T> skip(long n); /** * 返回一个包含此流的元素的数组。 */ Object[] toArray(); /** * 使用提供的 generator函数返回一个包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。 */ <A> A[] toArray(IntFunction<A[]> generator); /** * 合并流 */ public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)Copy to clipboardErrorCopied
实战
本文列出 Stream
具有代表性的方法之使用,更多的使用方法还是要看 Api。
@Test public void test() { List<String> strings = Arrays.asList("abc", "def", "gkh", "abc"); //返回符合条件的stream Stream<String> stringStream = strings.stream().filter(s -> "abc".equals(s)); //计算流符合条件的流的数量 long count = stringStream.count(); //forEach遍历->打印元素 strings.stream().forEach(System.out::println); //limit 获取到1个元素的stream Stream<String> limit = strings.stream().limit(1); //toArray 比如我们想看这个limitStream里面是什么,比如转换成String[],比如循环 String[] array = limit.toArray(String[]::new); //map 对每个元素进行操作返回新流 Stream<String> map = strings.stream().map(s -> s + "22"); //sorted 排序并打印 strings.stream().sorted().forEach(System.out::println); //Collectors collect 把abc放入容器中 List<String> collect = strings.stream().filter(string -> "abc".equals(string)).collect(Collectors.toList()); //把list转为string,各元素用,号隔开 String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(",")); //对数组的统计,比如用 List<Integer> number = Arrays.asList(1, 2, 5, 4); IntSummaryStatistics statistics = number.stream().mapToInt((x) -> x).summaryStatistics(); System.out.println("列表中最大的数 : "+statistics.getMax()); System.out.println("列表中最小的数 : "+statistics.getMin()); System.out.println("平均数 : "+statistics.getAverage()); System.out.println("所有数之和 : "+statistics.getSum()); //concat 合并流 List<String> strings2 = Arrays.asList("xyz", "jqx"); Stream.concat(strings2.stream(),strings.stream()).count(); //注意 一个Stream只能操作一次,不能断开,否则会报错。 Stream stream = strings.stream(); //第一次使用 stream.limit(2); //第二次使用 stream.forEach(System.out::println); //报错 java.lang.IllegalStateException: stream has already been operated upon or closed //但是可以这样, 连续使用 stream.limit(2).forEach(System.out::println); }Copy to clipboardErrorCopied
延迟执行
在执行返回 Stream
的方法时,并不立刻执行,而是等返回一个非 Stream
的方法后才执行。因为拿到 Stream
并不能直接用,而是需要处理成一个常规类型。这里的 Stream
可以想象成是二进制流(2 个完全不一样的东东),拿到也看不懂。
我们下面分解一下 filter
方法。
@Test public void laziness(){ List<String> strings = Arrays.asList("abc", "def", "gkh", "abc"); Stream<Integer> stream = strings.stream().filter(new Predicate() { @Override public boolean test(Object o) { System.out.println("Predicate.test 执行"); return true; } }); System.out.println("count 执行"); stream.count(); } /*-------执行结果--------*/ count 执行 Predicate.test 执行 Predicate.test 执行 Predicate.test 执行 Predicate.test 执行Copy to clipboardErrorCopied
按执行顺序应该是先打印 4 次「Predicate.test
执行」,再打印「count
执行」。实际结果恰恰相反。说明 filter 中的方法并没有立刻执行,而是等调用count()
方法后才执行。
上面都是串行 Stream
的实例。并行 parallelStream
在使用方法上和串行一样。主要区别是 parallelStream
可多线程执行,是基于 ForkJoin 框架实现的,有时间大家可以了解一下 ForkJoin
框架和 ForkJoinPool
。这里可以简单的理解它是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题。下面我们通过代码来体验一下并行流的多线程执行。
@Test public void parallelStreamTest(){ List<Integer> numbers = Arrays.asList(1, 2, 5, 4); numbers.parallelStream() .forEach(num->System.out.println(Thread.currentThread().getName()+">>"+num)); } //执行结果 main>>5 ForkJoinPool.commonPool-worker-2>>4 ForkJoinPool.commonPool-worker-11>>1 ForkJoinPool.commonPool-worker-9>>2Copy to clipboardErrorCopied
从结果中我们看到,for-each 用到的是多线程。
小结
从源码和实例中我们可以总结出一些 stream 的特点
- 通过简单的链式编程,使得它可以方便地对遍历处理后的数据进行再处理。
- 方法参数都是函数式接口类型
- 一个 Stream 只能操作一次,操作完就关闭了,继续使用这个 stream 会报错。
- Stream 不保存数据,不改变数据源