Java 框架体系读懂函数式接口、Lambda表达式、Stream

Java 8 中引入很多有意思的新特性,本篇文章我们来聊聊其中三个比较重要的特性:函数式接口、Lambda表达式、Stream流,我们分别从示例用法、底层原理、最佳实践三个方面来了解这些特性。

版本

• JDK 8

函数式接口

定义

• 函数式接口是 Java 8 引入的一种接口,它只包含一个抽象方法。函数式接口的存在是为了支持 Lambda 表达式,使得我们可以使用更简洁、更灵活的方式编写匿名函数。

@FunctionalInterface interface Calculator { int add(int a, int b); default int subtract(int a, int b) { return a - b; } static int multiply(int a, int b) { return a * b; } }

• @FunctionalInterface 注解是可选的,推荐使用。该注解会让编译器强制检查接口是否满足函数式接口定义。

特点

• 只能有一个抽象方法,可以有参数和返回值。

• 可以包含多个默认方法(使用 default 关键字)和静态方法(使用 static 关键字),不违反函数式接口的定义。

说明: 默认方法和静态方法在 Java 8 中引入,目的是在引入新功能的同时不改变已有实现。 从而实现接口的的逐步演进,不需要同时修改所有实现类。

使用

@FunctionalInterface interface Calculator { int add(int a, int b); default int subtract(int a, int b) { return a - b; } static int multiply(int a, int b) { return a * b; } } public class TestMain { public static void main(String[] args) { Calculator addCalculator = (a, b) -> a + b; System.out.println(addCalculator.add(1, 2)); System.out.println(addCalculator.subtract(1, 2)); } }

Lambda表达式

• Lambda 表达式是一种用于传递匿名函数的简洁语法。它提供了一种更紧凑的方式来表示可以传递给方法的代码块。Lambda 表达式主要用于函数式接口,可以看作是对函数式接口的一个实现。

Calculator addCalculator = (a, b) -> a + b;

主要场景

• 简化匿名内部类的写法,但无法简化所有匿名内部类,只能简化满足函数式接口的匿名内部类。

用法

无参写法

• 实现创建一个简单的线程。

