函数式接口、lambda表达式和Stream流式编程
基于IJava编辑
在Java中,函数式接口、Lambda表达式和Stream流式编程是Java 8及更高版本中引入的重要特性,它们共同支持了更简洁、更灵活的编程方式,特别是在处理集合和并发编程方面。下面分别介绍这三个概念:
函数式接口(Functional Interfaces)
在Java中,函数式接口是指那些只包含一个抽象方法的接口(除了Object
类中的方法,在接口中声明的方法默认是抽象的)。这样的接口可以与Lambda表达式结合使用,提供简洁的代码表示方式。函数式接口可以使用@FunctionalInterface
注解进行标记,但这不是强制的。只要接口只包含一个抽象方法,虚拟机会自动判断该接口为函数式接口。一般建议在接口上使用@FunctionalInterface 注解进行声明,这样的话,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错
常见的函数式接口
Java标准库中提供了多种类型的函数式接口,以下是其中一些常见的类型:
-
Function<T,R>
- 位于
java.util.function
包下。 - 接受一个输入参数T,并产生一个结果R。
- 主要方法是
apply(T t)
。 - 用于表示一个函数,该函数接受一个参数并返回一个结果。
- 位于
-
Consumer
- 位于
java.util.function
包下。 - 接受一个输入参数T,但不返回任何结果(void)。
- 主要方法是
accept(T t)
。 - 用于表示一个操作,该操作接受单个输入参数并且不返回结果。
- 位于
-
Predicate
- 位于
java.util.function
包下。 - 接受一个输入参数T,并返回一个布尔值结果。
- 主要方法是
test(T t)
。 - 用于表示一个断言函数,该函数接受一个参数并根据条件返回true或false。
- 位于
-
Supplier
- 位于
java.util.function
包下。 - 不接受任何输入参数,但返回一个结果T。
- 主要方法是
get()
。 - 用于表示一个供应商,该供应商提供值。
- 位于
-
Runnable
- 位于
java.lang
包下,尽管它不是一个java.util.function
包下的接口,但它也是一个函数式接口。 - 不接受任何输入参数,也不返回任何结果(void)。
- 主要方法是
run()
。 - 通常用于表示一个任务或操作,该任务或操作可以被新线程执行,或者简单地执行一些操作而不返回结果。
- 位于
-
Comparator
- 位于
java.util
包下,尽管它包含多个方法,但只有一个抽象方法compare(T o1, T o2)
,因此它也可以被视为函数式接口(尽管它不是java.util.function
包下的)。 - 用于表示一个比较器,该比较器定义了两个输入参数之间的自然顺序。
- 位于
需要注意的是,虽然Comparator
接口在java.util
包下,并且包含了多个方法,但根据Java语言规范,只有其compare
方法是抽象的,并且被视为函数式接口的关键方法。其他方法(如equals
、hashCode
等)是从Object
类继承的,或者是在Comparator
接口中作为默认方法提供的,因此它们不计入抽象方法的计数中。
此外,Java标准库中还包含了许多其他函数式接口,它们被设计用于特定的用途,如UnaryOperator<T>
(一元操作符)、BinaryOperator<T>
(二元操作符)、ToIntFunction<T>
(将输入映射到int的函数)等,这些接口都位于java.util.function
包下。
函数式接口的示例:
- Function<T, R>
作用:Function<T, R>接口表示一个接受一个输入参数T并产生结果R的函数。它主要用于类型转换、数据映射等场景。
import java.util.function.Function;
public class FunctionExample {
public static void main(String[] args) {
// 创建一个Function接口实例,将字符串转换为大写 “::” 被称为引用操作符,它用于获取类或对象的方法的引用,也就是我们常说的方法引用(Method Reference)。方法引用是一种简化Lambda表达式的语法糖,使得代码更加简洁易读。
Function<String, String> toUpperCaseFunction = String::toUpperCase;
// 使用Function接口实例进行转换
String result = toUpperCaseFunction.apply("hello world");
System.out.println(result); // 输出: HELLO WORLD
}
}
// [IJava] 调用main函数,查看 main()函数执行结果 在Jupyter notebook中的输出信息
FunctionExample.main(new String[] {})
HELLO WORLD
在Notebook中通过IJava内核运行Java程序需要注意的信息
使用 Jupyter Notebook 配合 IJava 内核来运行 Java 程序时,main 函数的输出没有显示在 Notebook 中。
这通常是因为 Java 程序的标准输出(stdout)和错误输出(stderr)的处理方式与 Python 在 Jupyter 中有所不同。
在 Java 中,当你运行一个包含 main 方法的类时,这个方法默认是设计来独立运行的,而不是直接作为脚本的一部分在 Jupyter Notebook 中执行。
// [IJava] 调用main函数,查看 main() 在Jupyter notebook中的输出信息
ClassName.main(new String[] {})
- Consumer
作用:Consumer接口表示一个接受单个输入参数但不返回结果的操作。它主要用于执行一些操作,如打印、写入文件等。
import java.util.function.Consumer;
public class ConsumerExample {
public static void main(String[] args) {
// 创建一个Consumer接口实例,用于打印字符串
Consumer<String> printConsumer = System.out::println;
// 使用Consumer接口实例进行打印
printConsumer.accept("Hello, Consumer!");
// 也可以链式调用Consumer接口的默认方法andThen
Consumer<String> upperCaseAndPrint = str -> System.out.println(str.toUpperCase());
Consumer<String> printConsumerChain = printConsumer.andThen(upperCaseAndPrint);
printConsumerChain.accept("Hello again!");
}
}
// [IJava] 调用main函数,查看 main() 在Jupyter notebook中的输出信息
ConsumerExample.main(new String[] {})
Hello, Consumer!
Hello again!
HELLO AGAIN!
- Predicate
作用:Predicate接口表示一个接受输入参数并返回布尔值结果的断言函数。它主要用于条件判断。
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class PredicateExample {
public static void main(String[] args) {
List<String> strings = Arrays.asList("apple", "banana", "cherry");
// 创建一个Predicate接口实例,检查字符串长度是否大于5
Predicate<String> isLongerThanFive = str -> str.length() > 5;
// 使用Predicate接口实例进行过滤
strings.stream()
.filter(isLongerThanFive)
.forEach(System.out::println); // 输出: banana
}
}
PredicateExample.main(new String[] {})
banana
cherry
- Supplier
作用:Supplier接口表示一个不接受任何输入参数并返回结果T的供应商。它主要用于延迟计算或按需生成值。
import java.util.function.Supplier;
public class SupplierExample {
public static void main(String[] args) {
// 创建一个Supplier接口实例,按需生成一个随机整数
Supplier<Integer> randomNumberSupplier = () -> (int) (Math.random() * 100);
// 多次调用get()方法获取不同的值
System.out.println(randomNumberSupplier.get());
System.out.println(randomNumberSupplier.get());
}
}
SupplierExample.main(new String [] {})
63
89
- Runnable
尽管Runnable接口位于java.lang包下,并非java.util.function包中的一部分,但它同样是一个函数式接口。
作用:Runnable接口表示一个无参数且无返回值的操作,通常用于创建线程任务。
public class RunnableExample implements Runnable {
@Override
public void run() {
System.out.println("Running a task in a new thread");
}
public static void main(String[] args) {
Thread thread = new Thread(new RunnableExample());
thread.start();
// 使用Lambda表达式简化
new Thread(() -> System.out.println("Lambda task in a new thread")).start();
}
}
RunnableExample.main(new String [] {})
Running a task in a new thread
- Comparator
接口是Java中用于定义对象排序规则的接口。尽管它包含了多个方法(如equals, hashCode, reversed(), thenComparing()等),但只有一个抽象方法compare(T o1, T o2),因此它可以被视为函数式接口。
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class ComparatorExample {
public static void main(String[] args) {
List<String> strings = Arrays.asList("banana", "apple", "cherry", "date");
// 使用Lambda表达式创建Comparator实例,按字符串长度排序
Comparator<String> byLength = (s1, s2) -> Integer.compare(s1.length(), s2.length());
// 使用Collections.sort(或List.sort在Java 8+)和Comparator进行排序
strings.sort(byLength);
// 打印排序后的列表
System.out.println(strings); // 输出可能是:[apple, date, banana, cherry],但顺序可能因Java版本和JVM实现而异
// 如果你想要逆序排序,可以使用Comparator.reversed()
strings.sort(byLength.reversed());
// 再次打印排序后的列表
System.out.println(strings); // 输出可能是:[cherry, banana, date, apple],但顺序同样可能因实现而异
// 你还可以链式调用Comparator来定义更复杂的排序逻辑
// 比如,首先按长度排序,如果长度相同,则按字典顺序排序
strings.sort(Comparator.comparingInt(String::length)
.thenComparing(String::compareTo));
// 打印最终排序后的列表
System.out.println(strings); // 输出应该是:[apple, date, banana, cherry],因为apple和date长度相同,但apple字典序在前
}
}
ComparatorExample.main(new String[] {})
[date, apple, banana, cherry]
[banana, cherry, apple, date]
[date, apple, banana, cherry]
Lambda表达式
Lambda表达式是Java 8引入的一项重要功能,
可以把Lambda表达式看作是一种更方便的匿名函数(即没有名称的方法),Lambda表达式特别适用于实现只有一个抽象方法的接口,即函数式接口。这意味着可以将函数作为参数传递给其他方法,使得代码更加灵活和可扩展。
Lambda表达式的语法
基本语法:
(parameters) -> expression
或
(parameters) ->{ statements; }
Lambda表达式由三部分组成:
- paramaters: 括号内是 lambda 表达式的参数列表。如果只有一个参数且类型可以推断出,可以省略括号。
- ->: Lambda操作符,用于分隔参数列表和方法体,可理解为“被用于”的意思。
- experssion: 一个单一表达式,表示 lambda 表达式的返回值。
- statements: 如果需要多条语句,则需要使用大括号包裹语句块,并且需要显式使用 return 语句返回结果。
Lambda表达式的示例
假设我们有一个函数式接口Greeting,它定义了一个接受String参数并返回void的greet方法:
@FunctionalInterface
interface Greeting {
void greet(String name);
}
// 使用Lambda表达式来实现这个接口
Greeting greeting = name -> System.out.println("Hello, " + name + "!");
// 使用Lambda表达式
greeting.greet("Alice"); // 输出: Hello, Alice!
Hello, Alice!
Lambda表达式的特点
- 简洁性:Lambda表达式使代码更加简洁,易于阅读和维护。通过Lambda表达式,我们可以将复杂的逻辑简化为几行代码。
- 函数式接口:Lambda表达式主要用于实现函数式接口的实例。函数式接口是只包含一个抽象方法的接口,Lambda表达式可以看作是该接口的一个匿名实现。
- 类型推断:在Lambda表达式中,如果编译器能够根据上下文推断出参数的类型,那么可以省略参数的类型声明。
- 语法灵活性:Lambda表达式在语法上提供了很大的灵活性。例如,如果Lambda表达式只接受一个参数,那么可以省略包围该参数的小括号;如果Lambda体只包含一条语句,那么可以省略大括号和分号;如果Lambda体中的唯一语句是一个返回语句,并且该语句的返回值类型与Lambda表达式声明的返回类型兼容,那么可以省略return关键字以及该语句后的分号。
Lambda表达式的应用场景
Lambda表达式在Java中有广泛的应用场景,包括但不限于以下几个方面:
- 集合操作:在Java的集合框架中,我们可以使用Lambda表达式对集合进行各种操作,如过滤、映射、排序等。
多线程编程:Lambda表达式可以用于简化多线程编程。通过Lambda表达式,我们可以更方便地创建线程、定义线程的执行逻辑等。 - GUI编程:在GUI编程中,Lambda表达式可以用于定义事件的处理逻辑,使代码更加简洁。
- 函数式编程:Lambda表达式支持函数式编程的相关特性,如高阶函数、闭包等,使得Java能够支持更加灵活的编程范式。
Lambda表达式的注意事项
虽然Lambda表达式可以使代码更加简洁,但过度使用可能会导致代码难以阅读和理解。因此,在使用Lambda表达式时,应遵循以下几点原则:
- 确保Lambda表达式的逻辑简单明了:避免在Lambda表达式中编写复杂的逻辑,以保持代码的清晰易懂。
- 合理使用参数名称和注释:通过合理的参数命名和必要的注释来增强代码的可读性。
- 对于复杂逻辑,使用传统方法:如果Lambda表达式的逻辑过于复杂,难以理解和维护,那么可以考虑使用传统的方法来编写代码。
lambda 示例
下面通过几个示例来说明Lambda表达式的参数、方法体、返回值在不同情况下的表现,以及哪些部分可以省略。
1. 单个参数且类型可推断
当Lambda表达式只有一个参数,并且该参数的类型可以被编译器推断出来时,可以省略参数的类型声明以及包围参数的小括号(如果参数列表不为空)。
List<String> list = Arrays.asList("apple", "banana", "cherry");
// 在循环中使用lambda表达式打印当前列表元素
list.forEach(s -> System.out.println(s)); // s的类型被推断为String,小括号可以省略但通常保留以提高可读性
apple
banana
cherry
2. 无参数
如果Lambda表达式没有参数,那么需要保留空的小括号。
// 使用 Lambda 表达式创建 Runnable 对象 (Runnable 是一个函数式接口,用于表示可以在线程中执行的任务。Runnable 接口包含一个方法 run(),当线程启动时会调用这个方法来执行任务。使用 Runnable 可以方便地创建线程并实现并发操作。)
Runnable runnable = () -> System.out.println("Hello, Lambda!"); // 无参数,需要空的小括号
// 创建并启动线程
Thread thread = new Thread(runnable);
thread.start();
3. 多个参数
当Lambda表达式有多个参数时,必须显式地声明它们,并且需要用小括号括起来。
Map<String, Integer> map = new HashMap<>();
map.put("apple", 100);
map.put("banana", 200);
// 即使在多个参数的情况下,只要 lambda 表达式的目标类型能够提供足够的信息来推断参数类型,就可以省略参数类型。
// map.forEach((String key, Integer value) -> System.out.println(key + ": " + value)); // 多个参数,需要小括号
map.forEach((key, value) -> System.out.println(key + ": " + value)); // 多个参数,需要小括号
banana: 200
apple: 100
4. 方法体只有一条语句
如果Lambda表达式的方法体只有一条语句,那么可以省略大括号和分号。如果这条语句是返回语句,并且Lambda表达式的返回类型与语句的返回类型兼容,那么还可以省略return关键字。
List<String> strings = Arrays.asList("apple", "banana", "cherry");
String firstFruit = strings.stream()
.filter(s -> s.startsWith("a")) // 过滤以'a'开头的字符串,只有一条语句,省略了大括号和分号
.findFirst() // 找到第一个符合条件的元素
.orElse("No fruit starts with 'a'"); // 如果找不到,则返回"No fruit starts with 'a'"
firstFruit
apple
5. 方法体有多条语句
如果Lambda表达式的方法体包含多条语句,那么必须使用大括号将它们括起来,并且每条语句后需要分号。
List<String> list = Arrays.asList("apple", "banana", "cherry");
list.forEach(s -> {
System.out.print(s.toUpperCase()); // 转换为大写
System.out.println("!"); // 添加感叹号
}); // 多条语句,需要大括号和分号
APPLE!
BANANA!
CHERRY!
6. 返回值
Lambda表达式的返回值取决于其实现的函数式接口的抽象方法的返回类型。如果Lambda表达式的方法体只有一条返回语句,并且该语句的返回类型与Lambda表达式的返回类型兼容,则可以省略return关键字。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.map(n -> n * 2) // 将每个数字乘以2,返回int类型,省略了return
.reduce(0, Integer::sum); // 累加结果,初始值为0
在这个例子中,map
操作中的Lambda表达式n -> n * 2
没有显式地使用return
关键字,因为编译器知道map
函数期望一个返回int
的函数式接口实例,而n * 2
的结果就是int
类型,因此可以省略return
。
流式编程(Stream API)详解
1. 引入背景
在Java 8之前,处理集合(如List、Set)中的元素通常需要通过循环(for-each、for循环等)和条件语句(if-else)来实现。这种方式在处理复杂的数据转换和过滤时,代码往往变得冗长且难以维护。Java 8引入的Stream API提供了一种更加高效、灵活且易于理解的方式来处理集合中的元素。
2. 核心概念
-
流(Stream):流是Java 8中引入的一个关键抽象概念,它代表了一个来自数据源的元素队列并支持聚合操作。流操作分为中间操作和终端操作两种。
-
中间操作:中间操作是指那些返回一个新的Stream流对象的操作,,它们不会消耗Stream流,也不会产生最终的结果,而是可以链式地调用,形成一个操作管道。中间操作是惰性的,即它们不会立即执行,而是会等到终端操作时才执行。
-
终端操作:触发流的执行,产生一个最终的结果或者一个副作用的操作。一旦执行了终端操作,流就被消费了,无法再次使用。
-
并行处理:Stream API支持并行处理,可以通过调用集合的parallelStream()方法获得并行流,以提高处理效率。
-
延迟执行:Stream API的操作是延迟执行的,即中间操作不会立即执行,而是会等待终端操作时才执行整个操作管道。
-
不改变源对象:Stream API的所有操作都不会改变源对象,而是返回一个新的Stream流对象或最终结果。
3. 函数式接口与Lambda表达式
在流式编程中,函数式接口和Lambda表达式是不可或缺的部分。函数式接口是只包含一个抽象方法的接口(可以有多个默认方法或静态方法),而Lambda表达式则是函数式接口的匿名实现。
Stream API定义了大量的函数式接口来支持各种操作,如:
-
Predicate
:这是一个用于测试某个条件是否满足的函数式接口。它定义了一个 test
方法,该方法接受一个输入参数并返回一个布尔值。常用于filter
操作中。 -
Consumer
:这是一个接受单个输入参数但不返回任何结果的操作,它对输入参数执行指定的操作。常用于 forEach
和accept
操作中。 -
Function<T,R>:这是一个将输入参数T映射到结果R的函数式接口。它定义了一个
apply
方法,该方法接受一个输入参数并返回相应的结果。常用于map
操作中。 -
Supplier
:这是一个不接受任何参数但返回一个结果的函数式接口。它定义了一个 get
方法,用于返回结果。常用于需要生成新对象或值的场景中。 -
UnaryOperator
:这是 Function<T,T>
的一个特殊化接口,表示接受一个参数并返回相同类型结果的函数。 -
BinaryOperator
:这是一个表示接受两个同类型参数并返回它们组合结果的函数式接口。它定义了一个 apply
方法,该方法接受两个输入参数并返回一个结果。常用于归约操作中,如reduce
。 -
Comparator
:这是一个用于比较两个对象的函数式接口。它定义了一个 compare
方法,该方法比较两个对象并返回一个整数来表示它们的顺序。虽然Comparator
不是专门为Stream API设计的,但它在排序和比较操作中非常有用。 -
ToIntFunction
、ToLongFunction 、ToDoubleFunction :这些是 Function
接口的特殊化,分别用于返回int
、long
和double
类型的值。 -
IntFunction
、LongFunction 、DoubleFunction :这些是 Function
接口的特殊化,它们的输入参数是int
、long
或double
类型,并返回一个泛型结果R
。 -
ObjIntConsumer
、ObjLongConsumer 、ObjDoubleConsumer :这些接口结合了 Consumer
和数值类型参数(int
、long
、double
),用于对单个对象和一个数值进行操作。 -
BiConsumer<T,U>:这是一个接受两个输入参数且没有返回值的函数式接口。虽然它不是Stream API特有的,但在需要对两个参数执行操作的场景中很有用。
-
BiFunction<T,U,R>:这是一个将两个输入参数T和U映射到结果R的函数式接口。它定义了一个
apply
方法,该方法接受两个输入参数并返回一个结果。
请注意,这里列出的只是Stream API中常用的一部分函数式接口。Java的java.util.function
包中定义了许多其他函数式接口,它们可以与Stream API或其他Java 8及以上版本的函数式编程特性一起使用。这些接口提供了丰富的功能,支持各种复杂的操作和数据转换。
4. 流式编程的基本步骤(详细)
-
创建流:
- 通过调用集合的
stream()
方法创建顺序流。 - 通过调用集合的
parallelStream()
方法创建并行流(如果可用)。 - 通过数组:使用Arrays.stream(T[] array)方法。
- 通过Stream的of()方法:直接生成包含指定元素的Stream。
- 其他:如使用IntStream.range(int startInclusive, int endExclusive)等生成数字序列的Stream。
- 通过调用集合的
-
中间操作(可选,可多个):
-
过滤:使用
filter(Predicate<? super T> predicate)
方法过滤流中的元素。 -
映射:使用
map(Function<? super T,? extends R> mapper)
方法将流中的每个元素映射成另一种形式。 -
排序:使用
sorted()
或sorted(Comparator<? super T> comparator)
方法对流中的元素进行排序。 -
去重:使用
distinct()
方法去除流中的重复元素(基于元素的equals()
和hashCode()
方法)。 -
截断:使用
limit(long maxSize)
方法限制流中元素的数量。 -
跳过:使用
skip(long n)
方法跳过流中的前n个元素。 -
其他:如
peek(Consumer<? super T> action)
方法,主要用于调试目的,对流中的每个元素执行操作,但不改变流本身。
-
-
终端操作(必须):
- 遍历/消费:使用
forEach(Consumer<? super T> action)
方法遍历流中的每个元素,并对其执行操作。 - 归约:使用
reduce(BinaryOperator<T> accumulator)
方法将流中的元素组合起来,得到一个值。 - 收集:使用
collect(Collectors.toList())
、collect(Collectors.toSet())
等方法将流中的元素收集到新的集合中。 - 匹配:使用
anyMatch(Predicate<? super T> predicate)
、allMatch(Predicate<? super T> predicate)
、noneMatch(Predicate<? super T> predicate)
等方法检查流中的元素是否满足某个条件。 - 查找:使用
findFirst()
、findAny()
等方法查找流中的元素。 - 最大值/最小值:使用
max(Comparator<? super T> comparator)
、min(Comparator<? super T> comparator)
方法找到流中的最大或最小元素。 - 计数:使用
count()
方法计算流中的元素数量。
- 遍历/消费:使用
关于Stream的更多信息可查看Java API 中文文档
Java API 官方文档
Stream API 示例
1. 通过集合创建Stream并进行过滤和排序
假设你有一个包含多个字符串的列表,你想要筛选出所有长度大于3的字符串,并按字典顺序排序。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("Java");
stringList.add("Stream");
stringList.add("API");
stringList.add("Example");
// 创建Stream
List<String> filteredAndSortedList = stringList.stream()
// 过滤操作:筛选出长度大于3的字符串
.filter(s -> s.length() > 3)
// 排序操作:按字典顺序排序
.sorted()
// 收集操作:将Stream转换为List
.collect(Collectors.toList());
System.out.println(filteredAndSortedList);
// 输出: [API, Example, Java, Stream](注意:这里的输出假设排序不考虑大小写,实际输出可能因排序规则而异)
}
}
StreamExample.main(new String[] {})
[Example, Java, Stream]
2. 使用Stream处理数组
假设你有一个整型数组,你想要找出其中所有的偶数并求和。
import java.util.Arrays;
import java.util.stream.IntStream;
public class StreamArrayExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用Arrays.stream()将数组转换为IntStream
int sumOfEvens = Arrays.stream(numbers)
// 过滤操作:筛选出偶数
.filter(n -> n % 2 == 0)
// 规约操作:求和
.sum();
System.out.println(sumOfEvens);
// 输出: 30
}
}
StreamArrayExample.main(new String [] {})
30
3. 无限流的创建和使用
虽然无限流在实际应用中较少,但了解如何创建它们也是有益的。你可以使用Stream.iterate()或Stream.generate()来创建无限流。
import java.util.stream.Stream;
public class InfiniteStreamExample {
public static void main(String[] args) {
// 使用Stream.iterate()创建无限流,从1开始,每次增加1
Stream<Integer> infiniteStream = Stream.iterate(1, n -> n + 1);
// 注意:无限流不能直接使用collect()等方法收集,因为它没有终止条件
// 这里仅作为示例,展示如何获取流的前几个元素
infiniteStream.limit(5) // 限制流中元素的数量
.forEach(System.out::println);
// 输出: 1, 2, 3, 4, 5
}
}
InfiniteStreamExample.main(new String[] {});
1
2
3
4
5
4. 映射和收集
假设你有一个包含人员信息的列表,每个人员都是一个包含姓名和年龄的Person对象。你想要创建一个新列表,其中只包含人员的姓名(大写)。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Person {
// 成员变量
private String name;
private int age;
// 无参构造函数
public Person() {
}
// 带参构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter 方法:获取 name
public String getName() {
return name;
}
// Setter 方法:设置 name
public void setName(String name) {
this.name = name;
}
// Getter 方法:获取 age
public int getAge() {
return age;
}
// Setter 方法:设置 age
public void setAge(int age) {
this.age = age;
}
}
public class StreamMapExample {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
List<String> names = people.stream()
.map(Person::getName) // 将Person对象映射为姓名字符串
.map(String::toUpperCase) // 将姓名转换为大写
.collect(Collectors.toList()); // 收集结果到新的List中
System.out.println(names); // 输出: [ALICE, BOB, CHARLIE]
}
}
StreamMapExample.main(new String[] {})
[ALICE, BOB, CHARLIE]
5. 过滤和归约
假设你有一个数字列表,你想要找到列表中所有偶数的和。
import java.util.Arrays;
import java.util.List;
public class StreamReduceExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sumOfEvens = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤出偶数
.reduce(0, Integer::sum); // 从0开始归约求和
System.out.println(sumOfEvens); // 输出: 30
}
}
StreamReduceExample.main(new String[] {})
30
6. 扁平化和收集
假设你有一个列表的列表,每个内部列表包含一些字符串。你想要创建一个新的列表,其中包含所有内部列表中的字符串。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamFlatMapExample {
public static void main(String[] args) {
List<List<String>> listOfLists = Arrays.asList(
Arrays.asList("Apple", "Banana"),
Arrays.asList("Orange", "Mango", "Peach"),
Arrays.asList("Grape", "Strawberry")
);
List<String> allFruits = listOfLists.stream()
.flatMap(List::stream) // 扁平化内部列表
.collect(Collectors.toList()); // 收集结果到新的List中
System.out.println(allFruits); // 输出: [Apple, Banana, Orange, Mango, Peach, Grape, Strawberry]
}
}
StreamFlatMapExample.main(new String[] {})
[Apple, Banana, Orange, Mango, Peach, Grape, Strawberry]
7. 查找最大值和最小值
假设你有一个数字列表,你想要找到列表中的最大值和最小值。
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class StreamMinMaxExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Optional<Integer> max = numbers.stream()
.max(Integer::compare); // 查找最大值
Optional<Integer> min = numbers.stream()
.min(Integer::compare); // 查找最小值
max.ifPresent(System.out::println); // 输出最大值,如果有的话
min.ifPresent(System.out::println); // 输出最小值,如果有的话
// 输出: 10
// 输出: 1
}
}
StreamMinMaxExample.main(new String[] {})
10
1
方法和构造函数引用(Method and Constructor References) ::
操作符
方法引用是Lambda表达式的一种更简洁的写法,用于直接引用已存在的方法或构造函数。
在Lambda表达式中,你可能需要显式地声明参数,并在花括号{}中编写方法体。然而,当Lambda表达式的逻辑只是调用一个已经存在的方法时,你可以使用方法引用来代替,从而避免重复编写这些参数和方法体。这种简写方式使得代码更加简洁、易于阅读,并且有时还可以提高性能(尽管这种性能提升通常是微小的,并且依赖于JVM的实现)。
::
操作符(方法引用)
::
操作符是Java 8引入的,用于方法引用。方法引用提供了一种引用已存在方法或构造函数而不执行它的更简洁的方式。 这主要用于Lambda表达式中,以提供一种更简洁、更易于阅读的方式来传递方法。
方法引用通过::
操作符实现,它主要有四种形式:
-
静态方法引用:当Lambda表达式只是调用了一个静态方法时,可以使用类名
::
静态方法名来引用该方法。例如,Arrays::sort
可以视为(arr, comparator) -> Arrays.sort(arr, comparator)
的简写,但这里需要注意Arrays::sort
通常与特定的Stream操作结合使用,其实际应用可能更复杂。 -
特定对象的实例方法引用:当Lambda表达式调用的是特定对象的实例方法时,可以使用
instance::instanceMethod
来引用。例如,str::length
可以视为str -> str.length()
的简写。 -
特定类型的任意对象的实例方法引用:当Lambda表达式需要接收一个对象作为参数,并调用该对象的实例方法时,可以使用
ClassName::instanceMethod
来引用。这里,Lambda表达式的参数会成为方法调用中的隐式参数。例如,List::size
可以视为(list) -> list.size()
的简写。 -
构造方法引用:当Lambda表达式只是简单地调用构造方法时,可以使用
ClassName::new
来引用。例如,ArrayList::new
可以视为() -> new ArrayList<>()
的简写(注意这里省略了泛型参数,因为::new
的语法在Java中不允许直接指定泛型参数,但可以通过类型推断来处理)。
方法引用示例
- 静态方法引用
静态方法引用是指向某个类的静态方法。它的语法是:
ClassName::staticMethodName
假设有一个 MathUtils 类,其中有一个静态方法 square:
public class MathUtils {
public static int square(int x) {
return x * x;
}
}
import java.util.function.IntUnaryOperator;
public class MethodReferenceExample {
public static void main(String[] args) {
// IntUnaryOperator是一个函数式接口,它接受一个 int 参数并返回它int类型的的平方; MathUtils::square 是对 MathUtils 类中静态方法 square 的引用
IntUnaryOperator squareOperator = MathUtils::square;
// IntUnaryOperator squareOperator = x -> MathUtils.square(x);
// 对 MathUtils 类中静态方法 square 的引用
System.out.println(squareOperator.applyAsInt(5)); // 输出 25
}
}
MethodReferenceExample.main(new String[] {})
25
- 实例方法引用
实例方法引用是指向某个对象的实例方法。语法是:
instance::instanceMethodName
假设有一个 Person 类,其中有一个实例方法 greet:
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class InstanceMethodReferenceExample {
// 一个简单的类,包含一个实例方法
static class Printer {
public void print(String message) {
System.out.println(message);
}
}
public static void main(String[] args) {
// 创建一个 Printer 实例
Printer printer = new Printer();
// 创建一个字符串列表
List<String> messages = Arrays.asList("Hello", "World", "Java", "Streams");
// 使用实例方法引用来打印每个字符串
// 传递 printer::print 作为 Consumer<String> 的实现
// messages.forEach(printer::print);
messages.forEach(message -> printer.print(message));
}
}
InstanceMethodReferenceExample.main(new String[] {})
Hello
World
Java
Streams
- 特定对象的方法引用
特定对象的方法引用是指向某个特定对象的实例方法。语法是:
ClassName::instanceMethodName
但对象是已知的。
假设有一个 Printer 类,其中有一个实例方法 print:
public class Printer {
public void print(String message) {
System.out.println(message);
}
}
import java.util.Arrays;
import java.util.List;
public class MethodReferenceExample {
public static void main(String[] args) {
Printer printer = new Printer();
List<String> messages = Arrays.asList("Hello", "World");
// 使用特定对象的方法引用 forEach 方法接受一个 Consumer,并对列表中的每个元素应用 print 方法; printer::print 是对 printer 对象的实例方法 print 的引用
// messages.forEach(printer::print);
messages.forEach(printer::print);
}
}
MethodReferenceExample.main(new String[] {})
Hello
World
- 构造函数引用
构造函数引用是指向类的构造函数。语法是:
ClassName::new
假设有一个 Person 类,其中有一个构造函数:
ClassName::new
假设有一个 Person 类,其中有一个构造函数:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
import java.util.function.Function;
public class MethodReferenceExample {
public static void main(String[] args) {
// 使用构造函数引用
// Function<String, Person> 是一个函数式接口,它接受一个 String 参数并返回一个 Person 对象; Person::new 是对 Person 类构造函数的引用
Function<String, Person> personCreator = Person::new;
// Function<String, Person> personCreator = name -> new Person(name);
Person person = personCreator.apply("John");
System.out.println(person.getName()); // 输出 "John"
}
}
MethodReferenceExample.main(new String[] {})
John
补充: .
操作符
在Java中,::
操作符和 .
操作符是两种非常不同但各自在特定上下文中非常重要的操作符。
.
操作符是Java中的成员访问操作符。它用于访问对象的字段(变量)和方法。当你有一个对象引用,并且想要访问该对象的某个字段或调用其某个方法时,你会使用 .
操作符。
例如,如果你有一个String
对象str
,你可以使用str.length()
来调用其length()
方法,或者使用str.charAt(0)
来访问其第一个字符。