Java 8 Stream和Optional: 实践指南
第一章:Java 8 Stream和Optional入门
- Java 8 Stream的概述和基本用法
- Java 8 Optional的概述和基本用法
- Stream和Optional的优缺点和适用场景
第二章:Stream的进阶用法
- 中间操作和终止操作的区别和作用
- 常用的中间操作和终止操作
- Stream的并行处理和性能优化
- Stream的异常处理和错误调试
第三章:Optional的进阶用法
- Optional的链式调用和方法组合
- Optional的过滤和映射
- Optional的orElse和orElseGet方法的区别
- Optional的异常处理和错误调试
第四章:Stream和Optional在项目中的实践
- 使用Stream进行数据处理和转换
- 使用Optional进行空值处理和默认值设置
- Stream和Optional在数据库查询和结果处理中的应用
- Stream和Optional在Web应用和API设计中的应用
第五章:Stream和Optional的最佳实践和注意事项
- 如何避免Stream和Optional的误用和滥用
- Stream和Optional在团队合作和代码维护中的应用
- Stream和Optional的性能和资源消耗的考虑
- Stream和Optional在Java 9和更高版本中的变化和更新
第六章:总结和展望
- Stream和Optional在Java开发中的重要性和未来发展趋势
- Stream和Optional在其他编程语言中的应用和比较
- 如何进一步学习和掌握Stream和Optional的高级用法和扩展功能
第一章:Java 8 Stream和Optional入门
1.1 Java 8 Stream的概述和基本用法
Java 8 Stream是一个基于集合(Collection)的元素处理框架,它提供了一种流式处理(stream processing)的方式,可以方便地进行数据过滤、映射、聚合等操作。
Stream具有以下特点
-
延迟执行(lazy evaluation):Stream中的操作并不会立即执行,而是在需要的时候才执行,这样可以减少不必要的计算。
-
内部迭代(internal iteration):Stream使用迭代器(Iterator)遍历集合,不需要手动编写循环代码。
-
不可变性(immutability):Stream不会修改原有的数据,而是返回新的Stream对象。
-
并行处理(parallel processing):Stream可以并行处理数据,充分利用多核处理器的性能。
Stream的基本用法包括以下几个步骤
-
创建Stream:可以通过集合、数组、文件等方式创建Stream。
-
中间操作:对Stream进行一系列中间操作,如filter、map、flatMap、distinct等,这些操作会返回新的Stream对象。
-
终止操作:对Stream进行终止操作,如forEach、reduce、collect等,这些操作会触发Stream的计算。
简单的示例代码
-
创建Stream
// 创建一个包含数字 1~10 的 Stream Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 通过数组创建 Stream String[] array = {"a", "b", "c", "d", "e"}; Stream<String> stream2 = Arrays.stream(array); // 通过集合创建 Stream List<String> list = Arrays.asList("hello", "world", "java8"); Stream<String> stream3 = list.stream();
-
中间操作
// 过滤操作:筛选出长度大于 3 的字符串 Stream<String> stream = Stream.of("hello", "world", "java8"); stream.filter(str -> str.length() > 3); // 映射操作:将每个字符串转换为大写 Stream<String> stream = Stream.of("hello", "world", "java8"); stream.map(String::toUpperCase); // 排序操作:按照字符串长度从小到大排序 Stream<String> stream = Stream.of("hello", "world", "java8"); stream.sorted(Comparator.comparing(String::length));
-
终止操作
// forEach() List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.stream().forEach(System.out::println); // toArray() List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); String[] namesArray = names.stream().toArray(String[]::new); // reduce() List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream().reduce(0, (a, b) -> a + b); // collect() List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<String> filteredNames = names.stream().filter(name -> name.startsWith("A")).collect(Collectors.toList()); // count() List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); long count = names.stream().filter(name -> name.startsWith("A")).count(); // anyMatch() List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); boolean hasMatch = names.stream().anyMatch(name -> name.startsWith("A")); // noneMatch() List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); boolean noneMatch = names.stream().noneMatch(name -> name.contains("z")); // findFirst() List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Optional<String> firstMatch = names.stream().filter(name -> name.startsWith("A")).findFirst(); // findAny() List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Optional<String> anyMatch = names.stream().filter(name -> name.startsWith("A")).findAny();
1.2 Java 8 Optional的概述和基本用法
Java 8中引入了一个新的类Optional,用于解决NullPointerException问题和使代码更加健壮。 Optional类代表一个值可能存在也可能不存在的容器。它可以用于任何类型的对象,并提供了一些有用的方法来处理可能为空的值。
Optional类有两种状态:存在和不存在。如果一个 Optional 对象存在,它将包含一个非空值,否则它将是一个空对象。
Optional具有以下特点
-
避免空指针异常: Optional 类可以帮助我们避免空指针异常,因为如果一个值不存在, Optional 类会返回一个空对象,而不是抛出空指针异常。
-
方便的API: Optional 类提供了一系列方便的API,用于处理可能存在或不存在的值。这些API包括 orElse() 、 orElseGet() 、orElseThrow()等方法。
-
函数式编程支持: Optional 类支持函数式编程,可以轻松地与Java 8中的Lambda表达式和Stream API结合使用。
-
不可变性:Optional类是不可变的,一旦创建了一个 Optional 对象,它的值就不能被修改。
-
可以通过工厂方法创建: Optional 类提供了静态工厂方法,如 of() 、 ofNullable() 等,可以方便地创建 Optional 对象。
-
可以嵌套使用:Optional 类可以嵌套使用,以表示嵌套的可能存在或不存在的值。
Stream的基本用法包括以下几个步骤
-
创建 Optional 对象:可以通过静态方法 Optional.of(value) 来创建一个非空的 Optional 对象,也可以通过 Optional.empty() 方法创建一个空的 Optional 对象。
-
判断是否存在值:可以使用 isPresent() 方法来判断 Optional 对象中是否存在值,如果存在则返回 true,否则返回 false。
-
获取 Optional 对象中的值:可以使用 get() 方法来获取 Optional 对象中的值,但是如果 Optional 对象为空则会抛出 NoSuchElementException 异常。更好的做法是使用 orElse(defaultValue) 方法来获取 Optional 对象中的值,如果 Optional 对象为空则返回一个默认值。
-
转换 Optional 对象中的值:可以使用 map() 方法来对 Optional 对象中的值进行转换,转换的结果会被包装成一个新的 Optional 对象。也可以使用 flatMap() 方法来对 Optional 对象中的值进行转换,但是转换的结果必须是一个 Optional 对象。
-
过滤 Optional 对象中的值:可以使用 filter() 方法来过滤 Optional 对象中的值,只有符合条件的值才会被包装成一个新的 Optional 对象,否则返回一个空的 Optional 对象。
-
在 Optional 对象为空时执行操作:可以使用 ifPresent() 方法来判断 Optional 对象是否为空,如果不为空则执行传入的 Consumer 对象中的代码,否则不执行。
简单的示例代码
-
创建 Optional 对象:
Optional<String> optionalString = Optional.of("Hello, world!"); // 创建包含非空值的 Optional 对象 Optional<String> emptyOptional = Optional.empty(); // 创建一个空的 Optional 对象 Optional<String> nullableOptional = Optional.ofNullable(null); // 创建一个包含可能为 null 的值的 Optional 对象
-
判断 Optional 对象是否存在值:
Optional<String> optionalString = Optional.of("Hello, world!"); if (optionalString.isPresent()) { System.out.println("Optional 对象中存在值:" + optionalString.get()); } else { System.out.println("Optional 对象中不存在值"); }
-
获取 Optional 对象中的值:
Optional<String> optionalString = Optional.of("Hello, world!"); String value = optionalString.get(); // 获取 Optional 对象中的值 System.out.println("Optional 对象中的值为:" + value);
-
转换 Optional 对象中的值:
Optional<String> optionalString = Optional.of("123"); Optional<Integer> optionalInteger = optionalString.map(Integer::parseInt); // 转换 Optional 对象中的值的类型 System.out.println("Optional 对象中的值为:" + optionalInteger.get());
-
过滤 Optional 对象中的值:
Optional<String> optionalString = Optional.of("Hello, world!"); Optional<String> filteredOptional = optionalString.filter(value -> value.contains("Hello")); // 过滤 Optional 对象中的值 if (filteredOptional.isPresent()) { System.out.println("Optional 对象中的值为:" + filteredOptional.get()); } else { System.out.println("Optional 对象中不存在符合条件的值"); }
-
在 Optional 对象为空时执行操作:
Optional<String> optionalString = Optional.empty(); optionalString.ifPresentOrElse( value -> System.out.println("Optional 对象中存在值:" + value), // 如果 Optional 对象中存在值,则执行该操作 () -> System.out.println("Optional 对象中不存在值") // 如果 Optional 对象中不存在值,则执行该操作 );
1.3 Stream和Optional的优缺点和适用场景
Stream
-
优点
-
可以使用流式操作对集合进行复杂的操作,例如过滤、映射、聚合等,提高了代码的可读性和简洁性。
-
可以使用并行流进行并行操作,提高了程序的性能。
-
可以与Lambda表达式和方法引用结合使用,进一步简化代码。
-
-
缺点
-
学习成本高:Stream 是一个全新的 API,需要对其运作原理、使用方式等进行学习和理解,对于初学者来说可能需要花费一定的时间和精力。
-
性能问题:在某些情况下,Stream 可能会导致性能问题,特别是在处理大量数据时。这是因为 Stream API 基于 lambda 表达式和函数式编程原理,需要进行额外的操作来创建 lambda 表达式、将其转换为函数对象,并执行相应的操作。
-
并行操作问题:Stream 支持并行操作,但并不是所有情况下都适用。在某些情况下,使用并行操作可能会导致线程安全问题或者额外的开销,需要开发者进行额外的测试和优化。
-
可读性问题:虽然 Stream 的链式调用能够提高代码的可读性,但当链式调用过于复杂时,代码可能变得难以阅读和维护。此外,Stream 还可能会导致命名冲突和方法重载等问题。
-
难以调试:由于 Stream 操作是延迟执行的,调试时可能会遇到一些困难。此外,Stream 还会进行中间操作和终端操作的区分,如果在中间操作中出现了错误,可能会导致终端操作无法执行。
-
不能很好地处理大量数据:由于 Stream 是基于内存的操作,如果处理的数据量非常大,可能会导致内存不足或性能下降的问题。
-
只能使用一次:Stream 只能被消费一次,一旦使用完毕,就不能再次使用。如果需要多次使用同一个数据集进行不同的操作,需要创建多个 Stream 对象。
-
-
适用场景
- 需要对大量的数据进行复杂的操作,例如数据的筛选、转换、排序等。
- 需要并行处理数据以提高程序的性能。
- 需要使用函数式编程的方式处理集合,以提高代码的可读性和简洁性。
Optional
-
优点
- 可以避免对null值进行操作时可能出现的空指针异常。
- 可以使用链式调用的方式,进一步简化代码。
-
缺点
-
学习成本较高: Optional 类型的概念比较新,需要一定的学习成本,需要理解它的工作原理以及如何正确使用它。
-
过度使用会导致代码冗长: 在某些情况下,使用 Optional 类型可能会导致代码变得冗长。因为要检查 Optional 是否包含值,并且使用其 API 获取值,需要编写大量的条件代码。
-
可能会掩盖问题: Optional 类型可能会掩盖问题。例如,如果使用 Optional 代替 null 值,代码可能会在运行时产生相同的错误,但是开发人员无法轻松地定位问题。
-
性能开销: 使用 Optional 类型可能会带来一定的性能开销。由于 Optional 类型是一个包装器类,需要在运行时创建对象,并且需要进行额外的检查来确定值是否存在。
-
-
适用场景
- 处理可能为空的对象时,可以使用Optional来封装对象,避免出现空指针异常。
- 在返回结果时,可以使用Optional来表示可能为空的返回值,进一步提高代码的健壮性和可读性。
第二章:Stream的进阶用法
2.1. 中间操作和终止操作的区别和作用
中间操作
中间操作是指对 Stream 对象进行转换或者过滤,但并不会触发 Stream 的处理,而是返回一个新的 Stream 对象。中间操作可以串联起来,形成一个操作流水线,最终生成一个新的 Stream 对象。常用的中间操作有 filter、map、distinct、sorted、limit、skip 等。
终止操作
终止操作是指对 Stream 对象进行处理,并产生一个最终结果,一旦触发了终止操作,Stream 对象就不能再进行其他的操作。常用的终止操作有 forEach、count、collect、reduce、min、max、allMatch、anyMatch、noneMatch、findAny、findFirst 等。
通过中间操作,我们可以将一个复杂的数据处理流程拆分成多个简单的操作,方便进行代码的组织和复用。而通过终止操作,我们可以触发 Stream 对象的处理,得到最终的结果。同时,Stream API 支持惰性求值,只有在终止操作被触发时才会执行中间操作,这样可以避免对不必要的数据进行处理,提高代码的执行效率。
2.2 常用的中间操作和终止操作
-
filter
它可以通过一个 Predicate 来过滤集合中的元素,返回一个新的 Stream 对象,只包含满足条件的元素。
下面是 filter 方法的语法:
Stream<T> filter(Predicate<? super T> predicate)
其中,T 表示 Stream 中的元素类型,Predicate 是一个函数接口,用于定义过滤条件。该方法会返回一个新的 Stream 对象,该对象只包含满足 predicate 条件的元素。
以下是一个简单的示例,演示如何使用 filter 方法来过滤一个字符串列表,只保留长度大于等于 5 的字符串:
List<String> list = Arrays.asList("apple", "banana", "orange", "watermelon", "peach"); Stream<String> stream = list.stream().filter(str -> str.length() >= 5); stream.forEach(System.out::println);
输出结果为:
banana orange watermelon
在上面的示例中,我们首先使用 Arrays.asList 方法创建了一个包含多个字符串的列表,然后使用 stream 方法将其转换为一个 Stream 对象。接着使用 filter 方法来过滤集合中长度大于等于 5 的元素,最后使用 forEach 方法遍历输出结果。
除了过滤字符串之外,filter 方法还可以用于过滤数值、对象等类型的元素。只需要传入相应的 Predicate 对象即可。
-
map
它接收一个函数作为参数,将该函数应用于Stream中的每个元素,生成一个新的Stream并返回。
下面是 map 方法的语法:
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
其中,Function是一个函数式接口,它接收一个参数并返回一个结果。在这个方法中,它接收一个类型为T的参数并返回一个类型为R的结果。
以下是一个简单的示例,演示如何使用 map 方法将一个字符串列表转换为大写:
List<String> words = Arrays.asList("hello", "world", "java"); List<String> upperWords = words.stream() .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println(upperWords); // [HELLO, WORLD, JAVA]
在上面的示例中,我们首先将字符串列表转换为 Stream ,然后使用 map 方法将每个字符串转换为大写形式,最后使用 collect 方法将Stream转换为列表并将其打印出来。
另一个例子是将一个整数列表中的每个元素都乘以2并输出结果:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> doubledNumbers = numbers.stream() .map(n -> n * 2) .collect(Collectors.toList()); System.out.println(doubledNumbers); // [2, 4, 6, 8, 10]
在这个例子中,我们使用 map 方法将列表中的每个元素乘以2,并将结果存储在一个新的列表中。
-
flatMap
它接受一个函数作为参数,该函数将流中的每个元素转换为一个流,然后将这些流合并成一个新的流。这个操作可以用于将一个嵌套的流展开成一个扁平的流。
下面是 flatMap 方法的语法:
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
其中:
- T 是原始流中元素的类型。
- R 是新生成流中元素的类型。
- mapper 是一个函数,将原始流中的每个元素映射到另一个流。
该方法的返回值是一个新的流,该流包含mapper函数的结果流中的所有元素。在结果流中,所有元素的类型都必须是相同的。
以下是一个简单的示例,演示如何使用 flatMap 将一个包含多个列表的列表转换为一个扁平的列表:
List<List<Integer>> list = Arrays.asList( Arrays.asList(1, 2), Arrays.asList(3, 4), Arrays.asList(5, 6) ); List<Integer> flatList = list.stream() .flatMap(Collection::stream) .collect(Collectors.toList()); System.out.println(flatList); // 输出 [1, 2, 3, 4, 5, 6]
在上面的示例中,我们使用了 flatMap 将每个包含整数的列表转换为一个扁平的流,并将所有的流合并成一个新的流。最后,我们使用 collect 将新的流转换为一个列表。
需要注意的是,flatMap 返回的是一个扁平的流,因此它的操作结果可能与原始流的元素顺序不同。如果需要保留原始流的元素顺序,可以考虑使用 flatMapSequential 方法。
-
distinct
它可以根据元素的“唯一性”去重。具体来说,它会根据元素的equals()方法来判断元素是否重复,如果元素相等,则只保留其中一个。该方法返回一个去重后的Stream。
下面是 distinct 方法的语法:
Stream<T> distinct()
以下是一个简单的示例,
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 4, 1, 5, 3, 6); List<Integer> distinctNumbers = numbers.stream().distinct().collect(Collectors.toList()); System.out.println(distinctNumbers); // [1, 2, 3, 4, 5, 6]
在上面的示例中,我们首先创建了一个包含重复元素的List。然后,我们将这个List转换为一个Stream,然后调用distinct()方法去重。最后,我们使用collect()方法将Stream转换回List,并将去重后的List输出到控制台。
需要注意的是,distinct()方法只能去重基本类型和实现了equals()方法的对象类型。如果想要去重自定义类型的对象,需要重写该类型的equals()和hashCode()方法。
-
sorted
它返回一个排序后的流,可以按照自然顺序或者指定的比较器来进行排序。
-
自然排序
下面是 sorted 方法的语法:
Stream<T> sorted()
该方法返回按照自然顺序排序后的流。
以下是一个简单的示例, 该方法返回按照自然顺序排序后的流。
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3); List<Integer> sortedNumbers = numbers.stream() .sorted() .collect(Collectors.toList()); System.out.println(sortedNumbers); // [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
-
自定义排序
下面是 sorted 方法的语法:
Stream<T> sorted(Comparator<? super T> comparator)
以下是一个简单的示例, 该方法返回按照指定比较器排序后的流。
List<String> names = Arrays.asList("Tom", "Jerry", "Alice", "Bob", "Carol"); List<String> sortedNames = names.stream() .sorted(Comparator.comparing(String::length)) .collect(Collectors.toList()); System.out.println(sortedNames); // [Tom, Bob, Carol, Alice, Jerry]
在上面的示例中,我们通过指定 Comparator 对流中的元素进行了按照字符串长度的排序。使用 Comparator.comparing() 方法来创建一个比较器,该方法接受一个函数作为参数,用于从对象中提取一个可比较的键。在本例中,我们使用 String::length 方法作为提取键的函数。
-
-
peek
它允许你在 Stream 中的每个元素上执行一个操作,而不改变 Stream 的内容。也就是说,peek() 方法接收一个函数式接口参数,该接口将被应用于 Stream 中的每个元素,并返回一个 Stream。
下面是 peek 方法的语法:
Stream<T> peek(Consumer<? super T> action)
其中,
- T 表示 Stream 中的元素类型。
- action 表示要对每个元素执行的操作。
- Consumer 是一个函数式接口,它接收一个参数并返回 void。
以下是一个简单的示例,演示如何使用 peek 方法:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.stream() .peek(name -> System.out.println("Before map: " + name)) .map(String::toUpperCase) .peek(name -> System.out.println("After map: " + name)) .forEach(System.out::println);
在上面的示例中,创建了一个包含三个字符串的列表 names。接着,它调用 stream 方法将其转换为一个 Stream,然后调用 peek 方法,在每个元素上打印出 "Before map:" 和元素本身。接着,它调用 map 方法将元素转换为大写形式,然后再次调用 peek 方法,在每个元素上打印出 "After map:" 和转换后的元素。最后,它调用 forEach 方法,打印出 Stream 中的每个元素。
输出结果如下:
Before map: Alice After map: ALICE ALICE Before map: Bob After map: BOB BOB Before map: Charlie After map: CHARLIE CHARLIE
可以看到,在每个元素上调用了两次 peek() 方法,并打印出了 "Before map:" 和 "After map:" 两个字符串,以及元素本身和转换后的元素。
-
limit
它可以限制流中元素的数量,只保留指定数量的元素。
下面是 limit 方法的语法:
Stream<T> limit(long maxSize)
其中,maxSize 是要保留的元素数量。
以下是一个简单的示例,演示如何使用 limit 方法:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> result = numbers.stream() .limit(3) .collect(Collectors.toList()); System.out.println(result); // Output: [1, 2, 3]
在上面的示例中,我们首先将一个包含 5 个整数的列表转换为一个流。然后我们使用 limit 方法来限制只保留前 3 个元素。最后,我们使用 collect 方法将流转换为一个列表,并打印出结果。
注意,如果流中元素的数量少于 maxSize,那么 limit 方法不会产生任何效果。
-
skip
它可以用来跳过指定数量的元素,返回一个新的 Stream 对象。skip 方法可以用于所有类型的 Stream,包括基本类型和对象类型。
下面是 skip 方法的语法:
Stream<T> skip(long n)
其中,n 表示要跳过的元素数量。skip 方法返回一个新的 Stream 对象,它包含了原始 Stream 中跳过了前 n 个元素后剩余的元素。
以下是一个简单的示例,演示如何使用 skip 方法:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Stream<Integer> stream = numbers.stream(); // 跳过前 2 个元素 Stream<Integer> newStream = stream.skip(2); // 输出剩余的元素 newStream.forEach(System.out::println);
输出结果如下:
3 4 5
在上面的示例中,我们首先创建了一个包含 5 个整数的列表,然后通过 stream() 方法将其转换为一个 Stream 对象。然后,我们调用 skip() 方法,跳过前 2 个元素,并返回一个新的 Stream 对象。最后,我们使用 forEach() 方法遍历新的 Stream 对象,并输出剩余的元素。
-
parallel
它用于将流转换为并行流。使用 parallel 方法可以将一个流分成多个子流,让它们在不同的线程上并行执行操作,以提高处理效率
下面是 parallel 方法的语法:
Stream<T> parallel()
以下是一个简单的示例,演示如何使用 parallel 方法:
List<Integer> list = new ArrayList<>(); for (int i = 1; i <= 100; i++) { list.add(i); } Stream<Integer> stream = list.parallelStream(); stream.map(x -> x * 2) .forEach(System.out::println);
在上面的示例中,我们首先创建了一个包含100个元素的List集合。然后使用parallelStream方法将其转换为并行流。接着使用map方法对每个元素进行操作,将其乘以2。最后使用forEach方法输出结果。由于并行执行操作,所以输出结果的顺序可能会不同于原始集合中元素的顺序。
需要注意的是,并行流并不一定比顺序流更快。在某些情况下,顺序流可能更快,这取决于集合的大小和元素的复杂度。在使用parallel方法时,需要进行性能测试,以确定哪种方式更适合你的应用程序。
-
sequential
它可以将并行流转换为顺序流。
下面是 sequential 方法的语法:
Stream<T> sequential()
以下是一个简单的示例,演示如何使用 sequential 方法:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Stream<Integer> parallelStream = numbers.parallelStream(); // 创建并行流 Stream<Integer> sequentialStream = parallelStream.sequential(); // 将并行流转换为顺序流 sequentialStream.forEach(System.out::println); // 使用顺序流执行操作
在上面的示例中,我们首先使用parallelStream()方法创建一个并行流,然后使用sequential()方法将其转换为顺序流,并最终使用forEach()方法来迭代处理每个元素。
2.3 Stream的并行处理和性能优化
并行处理
并行处理是指将一个任务分解成多个子任务,同时执行这些子任务,最后将子任务的结果合并成一个最终结果。在Stream API中,可以通过parallel()方法将顺序流转换为并行流,从而实现并行处理。
使用并行流的好处在于可以充分利用多核CPU的优势,提高程序的运行效率。但是,在使用并行流时需要注意以下几点:
-
并行处理可能会带来线程安全问题,需要使用线程安全的数据结构或者保证数据的同步。
-
并行处理的效果不一定比顺序处理更好,甚至有可能会更差。这是因为并行处理需要进行任务的拆分、合并等操作,这些操作本身也会消耗一定的资源。因此,在使用并行流时需要进行实际的性能测试,确定是否能够获得更好的性能。
-
并行处理还可能会带来一些额外的开销,比如创建额外的线程、进行任务拆分、合并等操作。因此,在处理小数据量的集合时,使用并行流反而会降低程序的性能。
性能优化
-
避免使用过多的中间操作:Stream中的每个中间操作都会产生一个新的Stream对象,这些对象会对内存和性能造成额外的负担。因此,尽量避免使用过多的中间操作,只使用必要的中间操作。
-
使用并行流进行并行计算:Stream提供了并行流的支持,可以使用parallel()方法将顺序流转换为并行流。这可以让Stream在多核CPU上更快地运行,但也需要注意线程安全和同步问题。
-
使用原始类型流:Stream提供了原始类型流,如IntStream、LongStream和DoubleStream,可以避免自动装箱和拆箱的开销,提高性能。
-
使用合适的数据结构:Stream中使用的数据结构也会影响性能。对于数据量较小的情况,可以使用ArrayList或LinkedList等集合,而对于数据量较大的情况,可以使用HashSet或TreeSet等集合。
-
注意Stream的生命周期:在使用Stream时,要注意Stream的生命周期,及时关闭Stream以释放资源。可以使用try-with-resources语句或调用close()方法来关闭Stream。
-
尽可能使用基于索引的操作:在使用Stream时,尽可能使用基于索引的操作,如limit、skip、findFirst、findAny等操作,而不是基于谓词的操作,如filter、map等操作。
-
使用Stream的短路操作:Stream提供了一些短路操作,如findFirst、findAny、limit、skip等操作,可以在满足条件后立即终止Stream的处理,提高性能。
总之,使用Stream时,要注意性能方面的细节,合理地使用Stream的操作和数据结构,以提高程序的运行效率。
2.4 Stream的异常处理和错误调试
异常处理
try-catch块
在Stream的操作过程中,可以使用try-catch块来捕捉任何可能的异常。但是,这种方法可能会使代码显得冗长,不易读懂。
以下是一个简单的示例
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Eve");
try {
names.stream()
.map(name -> {
if (name.length() == 3) {
throw new Exception("Name length is 3");
}
return name.toUpperCase();
})
.forEach(System.out::println);
} catch (Exception e) {
System.err.println("Error occurred: " + e.getMessage());
}
在上面的示例中,我们使用try-catch块来捕获在map()操作中可能抛出的异常。在map()操作中,我们检查名字的长度是否为3,如果是,就抛出一个异常。然后,我们将名字转换为大写,并使用forEach()方法打印出来。
如果在map()操作中抛出异常,我们将通过catch块捕获该异常,并打印出错误信息。
注意,在使用流时,最好将异常处理放在尽可能靠近产生异常的地方,以便更容易地定位问题。在上面的示例中,我们将异常处理放在了 forEach 方法的外部。
使用Optional类
可以使用Optional类来避免在Stream API中出现空指针异常。例如,可以使用Optional类的map 方法来处理可能为null的值。
以下是一个简单的示例
List<String> stringList = Arrays.asList("a", null, "b", "c", null, "d");
stringList.stream()
.filter(Objects::nonNull) // 过滤掉 null 值
.map(str -> str.toUpperCase()) // 转换为大写
.forEach(str -> {
Optional.ofNullable(str)
.ifPresent(System.out::println); // 输出值
});
在上面的示例中,我们有一个包含字符串的列表,其中包括 null 值。我们使用 Stream API 过滤掉 null 值,然后将字符串转换为大写。最后,使用 Optional 类的 ofNullable 方法来检查字符串是否为空,如果不为空则输出。
使用 Optional 类可以帮助我们避免空指针异常,使代码更加健壮和可读。
使用自定义异常
可以在Stream API的操作中使用自定义异常。例如,在 map 操作中可以抛出自定义的CheckedException。
以下是一个简单的示例
import java.util.Arrays;
import java.util.List;
class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}
public class Example {
public static void main(String[] args) {
List<String> strings = Arrays.asList("foo", "bar", "baz");
try {
strings.stream()
.map(s -> {
if (s.equals("bar")) {
throw new MyCustomException("Encountered an unexpected value: " + s);
}
return s.toUpperCase();
})
.forEach(System.out::println);
} catch (MyCustomException e) {
System.err.println("Caught exception: " + e.getMessage());
}
}
}
在上面的示例中,我们创建了一个自定义的异常类 MyCustomException,它继承自标准的 Exception 类。在流的 map() 操作中,我们检查了每个字符串是否等于 "bar",如果是,我们就抛出了一个自定义异常。
在主方法中,我们使用了一个 try-catch 块来捕获自定义异常。如果任何一个字符串等于 "bar",流操作就会抛出这个异常,然后我们就可以在 catch 块中处理它,例如打印错误消息。
请注意,我们在 map 操作中抛出异常时,流操作将立即停止,并且不会继续对流中的其他元素进行处理。
错误调试
使用 peek 方法
peek() 方法是一个调试工具,可以用来查看流中的元素,您可以在流中的任何位置添加 peek() 方法来查看流的内容,并检查每个元素的状态和值。
以下是一个简单的示例
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.peek(System.out::println) // 打印每个元素
.mapToInt(Integer::intValue)
.sum();
System.out.println("Sum = " + sum);
在上面的示例中,我们使用 peek 方法打印每个元素的值。这将允许我们在处理流时查看流中的元素,并在需要调试时检查它们的值。在上面的示例中,我们将流中的每个元素打印出来,并在最后打印出它们的总和。
使用 forEach 方法
forEach 方法可以遍历流中的元素,并执行一个操作,您可以使用 forEach 方法来查看流中的每个元素的值,以及执行操作的状态。
以下是一个简单的示例
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().forEach(System.out::println);
这将打印出以下内容:
1
2
3
4
5
在上面的示例中,我们使用 forEach 方法将每个元素打印到控制台。
使用 collect 方法
collect 方法可以将流中的元素收集到一个集合中,您可以使用 collect 方法将流中的元素转换为集合,并检查集合中的元素值。
以下是一个简单的示例
List<String> uppercaseList = list.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
在上面的示例中,我们有一个名为 list 的字符串列表,并且将它们转换为大写形式并收集到另一个列表中。
使用断言方法
我们可以使用Java中的断言方法来辅助调试
以下是一个简单的示例
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(-2);
numbers.add(3);
numbers.add(4);
numbers.add(-5);
numbers.stream()
.mapToInt(n -> {
assert n >= 0 : "Negative number found!";
return n * n;
})
.forEach(System.out::println);
在上面的示例中,我们使用了assert方法来判断数字是否为非负数,如果是负数,会抛出一个带有"Negative number found!"信息的AssertionError。这样,当流式操作处理到负数时,程序将抛出该异常,从而帮助我们快速发现问题。
使用日志输出
使用日志输出: 您可以使用日志框架,如 Log4j 或 SLF4J,来输出调试信息。在流式代码中,使用日志输出可以记录流中每个元素的值和状态。
以下是一个简单的示例,假设我们有一个Person类,其中包含一个age属性和一个name属性,我们想要使用Java Stream来过滤年龄大于等于18岁的人,并将其名称打印到日志中。
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public static void main(String[] args) {
Logger logger = Logger.getLogger("Person");
List<Person> people = Arrays.asList(
new Person(10, "Alice"),
new Person(20, "Bob"),
new Person(30, "Charlie")
);
people.stream()
.filter(person -> person.getAge() >= 18)
.map(Person::getName)
.forEach(name -> logger.info("Name: " + name));
}
}
在上面的示例中,我们使用Logger类创建一个名为"Person"的Logger实例。然后,我们创建一个包含三个Person对象的List,使用Java Stream过滤并映射这些对象,最终将名称打印到日志中。
在运行代码时,可以在控制台或日志文件中看到日志输出。如果存在错误或异常,可以查看日志输出来了解发生了什么并调试代码。
使用调试器
使用调试器: 当您无法解决问题时,您可以使用调试器来检查代码的执行状态,并查看变量的值。在 Java 中,您可以使用 Eclipse、NetBeans 或 IntelliJ IDEA 等集成开发环境来调试代码。
第三章:Optional的进阶用法
2.1 Optional的链式调用和方法组合
链式调用
链式调用是指将多个Optional对象链接在一起,从而避免在每个Optional对象上使用多个if语句。例如
Optional<String> str = Optional.of("Hello");
Optional<String> result = str.map(String::toUpperCase).flatMap(s -> Optional.of("PREFIX " + s));
在上面的代码中,首先创建了一个包含字符串“Hello”的Optional对象。然后,使用 map 方法将字符串转换为大写,并使用 flatMap 方法将新的Optional对象链接到结果中。
方法组合
方法组合可以将多个Optional对象组合在一起,并使用特定的函数来处理它们的值。例如:
Optional<String> optional1 = Optional.of("hello");
Optional<String> optional2 = Optional.of("world");
Optional<String> result = optional1.flatMap(s1 -> optional2.map(s2 -> s1 + " " + s2));
result.ifPresent(System.out::println);
在上面的代码中,我们创建了两个包含非空值的 Optional 对象。然后,我们使用 flatMap 方法和lambda表达式来将它们组合起来,并将它们的值连接成一个新的字符串。最后,我们使用 ifPresent 方法来打印结果字符串"hello world"。
需要注意的是,方法组合只有在所有的Optional对象都包含非空值时才会执行。如果任何一个Optional对象包含了空值,则整个方法组合都不会执行。
2.2 Optional的过滤和映射
过滤
主要使用到的是 filter 方法,用于对值进行过滤
下面是 filter 方法的语法:
Optional<T> filter(Predicate<? super T> predicate)
其中,
- T 表示可包含的值的类型。
- Predicate 表示一个函数接口,该接口用于对值进行过滤。
filter 方法接受一个 Predicate 参数,该参数用于对值进行判断。如果值满足 Predicate 中定义的条件,则返回一个包含该值的 Optional 对象;否则返回一个空的 Optional 对象。
以下是一个简单的示例,假设有一个Person类,有两个属性:name和age。现在要筛选年龄在18岁以上的人,可以使用Optional类的filter()方法:
import java.util.Optional;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public static void main(String[] args) {
Optional<Person> personOptional = Optional.of(new Person("John", 20));
personOptional.filter(person -> person.getAge() >= 18)
.ifPresent(person -> System.out.println(person.getName()));
}
}
在上面的示例中,首先使用Optional.of()方法创建了一个Optional对象,包含了一个年龄为20岁的Person对象。然后使用filter()方法对该对象进行筛选,筛选条件为年龄大于等于18
映射
主要使用到的是 map 方法,用于对值进行映射
下面是 map 方法的语法:
<U> Optional<U> map(Function<? super T, ? extends U> mapper)
其中,Function<? super T, ? extends U>是一个函数式接口,它将类型T的对象映射为类型U的对象。在 map 方法中,mapper参数就是一个实现了Function接口的Lambda表达式,用于对Optional对象进行转换操作。
以下是一个简单的示例
Optional<String> optional = Optional.of("Hello, world!");
Optional<Integer> lengthOptional = optional.map(String::length);
lengthOptional.ifPresent(System.out::println); // 输出 13
在上面的示例中,我们首先创建了一个包装字符串"Hello, world!"的Optional对象。然后,我们使用map方法将字符串转换为其长度,并将结果包装在一个新的Optional对象,使用 ifPresent 方法将有值的 Optional 对象输出。
2.3 Optional的orElse和orElseGet方法的区别
orElse
它用于检索 Optional 对象中包含的值,或者如果 Optional 对象为空(即它不包含非空值)则检索默认值。
下面是 orElse 方法的语法:
T orElse(T other)
以下是一个简单的示例。
Optional<String> optional = Optional.ofNullable(null);
String result = optional.orElse("default");
System.out.println(result); // 输出 "default"
在上面的示例中,Optional对象被创建并设置为null,orElse方法返回了指定的默认值"default",因为容器为空。如果Optional对象中存在一个非空值,orElse方法将返回该值。
orElseGet
如果Optional对象为空,orElseGet方法可以提供一个替代值,否则返回Optional对象包含的值。
下面是 orElseGet 方法的语法:
<X extends Throwable> T orElseGet(Supplier<? extends T> supplier) throws X
其中:
- supplier:提供替代值的Supplier函数式接口对象。
- 如果Optional对象不为空,返回Optional对象包含的值。
- 如果Optional对象为空,返回通过Supplier函数式接口提供的替代值。
以下是一个简单的示例。
Optional<String> optional = Optional.ofNullable(null);
String result = optional.orElseGet(() -> "default value");
System.out.println(result); // 输出: "default value"
在上面的例子中,由于optional对象为空,orElseGet方法调用了提供的Supplier生成默认值"default value"。
区别
orElse()方法总是返回一个默认值,而orElseGet()方法只有在Optional为空时才会执行传递的Supplier。因此,在使用orElseGet()方法时,传递的Supplier的计算成本可能比orElse()方法低得多,因为它只在需要时才会执行。
2.4 Optional的异常处理和错误调试
异常处理
Optional类提供了以下异常处理方法:
-
isPresent 方法:用于检查 Optional 实例是否包含非null的值。如果包含值,则返回true,否则返回false。
-
ifPresent 方法:如果 Optional 实例包含非null的值,则调用指定的消费函数。否则什么都不做。
-
orElse 方法:如果 Optional 实例包含非null的值,则返回该值。否则返回一个默认值。
-
orElseGet 方法:如果 Optional 实例包含非null的值,则返回该值。否则使用指定的供应函数生成一个默认值并返回该值。
-
orElseThrow 方法:如果 Optional 实例包含非null的值,则返回该值。否则抛出指定的异常。
以下是一个简单的示例
Optional<Integer> optional = Optional.ofNullable(null);
// 使用orElse()方法设置默认值
int result1 = optional.orElse(0);
// 使用orElseGet()方法设置默认值
int result2 = optional.orElseGet(() -> 0);
try {
// 使用orElseThrow()方法抛出异常
int result3 = optional.orElseThrow(() -> new NullPointerException("Value is null"));
} catch (NullPointerException ex) {
System.out.println(ex.getMessage());
}
错误调试
打印日志
你可以使用 Java.util.logging 或 log4j 等日志库来记录 Optional 对象的状态和流程。这可以帮助你在程序中确定 Optional 对象的值和流程。
可以使用 ifPresent 方法打印日志
以下是一个简单的示例
Optional<String> optionalValue = Optional.ofNullable(getValue());
optionalValue.ifPresent(value -> System.out.println("Value: " + value));
在上面的示例中,Optional 对象 optionalValue 可能为 null,我们使用 ofNullable() 方法来创建一个 Optional 对象。然后,我们使用 ifPresent() 方法来检查该对象是否包含非空值。如果对象包含非空值,则会执行指定的代码块并打印日志。在这个示例中,我们使用 lambda 表达式来打印值。
如果我们需要在日志中输出更多的信息,可以使用字符串拼接或格式化。例如:
Optional<String> optionalValue = Optional.ofNullable(getValue());
optionalValue.ifPresent(value -> System.out.println("Value: " + value + ", length: " + value.length()));
在上面的示例中,我们使用字符串拼接来输出值和长度。
另外,如果我们需要在 Optional 对象为空时打印日志,可以使用 orElseGet() 方法来提供一个默认值。例如:
Optional<String> optionalValue = Optional.ofNullable(getValue());
String value = optionalValue.orElseGet(() -> {
System.out.println("Value is null");
return "default value";
});
在上面的示例中,如果 optionalValue 为 null,则会打印一条日志并返回默认值 "default value"。
使用 assert 断言
可以使用 Java 的 assert 语句来确保 Optional 对象的值不为空。如果 Optional 对象为空,将会抛出 AssertionError 异常。这可以帮助你快速定位程序中的问题。
假设有以下Java方法:
public Optional<Integer> divide(int a, int b) {
assert b != 0 : "Cannot divide by zero";
return Optional.of(a / b);
}
该方法使用了Java 8的Optional类来表示可能为空的结果,并使用assert断言确保参数b不为零。
为了调试这个方法,我们可以使用以下代码示例:
public static void main(String[] args) {
Optional<Integer> result = divide(10, 0);
if (result.isPresent()) {
System.out.println("Result: " + result.get());
} else {
System.out.println("Result is empty");
}
}
在这个示例中,我们调用了divide方法,并将结果存储在result变量中。然后我们使用isPresent方法检查结果是否存在,如果存在,我们打印结果,否则打印结果为空。
由于我们在divide方法中使用了assert断言,如果我们调用该方法并将零作为第二个参数传递,程序将会抛出一个AssertionError异常。这个异常可以帮助我们找到程序中的错误,并且可以通过在运行程序时添加-ea选项来启用断言。
使用 IntelliJ IDEA 的调试工具
如果你在使用 IntelliJ IDEA 编辑器,可以在代码中设置断点,并使用调试工具来检查 Optional 对象的状态。在执行程序时,代码会在断点处停止,你可以查看 Optional 对象的内容,并逐步执行代码来检查程序的流程。
第四章:Stream和Optional在项目中的实践
4.1 使用Stream进行数据处理和转换
过滤数据
使用Stream的 filter 方法可以根据指定的条件过滤数据。
以下是一个简单的示例
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // output [2, 4, 6, 8, 10]
在上面的示例中,我们首先将 numbers 列表转换成一个流,然后使用 filter 方法传递一个 Lambda 表达式作为参数。该 Lambda 表达式使用模运算符来判断流中的每个整数是否是偶数。最后,我们使用 collect 方法将过滤后的结果收集到一个新的列表中。
映射数据
使用Stream的 map 方法可以将一种类型的数据转换为另一种类型的数据。
下面是一个简单的示例,展示如何使用map()方法将一个字符串列表中的每个字符串转换为大写形式:
List<String> words = Arrays.asList("hello", "world", "java", "stream");
List<String> capitalizedWords = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(capitalizedWords);
在上面的示例中,我们首先创建了一个字符串列表,然后使用stream()方法将它转换为一个流。接着,我们使用map 方法和String::toUpperCase方法引用来将每个字符串转换为大写形式。最后,我们使用 collect 方法将新的流转换回List并输出结果。
数据分组和聚合
使用Stream的groupingBy()和collect()方法可以将数据按照指定的属性进行分组,并进行聚合计算。
数据分组
Stream API提供了Collectors.groupingBy()方法,该方法可用于将流中的元素按照指定的条件分组。该方法返回一个Map对象,其中键为分组条件,值为属于该分组的元素列表。
下面是 Collectors.groupingBy 方法的语法:
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
其中
- classifier:用于将元素映射为分组依据的函数
- Collector:用于将元素按照分组依据进行分组的Collector对象
以下是一个简单的示例,假设有一个Person类,包含name和age属性,现在需要将一组Person对象按照年龄进行分组。
import java.util.*;
import java.util.stream.Collectors;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 20),
new Person("Bob", 25),
new Person("Charlie", 30),
new Person("David", 25),
new Person("Ella", 20)
);
Map<Integer, List<Person>> groupedByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println(groupedByAge);
}
}
输出结果:
{20=[Person{name='Alice', age=20}, Person{name='Ella', age=20}],
25=[Person{name='Bob', age=25}, Person{name='David', age=25}],
30=[Person{name='Charlie', age=30}]}
在上面的示例中,我们使用Person::getAge方法作为groupingBy()方法的参数,将Person对象按照年龄进行分组,最终得到了一个Map对象,其中键是年龄,值是该年龄下的所有Person对象。
数据聚合
以下是一个简单的示例,假设有一个包含 Employee 对象的 List,每个 Employee 对象包含姓名(name)、年龄(age)、性别(gender)、职位(position)和工资(salary)等属性,我们要按照职位对所有员工的工资进行求和:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Employee {
private String name;
private int age;
private String gender;
private String position;
private double salary;
public Employee(String name, int age, String gender, String position, double salary) {
this.name = name;
this.age = age;
this.gender = gender;
this.position = position;
this.salary = salary;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getGender() {
return gender;
}
public String getPosition() {
return position;
}
public double getSalary() {
return salary;
}
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("Alice", 25, "Female", "Engineer", 5000));
employees.add(new Employee("Bob", 30, "Male", "Engineer", 6000));
employees.add(new Employee("Charlie", 35, "Male", "Manager", 7000));
employees.add(new Employee("David", 40, "Male", "Manager", 8000));
Map<String, Double> result = employees.stream()
.collect(Collectors.groupingBy(Employee::getPosition, Collectors.summingDouble(Employee::getSalary)));
System.out.println(result); // output {Engineer=11000.0, Manager=15000.0}
}
}
在上面的示例中,我们使用了 Stream 的 groupingBy 方法按照职位对员工进行分组,然后使用 summingDouble 方法对每组员工的工资进行求和。最后,将结果保存在一个 Map 中并输出。
排序数据
使用Stream的sorted()方法可以根据指定的条件对数据进行排序。
以下是一个简单的示例
假设有一个Person类,包含name和age属性:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
我们可以使用以下代码将一个Person类型的List按照age属性进行升序排序:
List<Person> people = Arrays.asList(
new Person("John", 25),
new Person("Bob", 30),
new Person("Alice", 20)
);
List<Person> sortedPeople = people.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(Collectors.toList());
sortedPeople.forEach(System.out::println);
在上面的示例中,我们使用Stream的sorted方法,并传入一个Comparator,该Comparator基于Person对象的age属性进行比较。
注意,该排序操作并不会修改原始的List,而是返回一个新的List,其中的元素按照指定的排序顺序排列。最后,我们使用forEach方法将排好序的List中的元素逐个打印出来。
数据去重
使用Stream的 distinct 方法可以去除数据中的重复元素。
以下是一个简单的示例
List<String> listWithDuplicates = Arrays.asList("apple", "banana", "orange", "apple", "orange");
List<String> listWithoutDuplicates = listWithDuplicates
.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(listWithoutDuplicates); // 输出 [apple, banana, orange]
在上面的示例中,我们首先创建了一个包含重复数据的列表。然后使用 stream() 方法将列表转换为一个 Stream 对象,接着使用 distinct() 方法进行去重操作,最后使用 collect(Collectors.toList()) 方法将去重后的数据转换回一个列表。
注意,去重操作需要根据数据类型的 equals() 方法进行比较,因此如果列表中包含自定义的对象,需要确保这些对象正确地实现了 equals() 方法。
统计数据
使用Stream的count()、min()、max()、sum()和average()等方法可以对数据进行统计计算。
以下是一个简单的示例,假设有一个包含员工信息的列表,每个员工都有一个薪水属性,我们可以使用Java 8 Stream来统计这些员工的薪水总和、平均值、最高值和最低值:
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("John", new BigDecimal("5000.00")),
new Employee("Mary", new BigDecimal("6000.00")),
new Employee("Bob", new BigDecimal("4000.00")),
new Employee("Alice", new BigDecimal("7000.00"))
);
BigDecimal totalSalary = employees.stream()
.map(Employee::getSalary)
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Total salary: " + totalSalary);
BigDecimal averageSalary = employees.stream()
.map(Employee::getSalary)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(employees.size()), BigDecimal.ROUND_HALF_UP);
System.out.println("Average salary: " + averageSalary);
BigDecimal maxSalary = employees.stream()
.map(Employee::getSalary)
.max(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
System.out.println("Max salary: " + maxSalary);
BigDecimal minSalary = employees.stream()
.map(Employee::getSalary)
.min(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
System.out.println("Min salary: " + minSalary);
}
static class Employee {
private String name;
private BigDecimal salary;
public Employee(String name, BigDecimal salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public BigDecimal getSalary() {
return salary;
}
}
}
输出结果:
Total salary: 22000.00
Average salary: 5500.00
Max salary: 7000.00
Min salary: 4000.00
多线程处理数据
使用Stream的parallel()方法可以启用多线程对数据进行处理,以提高处理效率。
以下是一个简单的示例
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
public class StreamMultiThreadExample {
public static void main(String[] args) {
// 创建一个 ForkJoinPool,用于执行并行操作
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 创建一个列表来保存要处理的数据
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 将列表转换为并行流
forkJoinPool.submit(() ->
numbers.parallelStream()
// 使用 map 操作将数字乘以 2
.map(n -> n * 2)
// 使用 forEach 操作输出结果
.forEach(System.out::println)
).join();
// 关闭 ForkJoinPool
forkJoinPool.shutdown();
}
}
在上面的示例中,我们创建了一个 ForkJoinPool,它是一个 Java 并发库中的类,用于执行并行操作。我们使用 parallelStream() 方法将列表转换为并行流,然后使用 map() 操作将每个数字乘以 2。最后,我们使用 forEach() 操作输出结果。
需要注意的是,在使用并行流时,我们必须确保操作是线程安全的。例如,在上面的代码中,我们使用了不可变的 List 和不可变的数字,因此它是线程安全的。如果要处理可变状态,请使用线程安全的数据结构或同步操作。
4.2 使用Optional进行空值处理和默认值设置
空值处理
Optional提供了一种优雅且安全的方法来处理可能为空的值,可以避免出现NullPointerException等错误,并提高代码的可读性和维护性。
以下是一个简单的示例,
-
创建Optional对象
Optional<String> optional = Optional.of("hello"); Optional<String> emptyOptional = Optional.empty();
-
检查是否存在值
if (optional.isPresent()) { String value = optional.get(); // do something with the value }
-
如果存在值,则获取该值
String value = optional.orElse("default value");
-
如果存在值,则执行一些操作
optional.ifPresent(val -> System.out.println("Value is present: " + val));
-
使用Optional链式调用
Optional<User> userOptional = Optional.ofNullable(user); String email = userOptional.map(User::getEmail) .orElse("default@email.com");
在上面的示例中,map方法会将User对象映射为其email属性,并将结果存储在一个新的Optional对象中。如果原始的Optional对象为空,则将返回默认的电子邮件地址。
默认值设置
以下是一个简单的示例
import java.util.Optional;
public class OptionalDemo {
public static void main(String[] args) {
String str = null;
Optional<String> optionalStr = Optional.ofNullable(str);
String defaultStr = "default";
String resultStr = optionalStr.orElse(defaultStr);
System.out.println(resultStr); // 输出结果为 "default"
}
}
在上面的示例中,我们首先将一个可能为空的字符串str转换成一个Optional对象optionalStr,然后使用orElse方法设置一个默认值defaultStr,最后将返回值赋值给resultStr变量。如果optionalStr对象不为空,则返回optionalStr的值,否则返回默认值defaultStr。
你也可以使用orElseGet方法,它的参数是一个Supplier对象,用于提供默认值。下面是一个使用orElseGet的示例:
import java.util.Optional;
public class OptionalDemo {
public static void main(String[] args) {
String str = null;
Optional<String> optionalStr = Optional.ofNullable(str);
String defaultStr = "default";
String resultStr = optionalStr.orElseGet(() -> getDefaultStr());
System.out.println(resultStr); // 输出结果为 "default"
}
public static String getDefaultStr() {
System.out.println("get default str");
return "default";
}
}
在上面的示例中,我们定义了一个静态方法getDefaultStr,用于提供默认值。在orElseGet方法中传递该方法的引用,当optionalStr对象为空时,会调用该方法来获取默认值。由于getDefaultStr方法是一个耗时的操作,我们在控制台打印了一条消息来验证它是否被调用了。
4.3 Stream和Optional在数据库查询和结果处理中的应用
Stream的应用
在数据库查询中,通常会使用JDBC(Java Database Connectivity)来执行SQL语句并返回结果集。返回的结果集可以被转换为一个Java集合,然后使用Stream来处理。
例如,假设我们有一个名为"person"的表,其中包含id、name和age列。我们可以使用JDBC来执行一个查询语句来获取所有人的名字:
Connection conn = DriverManager.getConnection(url, username, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT name FROM person");
然后我们可以将结果集转换为一个Java集合并使用Stream来处理:
List<String> names = new ArrayList<>();
while (rs.next()) {
String name = rs.getString("name");
names.add(name);
}
names.stream().forEach(System.out::println);
Optional的应用
在数据库查询中,通常会使用JDBC来执行SQL语句并返回结果集。返回的结果集可能包含零行或多行,因此需要使用Optional来处理结果集中可能为空的情况。
例如,假设我们有一个名为"person"的表,其中包含id、name和age列。我们可以使用JDBC来执行一个查询语句来获取指定ID的人的名字:
Connection conn = DriverManager.getConnection(url, username, password);
PreparedStatement stmt = conn.prepareStatement("SELECT name FROM person WHERE id = ?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
然后我们可以使用Optional来处理可能为空的结果集:
String name = null;
if (rs.next()) {
name = rs.getString("name");
}
Optional<String> optionalName = Optional.ofNullable(name);
optionalName.ifPresent(System.out::println);
在上面的代码中,我们检查结果集是否包含行并将结果集中的名称赋值给name变量。然后我们使用Optional.ofNullable方法来创建一个Optional对象,如果名称为null则返回一个空的Optional对象。最后,我们使用ifPresent方法来检查Optional对象是否包含值并输出名称。
支持Stream和Optional的数据访问库
Java中有许多支持Stream和Optional的数据库访问库。以下是一些常见的库:
-
Spring Data JPA:Spring Data JPA是一个用于简化JPA开发的库。它支持使用Stream和Optional来处理返回值。
-
Hibernate:Hibernate是一个流行的ORM库,它提供了对Stream和Optional的支持。
-
Jooq:Jooq是一个用于构建类型安全SQL查询的库。它支持使用Stream和Optional来处理返回值。
-
MyBatis:MyBatis是一个轻量级的ORM框架,它支持使用Stream和Optional来处理返回值。
这些库都有自己的特点和使用方式,你可以根据自己的需求来选择合适的库。
4.4 Stream和Optional在Web应用和API设计中的应用
Stream
在Web应用和API设计中,Stream常用于:
- 数据查询和筛选:通过使用Stream的过滤、映射和排序等方法,可以轻松地查询和筛选大量数据,从而实现更高效的数据处理。
- 数据转换:通过使用Stream的映射方法,可以将集合中的元素转换为其他类型的对象,从而实现更复杂的数据转换。
- 并行处理:使用Stream可以轻松地将数据并行处理,从而提高应用程序的性能。
Optional
在Web应用和API设计中,Optional常用于:
- API设计:使用Optional可以使API更清晰和易于使用。例如,返回Optional类型的方法可以明确地表明返回值可能为空,并且可以避免出现空值异常。
- 数据查询:通过使用Optional可以更方便地处理可能为空的查询结果。
- 配置处理:Optional可以帮助开发人员更容易地处理可能为空的配置参数,从而提高代码的健壮性
Java 支持使用 Option Stream 映射的 MVC 框架
Java 支持使用 Option Stream 映射的 MVC 框架有多种选择,以下是其中一些常用的框架:
-
Spring Framework:Spring Framework 是 Java 开发中最流行的 MVC 框架之一。它支持使用 Java 8 的 Option Stream 来进行映射,例如使用 Java 8 的 Lambda 表达式来处理请求和响应。
-
Play Framework:Play Framework 是一个基于 Java 和 Scala 的 Web 应用程序框架,支持使用 Option Stream 进行请求路由映射。
-
Spark Framework:Spark Framework 是一个轻量级的 Web 应用程序框架,它支持使用 Option Stream 进行路由映射。
这些框架都是在 Java 开发中非常流行的 MVC 框架,它们提供了一种方便的方法来处理 HTTP 请求和响应,并使用 Option Stream 进行请求路由映射。
第五章:Stream和Optional的最佳实践和注意事项
5.1 如何避免Stream和Optional的误用和滥用
ream和Optional可以显著提高代码的可读性、简洁性和可维护性。但是,它们也容易被滥用或误用,可能会导致性能问题或产生意想不到的错误。以下是一些建议,帮助您避免 Stream 和 Optional 的误用和滥用:
-
避免过度使用 Stream 和 Optional。Stream 和 Optional 是用来简化代码、增强可读性和可维护性的,而不是用来取代所有循环和判断语句的。在某些情况下,简单的循环或 if 语句可能比使用 Stream 和 Optional 更加清晰和易于理解。
-
尽量避免在复杂表达式中使用 Optional。尽管 Optional 有助于减少代码中的 null 检查和异常,但在复杂表达式中使用 Optional 会导致代码变得难以阅读和理解。
-
不要在流中使用过多的 map() 和 filter() 操作。虽然这些操作在某些情况下很有用,但是在处理大量数据时,它们可能会导致性能问题。因此,应该谨慎使用它们,并使用更高效的操作来处理数据,例如 flatMap() 或 reduce()。
-
避免将 Stream 转换为集合。Stream 在处理大量数据时非常高效,但一旦将其转换为集合,它的优势就会丧失。如果需要对 Stream 进行聚合操作,可以考虑使用 reduce() 方法,而不是将其转换为集合。
-
避免将 Optional 用于集合类型。尽管 Optional 可以用于包装单个值,但是它不适用于集合类型。如果需要表示空集合,应该使用空集合而不是 Optional。
-
使用 Stream 和 Optional 的正确方式。了解 Stream 和 Optional 的正确使用方式非常重要。应该熟悉它们的 API,以及它们的限制和适用范围。可以通过阅读官方文档和参考资料来了解它们的使用方式。
总之,Stream 和 Optional 是非常强大的 API,但是必须小心谨慎地使用它们,以避免产生意想不到的错误和性能问题。
5.2 Stream和Optional在团队合作和代码维护中的应用
团队合作
在团队合作中,Stream和Optional可以发挥以下作用:
-
提高代码的可读性和可维护性:它们提供了一种新的编程方式,可以使代码更加简洁和易于理解。使用这些特性可以使代码更易于维护和扩展。
-
减少空指针异常的发生:Optional可以帮助我们避免空指针异常的发生,因为它强制要求我们显式地处理null值的情况。这可以帮助团队成员编写更健壮的代码。
-
改善代码的性能:Stream可以提高代码的性能,因为它可以利用多核CPU并行处理数据。在处理大量数据的情况下,使用Stream可以更快地完成任务。
-
促进代码的复用:使用Stream和Optional可以将代码分解为小的、可复用的组件。这些组件可以在不同的上下文中重复使用,从而提高了代码的复用性。
-
促进团队成员的学习和交流:它们提供了一种新的编程范式。通过使用这些特性,团队成员可以学习新的编程技术,分享彼此的经验和知识,促进团队成员之间的学习和交流。
在团队合作中,Stream和Optional是Java中非常有用的工具,可以帮助开发人员编写更简洁、可读性更高的代码。以下是它们在团队合作中的一些应用:
-
使用Stream来处理集合数据
Stream提供了一种更直观、更优雅的方式来处理集合数据。在团队合作中,使用Stream可以使代码更易于理解和维护。例如,一个开发人员可以使用Stream来过滤和排序一个列表,并将结果传递给另一个开发人员进行进一步处理。
-
使用Optional来避免空指针异常
Optional可以帮助开发人员更好地处理可能为空的值,避免空指针异常。在团队合作中,使用Optional可以使代码更健壮,更容易阅读和理解。例如,一个开发人员可以使用Optional来处理返回可能为空的方法,而另一个开发人员可以在不引发异常的情况下继续处理结果。
-
与代码规范相结合使用
在团队合作中,遵循一致的代码规范是非常重要的。使用Stream和Optional可以与代码规范相结合,使代码更易于阅读和理解。例如,可以使用Stream的一些规范来遍历集合数据,并使用Optional的一些规范来处理可能为空的值。
-
使用代码审查和重构
在团队合作中,代码审查和重构是非常重要的。使用Stream和Optional可以帮助开发人员编写更简洁、可读性更高的代码,从而更易于进行代码审查和重构。例如,使用Stream可以帮助开发人员更好地组织代码,而使用Optional可以帮助开发人员更好地处理可能为空的值。
总之,在团队合作中,Stream和Optional是非常有用的工具,可以帮助开发人员编写更简洁、可读性更高的代码,从而使代码更易于理解和维护。
代码维护
在代码维护中,Stream和Optional可以发挥以下作用:
-
在代码维护中,使用Stream可以让代码更加易读易维护,因为它的操作会按照顺序执行,而不需要手动控制每个操作的执行顺序。
-
在代码维护中,使用Optional可以让代码更加健壮,因为它可以明确地表示一个值是否为空,并在使用时进行必要的处理。同时,使用Optional也可以让代码更加易读易维护,因为它明确了程序员对于空值的处理逻辑,从而减少了代码中的歧义和不确定性。
他们可以让代码更加简洁、易读、易维护、健壮。在编写代码时,我们应该根据具体的场景选择使用它们,以提高代码质量和可维护性。
在代码维护中,它们可以帮助你更加简洁和安全地处理集合和可空对象。
- 使用 Stream 简化集合处理逻辑,提高代码的可读性和可维护性。
- 使用 Optional 处理可能为 null 的对象,避免 NullPointerException 异常和冗长的 null 判断。
- 在使用 Stream 和 Optional 的过程中,遵循良好的编码规范和最佳实践,确保代码的可读性、可维护性和性能。
5.3 Stream和Optional的性能和资源消耗的考虑
Stream的性能和资源消耗
Stream的性能和资源消耗取决于处理的数据量和流水线的操作。在处理小规模数据集时,Stream的性能和资源消耗相对较低,但是在处理大规模数据集时,Stream可能会占用较高的内存和CPU资源,因为Stream会将数据集加载到内存中进行处理,而且可能会产生大量的中间结果,增加GC的压力。
此外,Stream还有并行流和顺序流两种方式,其中并行流可以将数据集划分为多个子集进行并行处理,从而提高处理效率,但是并行流的使用需要考虑线程安全和资源竞争等问题,而顺序流则是单线程处理数据集,因此在数据集较小或操作复杂度较高的情况下,顺序流可能会比并行流更适合。
Optional的性能和资源消耗
Optional的性能和资源消耗相对较低,因为它只是对一个可能为空的值进行了包装,不会产生额外的内存和CPU开销。在使用Optional时,需要注意避免使用过多的map和flatMap等操作,因为这些操作可能会产生不必要的Optional对象,增加GC的压力。
总的来说,它们的性能和资源消耗与具体的使用场景和数据规模有关,需要根据实际情况进行选择和优化。
5.4 Stream和Optional在Java 9和更高版本中的变化和更新
在Java 9中,Stream和Optional都得到了一些变化和更新,以下是它们的详细说明:
Stream
Java 9中引入了几种新的Stream操作,包括:
-
takeWhile():从流的开头开始,只要元素满足指定的条件,就会一直获取下去,直到第一个不满足条件的元素出现为止。
-
dropWhile():与takeWhile()相反,从流的开头开始,只要元素满足指定的条件,就会一直跳过,直到第一个不满足条件的元素出现为止。
-
ofNullable():如果提供的值不是null,就返回一个包含单个元素的流,否则返回一个空流。
-
iterate():允许提供一个初始值和一个UnaryOperator(一元操作符),生成一个无限流。
此外,Java 9还引入了一种新的Stream实现方式:流的建造者(Stream.Builder)。这使得创建流变得更加灵活和高效。
Optional
Java 9中对Optional的更新主要涉及到性能优化和API扩展。其中一些变化包括:
- ifPresentOrElse():允许提供两个操作,一个在Optional包含值的情况下执行,一个在不包含值的情况下执行。
- or()和orElseThrow():允许在Optional为空时提供备用值或抛出自定义异常。
- stream():将Optional转换为包含一个或零个元素的流。
- rElse()和stream()操作的性能得到了显著的提高,这使得使用Optional变得更加高效。
第六章:总结和展望
6.1 Stream和Optional在Java开发中的重要性和未来发展趋势
重要性
Stream的重要性
它提供了一种函数式编程的方式来操作集合类数据。通过Stream,开发人员可以更加简单和方便地对集合进行筛选、映射、过滤等操作,同时还可以利用并行处理提高程序的性能。
Stream的优点如下:
-
更加简洁的代码:Stream的操作可以链式调用,使代码更加简洁易懂。
-
更加函数式的编程风格:Stream的操作是基于函数式编程的,这使得代码更加清晰、易于维护。
-
更加高效的并行处理:Stream可以通过并行处理来提高程序的性能,特别是处理大量数据时更为明显。
Optional的重要性
它提供了一种安全的方式来处理可能为空的对象。在Java中,如果一个变量可能为空,开发人员通常会使用null来表示,但这很容易导致NullPointerException异常,给程序带来安全隐患。Optional则提供了一种更加安全的方式来处理可能为空的对象,避免了NullPointerException异常的出现。
Optional的优点如下:
-
更加安全的编程方式:Optional提供了一种更加安全的方式来处理可能为空的对象,避免了NullPointerException异常的出现。
-
更加简洁的代码:Optional的操作可以链式调用,使代码更加简洁易懂。
-
更加函数式的编程风格:Optional的操作是基于函数式编程的,这使得代码更加清晰、易于维护。
总的来说,Stream和Optional是Java中非常重要的特性,它们能够大大提高开发人员的编程效率和代码质量,同时也能够提高程序的性能和安全性。
未来发展趋势
从当前的趋势来看,Stream和Optional将会在Java开发中继续发挥重要的作用,并逐渐得到更广泛的应用。
-
更多的Java库和框架将开始采用Stream和Optional作为API的一部分,从而使得这些特性更加普及。
-
随着Java 9、Java 10等新版本的发布,Stream和Optional等特性可能会得到更多的改进和扩展,以满足开发者日益增长的需求。
-
开发者将会更加关注Stream和Optional的性能和效率,并且探索如何在自己的代码中使用这些特性来提高代码的质量和效率。
-
在大数据和人工智能等领域,Stream和Optional等特性已经被广泛应用,未来可能会有更多的应用场景涉及到这些特性。
总之,Stream和Optional等特性是Java开发中非常重要的组成部分,它们将会在未来的发展中继续发挥重要的作用。
6.2 Stream和Optional在其他编程语言中的应用和比较
Stream
以下是一些流式处理在其他编程语言中的应用和比较:
-
Python中的Generator和List Comprehension
Python中的Generator和List Comprehension与Java Stream具有相似的概念。Generator是一种可以被迭代的对象,它在需要时生成数据,而不是一次性生成所有数据。这样可以节省内存和时间。List Comprehension是一种用于创建列表的快速方法,它使用类似于Java Stream的函数式编程概念。
-
C#中的LINQ
LINQ是C#中的一种查询语言,它与Java Stream非常相似。它可以用于处理集合、数组和其他数据源,并提供类似于Java Stream的函数式编程概念。它可以进行过滤、投影、排序等操作,可以大大简化代码。
-
JavaScript中的Array.prototype方法
JavaScript中的Array.prototype方法提供了一些类似于Java Stream的函数式编程概念。这些方法包括map、filter、reduce等,它们可以用于处理数组,并且非常方便。尽管它们的语法和用法与Java Stream略有不同,但它们的功能是相似的。
总的来说,流式处理在很多编程语言中都有应用,并且它们在功能和性能方面都非常相似。因此,如果您了解了一种编程语言中的流式处理,那么在学习其他编程语言时,您也可以更容易地理解它们。
Optional
下面是一些其他编程语言中类似Optional的实现方式和比较:
-
Kotlin中的Nullable Types
Kotlin是一种静态类型的编程语言,与Java非常相似。在Kotlin中,可以使用Nullable Types来表示一个值可以为空的情况。与Java的Optional类似,Nullable Types可以通过安全调用操作符(?.)来避免空指针异常。
-
Swift中的Optional
Swift是一种由苹果公司开发的编程语言。在Swift中,Optional也是一种包装类型,用于表示一个值可以存在或不存在的情况。与Java的Optional类似,Swift的Optional可以通过可选绑定(Optional Binding)来安全地处理可选值。
-
C++中的std::optional
C++17引入了一个新的标准库类型std::optional,它用于表示一个值可以存在或不存在的情况。与Java的Optional类似,std::optional提供了一些成员函数和操作符来处理可选值。
-
Python中的NoneType
Python是一种动态类型的编程语言,与Java和C++有很大的不同。在Python中,NoneType是一个特殊的类型,用于表示一个值不存在的情况。与Java的Optional类似,可以使用条件语句来处理NoneType。
总的来说,虽然不同的编程语言可能采用不同的实现方式,但是它们都提供了一种机制来处理可选值。在Java中,Optional可以帮助我们更清晰地表达代码的含义,并且可以在编译时捕获潜在的空指针异常。
6.3 如何进一步学习和掌握Stream和Optional的高级用法和扩展功能
要进一步学习和掌握Stream和Optional的高级用法和扩展功能,以下是一些建议:
-
阅读官方文档:阅读Java官方文档的Stream和Optional部分,这是深入了解它们的最佳途径。官方文档提供了丰富的示例和解释,帮助您理解这些API的高级用法和扩展功能。
-
学习Lambda表达式:Stream和Optional通常与Lambda表达式一起使用,因此,如果您不熟悉Lambda表达式,就很难理解这些API的高级用法。学习Lambda表达式的最佳方法是使用Java 8的Lambda表达式和函数式接口。你可以使用Java 8的Lambda表达式教程和练习,来学习Lambda表达式的基础和进阶知识。
-
练习编程:使用Stream和Optional编写一些小程序来练习这些API的高级用法和扩展功能。通过编写实际的代码,您可以更好地理解这些API的行为和使用方法。
-
阅读优秀的博客和教程:许多优秀的博客和教程都提供了关于Stream和Optional的深入解释和实用示例。可以查找一些您认为适合自己的博客和教程,并进行学习和实践。
-
参与在线社区:加入一些Java程序员的在线社区(如Stack Overflow和GitHub)并参与讨论,与其他开发者分享经验和知识,从中学习和发展自己的技能。
-
总之,要深入学习和掌握Stream和Optional的高级用法和扩展功能,需要不断学习、实践和分享。通过不断努力和探索,您可以逐步掌握这些API的精髓,提高自己的编程水平。