@FunctionalInterface public interface Runnable { /** * When an object implementing interface Runnable is used * to create a thread, starting the thread causes the object's * run method to be called in that separately executing * thread. *

* The general contract of the method run is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); } // JDK7 匿名内部类写法 new Thread(new Runnable() {// 接口名 @Override public void run() {// 方法名 System.out.println("Thread run()"); } }).start(); // JDK8 Lambda表达式代码块写法 new Thread( () -> System.out.print("Thread run()") ).start();

有参写法

• 实现根据列表中字符串元素长度进行排序。

@FunctionalInterface public interface Comparator { int compare(T o1, T o2); } // JDK7 匿名内部类写法 List list = Arrays.asList("my", "name", "is", "lorin"); list.sort(new Comparator() { @Override public int compare(String s1, String s2) { if (s1 == null) return -1; if (s2 == null) return 1; return s1.length() - s2.length(); } }); // JDK8 Lambda表达式写法 List list = Arrays.asList("my", "name", "is", "lorin"); list.sort((s1, s2) -> {// 省略参数表的类型 if (s1 == null) return -1; if (s2 == null) return 1; return s1.length() - s2.length(); });

Lambda 表达式的基础:函数式接口 + 类型推断

• Lambda 表达式除了上文中提到的函数式接口,还有一个比较重要的特性来支持 Lambda 表达式简洁的写法,即类型推断:指编译器根据上下文信息推断变量的类型,而不需要显式地指定类型。类型推断的引入是为了简化代码,并提高代码的可读性和可维护性。

CustomerInterface action = (Integer t) -> { System.out.println(this); return t + 1; }; // 使用类型推断 CustomerInterface action1 = t -> { System.out.println(this); return t + 1; };

自定义函数接口使用 Lambda 表达式

• 首先定义一个函数接口,函数作用是对传入的元素进行操作,最后返回操作后的元素。

// 自定义函数接口 @FunctionalInterface public interface CustomerInterface { T operate(T t); }

• 自定义的 MyStream 类来使用自定义的函数接口。

class MyStream { private final List list; MyStream(List list) { this.list = list; } public void customerForEach(CustomerInterface action) { Objects.requireNonNull(action); list.replaceAll(action::operate); } }

• 使用自定义的 MyStream 类实现对每一个元素的 +1 操作。

public class TestMain { public static void main(String[] args) { List arr = Arrays.asList(1, 2, 3, 4); MyStream myStream = new MyStream<>(arr); myStream.customerForEach(t -> t + 1); System.out.println(arr); } } // 输出结果 [2, 3, 4, 5]

底层实现

• 上面我们回顾了 JDK7 和 JDK8 对匿名内部类的写法,我们发现 JDK8 中的实现更加简洁了,但实际上不仅仅语法上更加简洁,即不是纯粹的语法糖,底层实现也发生了一些变化,下面我们一起来看一下。

JDK7

• 由于 JDK7 并不支持函数式接口、Lambda表达式,所以我们先对代码做一些简单的改造:

public interface CustomerInterface { T operate(T t); } class MyStream { private final List list; MyStream(List list) { this.list = list; } public void customerForEach(CustomerInterface action) { Objects.requireNonNull(action); for (int i = 0; i < list.size(); i++) { list.set(i, action.operate(list.get(i))); } } } public class TestMain { public static void main(String[] args) { List arr = Arrays.asList(1, 2, 3, 4); MyStream myStream = new MyStream<>(arr); myStream.customerForEach(new CustomerInterface() { @Override public Integer operate(Integer integer) { return integer + 1; } }); System.out.println(arr); } }

• 使用 javap 分析字节码:

javap -c -p .\TestMain.class Compiled from "TestMain.java" public class test.TestMain { public test.TestMain(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_4 1: anewarray #2 // class java/lang/Integer 4: dup 5: iconst_0 6: iconst_1 7: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 10: aastore 11: dup 12: iconst_1 13: iconst_2 14: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 17: aastore 18: dup 19: iconst_2 20: iconst_3 21: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 24: aastore 25: dup 26: iconst_3 27: iconst_4 28: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 31: aastore 32: invokestatic #4 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List; 35: astore_1 36: new #5 // class test/MyStream 39: dup 40: aload_1 41: invokespecial #6 // Method test/MyStream."":(Ljava/util/List;)V 44: astore_2 45: aload_2 46: new #7 // class test/TestMain$1 创建匿名内部类 49: dup 50: invokespecial #8 // Method test/TestMain$1."":()V 53: invokevirtual #9 // Method test/MyStream.customerForEach:(Ltest/CustomerInterface;)V 56: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 59: aload_1 60: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 63: return }

• 从上面 46 行我们可以看出,JDK7 创建了真实的的匿名内部类。

JDK8

• JDK8 我们以上述 自定义函数接口使用 Lambda 表达式 为例:

• 使用 javap 分析字节码可以发现,Lambda 表达式 被封装为一个内部的私有方法并通过 InvokeDynamic 调用,而不是像 JDK7 那样创建一个真实的匿名内部类。

javap -c -p .\TestMain.class Compiled from "TestMain.java" public class test.TestMain { public test.TestMain(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_4 1: anewarray #2 // class java/lang/Integer 4: dup 5: iconst_0 6: iconst_1 7: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 10: aastore 11: dup 12: iconst_1 13: iconst_2 14: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 17: aastore 18: dup 19: iconst_2 20: iconst_3 21: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 24: aastore 25: dup 40: aload_1 41: invokespecial #6 // Method test/MyStream."":(Ljava/util/List;)V 44: astore_2 45: aload_2 46: invokedynamic #7, 0 // InvokeDynamic #0:operate:()Ltest/CustomerInterface; InvokeDynamic 调用 51: invokevirtual #8 // Method test/MyStream.customerForEach:(Ltest/CustomerInterface;)V 54: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 57: aload_1 58: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 61: return private static java.lang.Integer lambda$main$0(java.lang.Integer); // lambda 表达式被封装为内部方法 Code: 0: aload_0 1: invokevirtual #11 // Method java/lang/Integer.intValue:()I 4: iconst_1 5: iadd 6: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 9: areturn }

this 的含义

• 从上面我们可以知道 JDK7 和 JDK8 对匿名内部类不仅写法上不一致,底层原理也不相同。因此,如果我们在两种写法种使用 this 关键字,两者是一样的?先说答案:不一样,JDK7 的 this 指向创建的匿名内部内,而 JDK8 中Lambda表达式并不会创建真实存在的类,指向的是当前类。

• 下面我们结合实际案例来看一下:

JDK7

CustomerInterface action = new CustomerInterface() { @Override public Integer operate(Integer integer) { System.out.println(this); return integer + 1; } }; CustomerInterface action1 = new CustomerInterface() { @Override public Integer operate(Integer integer) { System.out.println(this); return integer + 1; } }; System.out.println(action.operate(2)); System.out.println(action1.operate(2)); // 输出 test.TestMain$1@8939ec3 3 test.TestMain$2@456bf9ce 3

• 可以看到两个 this 输出地址不同,分别指向自身的匿名内部类对象。

JDK8

public class TestMain { CustomerInterface action = t -> { System.out.println(this); return t + 1; }; CustomerInterface action1 = t -> { System.out.println(this); return t + 1; }; public static void main(String[] args) { TestMain testMain = new TestMain(); System.out.println(testMain.action.operate(2)); System.out.println(testMain.action1.operate(2)); } } // 输出 test.TestMain@1d81eb93 3 test.TestMain@1d81eb93 3

• 可以看到,两个 this 都指向同一个 testMain 对象,因为我们从前文我们可以知道 JDK8 中 Lambda 表达式 被封装为一个内部的私有方法并通过 InvokeDynamic 调用,而不是创建一个真实的匿名内部类。

Stream

• Stream 是一种用于处理集合数据的高级抽象,它允许我们以声明式的方式对集合进行操作。

• 函数式接口提供了Lambda表达式的类型,Lambda表达式提供了一种简洁的语法来定义匿名内部类,而 Stream 提供了一种声明式的方式来处理集合数据,并与Lambda表达式无缝结合,共同支持函数式编程在Java中的应用。

特点

• Stream 不存储数据,按照特定的规则进行计算,最后返回计算结果。

• Stream 不改变源数据源,而返回一个新的数据源。

• Stream 是惰性计算,只有调用终端操作时,中间操作才会执行。

操作

Stream 流创建

• Stream 流支持并行流和串行流两种方式,串行流每个元素按照顺序依次处理,并行流会将流中元素拆分为多个子任务进行处理,最后再合并结果,从而提高处理效率。

List list = Arrays.asList("11", "2222", "333333"); // 串行流 list.stream().map(String::toString).collect(Collectors.toList()); // 并行流 list.parallelStream().map(String::toString).collect(Collectors.toList()); list.stream().parallel().map(String::toString).collect(Collectors.toList());

中间操作和终端操

中间操作

• 只会记录操作不会立即执行,中间操作可以细分为:无状态 Stateless 和 有状态 Stateful 两种。

无状态 Stateless

• 指元素不受其它元素影响,可以继续往下执行,比如 filter() map() mapToInt() 等。

filter

• 用于筛选符合条件的元素,下一步只会拿到符合条件的元素。

Liststrings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl"); // 获取空字符串的数量 long count = strings.stream().filter(string -> string.isEmpty()).count();

map

• 用于将一个流中的元素通过指定的映射函数转换为另一个流。返回类型必须是传入类型或传入类型的子类型。

List numbers = Arrays.asList(1, 2, 3, 4, 5); // 使用 map 方法将列表中的每个元素乘以2 List doubledNumbers = numbers.stream() .map(n -> n * 2) .collect(Collectors.toList());

mapToInt() mapToLong() 等

• mapToInt() 方法用于将流中的元素映射为 int 类型的流。IntStream 是针对 int 类型数据进行优化的特殊流,提供了更高效的操作和更方便的处理方式。当处理基本类型 int 数据时,推荐使用 IntStream,可以提高代码的性能和可读性。

• mapToLong() 方法用于将流中的元素映射为 long 类型的流。

// 整数列表 Long[] numbers = {1, 2, 3, 4, 5}; // 使用 mapToLong() 方法将每个整数乘以自身,并收集到一个 LongStream 流中 LongStream squares = Arrays.stream(numbers).mapToLong(t -> t * t); squares.sum();

flatMap() flatMapToInt() 等

• flatMap()用于将流中的每个元素映射为一个流,然后将所有映射得到的流合并成一个新的流。

• flatMapToInt() 和 flatMap() 的区别在于返回的流为 IntStream。

// 字符串列表 List words = Arrays.asList("Java is fun", "Stream API is powerful", "FlatMap is useful"); // 使用 flatMap() 提取每个字符串中的单词,并放入一个新的流中 Stream wordStream = words.stream() .flatMap(str -> Arrays.stream(str.split("\\s+"))); // 打印流中的每个单词 wordStream.forEach(System.out::println); // 输出 Java is fun Stream API is powerful FlatMap is useful

peek

• 用于在流的每个元素上执行指定的操作,同时保留流中的元素。peek() 方法不会改变流中的元素,而是提供一种查看每个元素的机会,通常用于调试、日志记录或记录流中的中间状态。

// 整数列表 List numbers = Arrays.asList(1, 2, 3, 4, 5); // 使用 peek() 打印每个元素,并将元素乘以2,然后收集到一个新的列表中 List doubledNumbers = numbers.stream() .peek(num -> System.out.println("Original: " + num)) .map(num -> num * 2) .peek(doubledNum -> System.out.println("Doubled: " + doubledNum)) .collect(Collectors.toList()); // 打印新列表中的元素 System.out.println("Doubled Numbers: " + doubledNumbers);

有状态 Stateful

• 指元素受到其它元素影响,比如 distinct() 去重,需要处理完所有元素才能往下执行。

distinct

• 用于去除流中重复的元素,返回一个去重后的新流。distinct() 方法根据元素的 equals() 方法来判断是否重复,因此流中的元素必须实现了 equals() 方法以确保正确的去重。

// 字符串列表 List words = Arrays.asList("hello", "world", "hello", "java", "world"); // 使用 distinct() 方法获取不重复的单词,并收集到一个新的列表中 List uniqueWords = words.stream() .distinct() .collect(Collectors.toList()); // 打印不重复的单词列表 System.out.println("Unique Words: " + uniqueWords);

limit

• 用于限制流中元素的数量,返回一个包含了指定数量元素的新流。limit() 方法通常用于在处理大型数据集时,限制处理的数据量,以提高性能或减少资源消耗。需要注意的,返回的元素不一定是前三个。

// 整数列表 List numbers = Arrays.asList(1, 2, 3, 4, 5); // 使用 limit() 方法获取前3个元素,并收集到一个新的列表中 List limitedNumbers = numbers.stream() .limit(3) .collect(Collectors.toList()); // 打印前3个元素 System.out.println("Limited Numbers: " + limitedNumbers);

终端操作

• 调用终端操作计算会立即开始执行,终端操作可以细分为:非短路操作 和 短路操作。

非短路操作

• 非短路操作:需要处理完所有元素才可以拿到结果,比如 forEach() forEachOrdered()。

collect

• 将流中的元素收集到一个集合或者其他数据结构中。下面是一些常见的用法:

// 将流中的元素收集到一个列表中: List list = stream.collect(Collectors.toList()); // 将流中的元素收集到一个集合中: Set set = stream.collect(Collectors.toSet()); // 将流中的元素收集到一个指定类型的集合中: ArrayList arrayList = stream.collect(Collectors.toCollection(ArrayList::new)); // 将流中的元素收集到一个字符串中,使用指定的分隔符连接: String result = stream.collect(Collectors.joining(", ")); // 将流中的元素收集到一个 Map 中,根据指定的键值对: Map map = stream.collect(Collectors.toMap(String::length, Function.identity())); // 对流中的元素进行分组: Map> groupedMap = stream.collect(Collectors.groupingBy(String::length)); // 对流中的元素进行分区: Map> partitionedMap = stream.collect(Collectors.partitioningBy(s -> s.length() > 3)); // 对流中的元素进行统计: IntSummaryStatistics statistics = stream.collect(Collectors.summarizingInt(String::length));

reduce

• 用于将流中的元素组合成一个值。

• 灵活性:reduce() 方法提供了灵活的参数选项,可以根据需求选择不同的重载形式,包括指定初始值、选择累加器函数和组合器函数等,使得它可以适用于各种场景。

• 统一操作:reduce() 方法提供了一种统一的方式来对流中的元素进行组合操作,不论是求和、求积、字符串拼接还是其他任何类型的组合操作,都可以使用 reduce() 方法来实现,这样可以减少代码重复,提高代码的可读性和可维护性。

• 并行流支持:在并行流中,reduce() 方法可以更高效地利用多核处理器,通过并行化操作来提高性能。使用合适的组合器函数,可以在并行流中正确地合并部分结果,从而实现更高效的并行计算。而 sum() 函数是串行的。

// 将流中的元素累加求和: List numbers = Arrays.asList(1, 2, 3, 4, 5); Optional sum = numbers.stream().reduce((a, b) -> a + b); System.out.println("Sum: " + sum.orElse(0)); // 输出 15 // 使用初始值进行累加求和: List numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream().reduce(0, (a, b) -> a + b); System.out.println("Sum: " + sum); // 输出 15 // 使用初始值和组合器函数在并行流中进行累加求和: List numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.parallelStream().reduce(0, (a, b) -> a + b, Integer::sum); System.out.println("Sum: " + sum); // 输出 15

短路操作

• 短路操作:得到符合条件的元素就可以立即返回,而不用处理所有元素,比如 anyMatch() allMatch()。

findFirst

• 用于获取流中的第一个元素(如果存在的话),返回一个 Optional 对象。注意:返回值不一定为第一个元素。

List numbers = Arrays.asList(1, 2, 3, 4, 5); Optional firstNumber = numbers.stream().findFirst(); if (firstNumber.isPresent()) { System.out.println("First number: " + firstNumber.get()); // 输出 First number: 1 } else { System.out.println("No elements found in the stream."); }

总结

• 函数式接口、Lambda表达式和Stream是Java 8引入的重要特性,它们使得Java代码更加简洁、灵活、易读。函数式接口定义了一种新的编程模式,Lambda表达式提供了一种更加简洁的语法来实现函数式接口,Stream则提供了一套丰富的操作方法来处理集合数据。通过这些特性的组合应用,可以极大地提高Java代码的开发效率和质量。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容