JDK新特性
1.Lambda表达式
在Java中,Lambda表达式是一种简洁的表示匿名函数的方法。它们提供了一种方式来传递代码作为数据,这在实现某些接口(特别是那些只包含一个抽象方法的接口,即函数式接口)时特别有用。Lambda表达式使得代码更加简洁,并提高了可读性。
1.1.基本语法
Java中Lambda表达式的基本语法如下:
java复制代码
(参数列表) -> { Lambda体 }
或者,如果Lambda体只包含一个表达式(且该表达式的计算结果会自动返回),则可以省略大括号{}
和return
关键字:
java复制代码
(参数列表) -> 表达式
- 参数列表:Lambda表达式的参数,其类型可以显式声明,也可以由编译器自动推断。如果Lambda表达式只有一个参数,则圆括号
()
可以省略(但在某些情况下,为了清晰起见,最好保留它们)。 - ->:Lambda操作符,用于分隔参数列表和Lambda体。
- Lambda体:包含Lambda表达式要执行的代码。如果代码只有一行,则可以省略大括号
{}
;如果有多行代码,则需要使用大括号{}
包围起来,并且可以通过return
语句来返回值(如果Lambda表达式的返回类型不是void
)。
1.2.示例
假设我们有一个函数式接口Greeting
,它包含一个方法greet
,该方法接受一个String
参数并返回一个String
。
@FunctionalInterface
interface Greeting {
String greet(String name);
}
public class LambdaExample {
public static void main(String[] args) {
Greeting greeting = name -> "Hello, " + name + "!";
String greetingMessage = greeting.greet("Alice");
System.out.println(greetingMessage); // 输出: Hello, Alice!
}
}
在上面的示例中创建了一个Greeting
接口的匿名实现,并直接将其赋值给Greeting
类型的变量greeting
。这个匿名实现使用了一个Lambda表达式,它接受一个名为name
的参数,并返回一个包含问候语的字符串。
1.3.在Java集合框架中的使用
Lambda表达式在Java集合框架(如List
、Set
和Map
的接口及其实现类)中特别有用,因为Java 8引入了许多新的默认方法和静态方法,这些方法接受函数式接口作为参数,允许你以声明性方式处理集合。
例如,使用List
接口的forEach
方法遍历列表:
List<String> list = Arrays.asList("apple", "banana", "cherry");
list.forEach(s -> System.out.println(s));
或者使用Stream
API对集合进行更复杂的操作:
List<String> filteredList = list.stream()
.filter(s -> s.startsWith("a"))
.collect(Collectors.toList());
System.out.println(filteredList); // 输出: [apple, banana]
在这个例子中,我们使用了Stream
API的filter
方法来过滤出以字母"a"开头的字符串,并使用collect
方法将结果收集到一个新的列表中。Lambda表达式s -> s.startsWith("a")
作为filter
方法的参数,定义了过滤条件。
2.新的日期/时间API的使用
Java 8(也称为JDK 1.8)引入了全新的日期和时间API,旨在解决旧的java.util.Date
和java.util.Calendar
类中存在的许多问题,如设计缺陷、易用性差和时区处理复杂等。新的日期/时间API位于java.time
包及其子包中,提供了一套全面、强大且易于使用的日期和时间类。
1.1.主要类
LocalDate
:表示一个具体的日期,不包含时间信息,也不包含时区信息。LocalTime
:表示一个具体的时间,不包含日期信息,也不包含时区信息。LocalDateTime
:表示一个具体的日期和时间,不包含时区信息。ZonedDateTime
:表示一个具体的日期、时间和时区。Instant
:表示时间线上的一个瞬时点,可以精确到纳秒。它通常用于表示UTC时间。Duration
:表示两个时间点之间的时间量,以秒和纳秒为单位。Period
:表示两个日期之间的时间量,以年、月和日为单位。DateTimeFormatter
:用于格式化和解析日期-时间对象的类。
1.2.示例
1.2.1.创建日期和时间
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
public class NewDateTimeAPIExample {
public static void main(String[] args) {
// LocalDate
LocalDate date = LocalDate.of(2023, 10, 1);
System.out.println("Date: " + date);
// LocalTime
LocalTime time = LocalTime.of(14, 30);
System.out.println("Time: " + time);
// LocalDateTime
LocalDateTime dateTime = LocalDateTime.of(2023, 10, 1, 14, 30);
System.out.println("Date-Time: " + dateTime);
// ZonedDateTime
ZonedDateTime zonedDateTime = dateTime.atZone(ZoneId.of("Europe/Paris"));
System.out.println("Zoned Date-Time: " + zonedDateTime);
}
}
1.2.2.格式化日期和时间
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateTimeFormatterExample {
public static void main(String[] args) {
LocalDateTime dateTime = LocalDateTime.now();
// 自定义格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
System.out.println("Formatted Date-Time: " + formattedDateTime);
// 使用预定义的格式化
String isoDateTime = dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println("ISO Formatted Date-Time: " + isoDateTime);
}
}
1.2.3.解析字符串为日期和时间
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ParseDateTimeExample {
public static void main(String[] args) {
String dateTimeStr = "2023-10-01 14:30:00";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, formatter);
System.out.println("Parsed Date-Time: " + dateTime);
}
}
3.Optional类的使用
Optional
类是 Java 8 引入的一个容器类,它可以包含也可以不包含非空的值。使用 Optional
类可以避免显式的 null
检查,从而使代码更加简洁,并且可以减少 NullPointerException
的风险。
3.1.主要方法和使用场景
3.1.1.创建 Optional 对象
Optional.of(T value)
:创建一个Optional
实例,该实例包含指定的非空值。如果value
为null
,则抛出NullPointerException
。Optional.ofNullable(T value)
:如果value
不为null
,则创建一个包含value
的Optional
实例,否则创建一个空的Optional
实例。Optional.empty()
:创建一个空的Optional
实例。
3.1.2.获取 Optional 中的值
T get()
:如果值存在,则返回该值,否则抛出NoSuchElementException
。boolean isPresent()
:如果值存在,则返回true
,否则返回false
。T orElse(T other)
:如果值存在,则返回该值,否则返回other
。T orElseGet(Supplier<? extends T> other)
:如果值存在,则返回该值,否则返回other
生成的值。T orElseThrow(Supplier<? extends X> exceptionSupplier)
:如果值存在,则返回该值,否则抛出exceptionSupplier
生成的异常。
3.1.3.其他常用方法
Optional<U> map(Function<? super T, ? extends U> mapper)
:如果值存在,则对其应用给定的函数,并返回一个新的Optional
结果。Optional<T> filter(Predicate<? super T> predicate)
:如果值存在,并且满足给定的谓词,则返回包含该值的Optional
,否则返回一个空的Optional
。
3.2.示例
import java.util.Optional;
public class OptionalExample {
public static void main(String[] args) {
// 创建一个包含非空值的Optional
Optional<String> optionalName = Optional.of("Alice");
// 使用isPresent()检查值是否存在
if (optionalName.isPresent()) {
System.out.println(optionalName.get());
}
// 使用orElse()提供默认值
String nameOrDefault = optionalName.orElse("Unknown");
System.out.println(nameOrDefault); // 输出: Alice
// 使用map()转换值
Optional<Integer> optionalLength = optionalName.map(String::length);
System.out.println(optionalLength.orElse(0)); // 输出: 5
// 创建一个可能为空的Optional
Optional<String> optionalMaybeName = Optional.ofNullable(null);
// 使用orElseGet()提供默认值(通过方法调用)
String nameFromSupplier = optionalMaybeName.orElseGet(() -> "Default Name");
System.out.println(nameFromSupplier); // 输出: Default Name
// 使用filter()进行条件检查
Optional<String> filteredName = optionalName.filter(name -> name.startsWith("A"));
System.out.println(filteredName.orElse("Name does not start with A")); // 输出: Alice
}
}
3.3.注意事项
- 尽管
Optional
可以帮助减少null
检查,但过度使用或误用(如在字段、返回类型或方法参数中)可能会使代码更加复杂,降低可读性。 - 在处理可能为
null
的返回值时,Optional
是一个很好的选择,但在编写你自己的代码时,最好通过设计避免null
值,或者至少限制它们的传播。 - 使用
Optional
时,建议优先考虑使用ifPresent()
、orElse()
、orElseGet()
、orElseThrow()
等方法,而不是直接调用get()
方法,以避免NoSuchElementException
。
4.接口增强
在Java中,接口(Interface)是一种引用类型,是一种抽象的类型,用于指定一组方法规范,但不提供这些方法的具体实现。从Java 8开始,接口得到了显著的增强,引入了默认方法(Default Methods)和静态方法(Static Methods),以及从Java 9开始引入的私有方法(Private Methods,包括私有实例方法和私有静态方法)。这些增强使得接口更加灵活和强大,同时也为Java的API设计提供了更多的可能性。
4.1. 默认方法(Default Methods)
在Java 8之前,接口中的所有方法都是抽象的,即它们没有实现体。Java 8引入了默认方法的概念,允许接口包含具有实现的方法。这意呀着,即使一个类没有显式地实现接口中的某个方法,它仍然可以被认为是该接口的实现,因为接口已经为该方法提供了默认实现。
默认方法使用default
关键字来声明。
public interface MyInterface {
default void defaultMethod() {
System.out.println("This is a default method.");
}
}
4.2. 静态方法(Static Methods)
静态方法也是Java 8引入的接口增强之一。与默认方法不同,静态方法不能被接口的实现类继承或重写。静态方法可以直接通过接口名来调用,而不需要接口的实例。
静态方法使用static
关键字来声明。
public interface MyInterface {
static void staticMethod() {
System.out.println("This is a static method.");
}
}
4.3. 私有方法(Private Methods,Java 9+)
从Java 9开始,接口中可以包含私有方法,包括私有实例方法和私有静态方法。私有方法主要用于辅助默认方法和静态方法的实现,使得接口内部的代码更加模块化和可重用。
私有实例方法使用private
关键字声明,并且可以通过默认方法或另一个私有实例方法调用。
私有静态方法也使用private
关键字声明,并且可以通过接口中的其他静态方法或私有静态方法调用。
public interface MyInterface {
default void defaultMethod() {
helperMethod();
}
private void helperMethod() {
System.out.println("This is a private method.");
}
static void staticMethod() {
helperStaticMethod();
}
private static void helperStaticMethod() {
System.out.println("This is a private static method.");
}
}
4.4.总结
Java接口的这些增强使得接口不再仅仅是方法的声明集合,而是可以包含具体实现(默认方法)、静态工具方法(静态方法)以及辅助实现的私有方法。这些特性极大地提高了Java API的灵活性和表达能力,同时也为Java的面向对象编程带来了更多的可能性。
5.局部变量类型推断
在Java中,从Java 10开始引入了一种新的局部变量类型推断功能,称为局部变量类型推断(Local Variable Type Inference),主要通过var
关键字来实现。这一特性使得开发者在声明局部变量时不必显式地指定变量的类型,编译器会自动根据变量初始化的表达式来推断其类型。
5.1.使用var
关键字
使用var
作为类型声明时,变量必须被初始化,因为编译器需要通过初始化表达式来推断变量的类型。一旦变量被声明并初始化,它就像被显式指定了类型的变量一样,可以进行后续的操作和赋值(但赋值时类型必须兼容)。
5.2.示例
var list = new ArrayList<String>(); // 编译器推断出list的类型为ArrayList<String>
list.add("Hello");
System.out.println(list.get(0));
var number = 10; // 编译器推断出number的类型为int
number = 20; // 正确,因为赋值类型是兼容的
// number = "Hello"; // 错误,因为类型不兼容
var stream = Stream.of("Java", "is", "cool"); // 编译器推断出stream的类型为Stream<String>
stream.forEach(System.out::println);
5.3.注意事项
var
只能用于局部变量,不能用于成员变量、方法参数、返回类型、捕获类型等。- 使用
var
可以提高代码的可读性和编写效率,但也可能使代码的类型信息不那么明显,因此建议在类型明确且代码简短时使用。 - 在使用
var
时,如果初始化表达式比较复杂或难以一眼看出其类型,那么使用var
可能会降低代码的可读性。 var
类型推断是在编译时进行的,不会引入任何运行时成本。
5.4.总结
局部变量类型推断是Java 10引入的一个重要特性,通过var
关键字简化了局部变量的声明,提高了代码编写的灵活性和效率。然而,在使用时也需要注意其对代码可读性的影响,并遵循最佳实践来合理使用。
6.switch表达式增强
Java中的switch表达式增强是Java语言在近年来进行的一项重要改进,旨在提供更简洁、灵活和强大的分支控制结构。这一特性在Java 12中首次作为预览特性引入,并在后续版本中逐步完善,最终在Java 14中成为标准特性。以下是关于Java中switch表达式增强的详细介绍:
6.1.基本语法
增强的switch表达式使用箭头(->)操作符替代了传统的case关键字,并引入了yield关键字用于返回表达式的值。其基本语法如下:
switch (expression) {
case value1 -> result1;
case value2 -> result2;
// ...
default -> defaultResult;
}
在这里,expression
是待匹配的表达式,valueN
是与之匹配的值,resultN
是当expression
等于valueN
时返回的结果,defaultResult
是当没有任何case匹配时的默认结果。
6.2.主要特性
- 更简洁的语法:
- 使用箭头(->)操作符简化了case语句的书写,使得每个分支的结果更加直观。
- 不再需要显式的break语句来结束case块,因为每个case后面的表达式在执行完毕后会自动退出switch。
- 更灵活的使用:
- 支持在case表达式中使用任何合法的Java表达式,包括方法调用、算术运算等。
- 允许在同一个case中处理多个值,使用逗号分隔即可(如
case 1, 2, 3 -> ...
)。 - switch表达式可以作为值返回,这在函数式编程中非常有用。
- 更好的可读性:
- 由于语法更加简洁,代码的可读性也得到了提升。
- 可以通过合理的命名和注释来进一步提高代码的可读性。
- 支持复杂的模式匹配:
- 虽然基本的switch表达式主要支持常量匹配,但Java也在不断探索在switch表达式中引入更复杂的模式匹配(如字符串、枚举、范围等)的可能性。
6.3.示例代码
以下是一个使用增强的switch表达式的示例代码:
public class SwitchExpressionExample {
public static void main(String[] args) {
int day = 3;
String dayOfWeek = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
case 4 -> "Thursday";
case 5 -> "Friday";
case 6 -> "Saturday";
case 7 -> "Sunday";
default -> throw new IllegalArgumentException("Invalid day of the week: " + day);
};
System.out.println("Day of the week: " + dayOfWeek);
}
}
在这个示例中,我们根据day
变量的值来返回对应的星期几字符串。如果day
的值不在1到7之间,则抛出异常。
7.文本块标准化
Java中的文本块(Text Blocks)标准化主要指的是从预览特性到正式特性的转变过程,以及相关的语法规则和使用规范。文本块是Java 13中引入的一个预览特性,旨在简化多行字符串和格式化字符串的编写工作。经过多个版本的迭代,文本块在Java 17中成为正式特性。
7.1.文本块的标准化历程
- 引入与预览:
- Java 13:文本块作为预览特性首次引入,允许开发者以更自然的方式编写跨越多行、包含特殊字符或需要特定格式的字符串。
- Java 14:文本块继续作为预览特性提供,进一步改进和完善。
- Java 15:虽然文本块在Java 15中仍然是预览特性,但已经为最终成为正式特性做好了准备。
- 正式特性:
- Java 17:文本块在Java 17中成为正式特性,标志着其语法和行为的标准化。
7.2.文本块的标准化内容
- 语法规则:
- 文本块使用三重双引号(""")作为开始和结束的分隔符。
- 开始分隔符必须单独成行,且后面可以跟随零个或多个空格以及一个行结束符。
- 文本块的内容直接按原样保留格式和换行,无需使用
\n
来手动插入换行符。 - 文本块会自动处理字符串中的换行和缩进,使得代码更加清晰易读。
- 在大多数情况下,文本块中的引号、反斜杠等特殊字符无需转义。
- 使用场景:
- 文本块特别适合于编写SQL查询、HTML、JSON等需要跨越多行且格式化的文本内容。
- 文本块可以作为方法参数传递,也可以在任何可以使用字符串文本的位置使用。
- 格式化与缩进:
- 文本块会删除所有附带的缩进(即与第一行非空白内容相比,前面多余的空格或制表符),只保留必要的缩进。
- 如果需要保留尾部的空格,可以使用
\s
转义字符。 - 文本块中的换行符会被统一转换为LF(\u000A),以消除不同平台间的差异。
- 兼容性:
- 尽管文本块在Java 17中成为正式特性,但在团队协作或维护旧项目时,仍需考虑兼容性问题。
- 确保项目环境和团队已准备好采用这一新特性,以避免潜在的兼容性问题。
8.模式匹配
Java中的模式匹配(Pattern Matching)是一种用于简化代码的特性,它允许开发者以更直观和简洁的方式检查对象的类型并提取其值。这一特性在Java 14及更高版本中得到了引入和逐步增强,特别是在Java 17中,switch语句对模式匹配的支持变得更加完善。以下是关于Java中模式匹配的详细解析:
8.1.模式匹配的基本概念
模式匹配是一种通用的技术,用于匹配各种类型的数据,包括字符串、树、列表等。在Java中,模式匹配主要用于以下方面:
- 检查对象的类型:通过模式匹配,开发者可以检查一个对象是否属于特定的类型。
- 从对象中提取值:如果对象匹配特定的类型,模式匹配还可以允许开发者从该对象中提取值。
8.2.模式匹配的语法和用法
8.2.1. instanceof的简化
在Java 14及更高版本中,instanceof
操作符得到了简化,允许在检查对象类型的同时进行类型转换。以前,我们需要显式地将对象转换为目标类型,现在可以直接在instanceof
表达式中声明一个变量来接收转换后的对象。
示例:
Object obj = "Hello, World!";
if (obj instanceof String str) {
System.out.println("The string length is: " + str.length());
} else {
System.out.println("Not a string");
}
8.2.2. switch表达式的增强
从Java 12开始,switch表达式引入了预览特性,支持在case语句中使用模式匹配。在Java 17中,这一特性成为标准,使得switch语句更加强大和灵活。
示例:
Object obj = "Java";
String result = switch (obj) {
case String s && s.length() > 4 -> "Long string";
case String s -> "Short string";
default -> "Not a string";
};
System.out.println(result);
8.2.3. 记录类型(Record Types)与模式匹配
记录类型在Java 14中首次引入,并在Java 16中正式成为标准。它们提供了一种简洁的方式来定义数据载体,结合模式匹配可以方便地解构和操作这些数据。
示例:
record Person(String name, int age) {}
Person person = new Person("Alice", 30);
if (person instanceof Person(String name, int age)) {
System.out.println("Name: " + name + ", Age: " + age);
} else {
System.out.println("Not a Person");
}
8.3模式匹配的优势
- 简化代码:通过减少冗长的代码和重复的类型检查,模式匹配使得代码更加简洁。
- 提高可读性:简化的语法和直观的匹配方式提高了代码的可读性。
- 增强灵活性:增强的switch表达式和记录类型结合模式匹配,使得Java在处理复杂条件和数据结构时更加灵活。
9.Stream流处理
Java中的Stream流处理是Java 8引入的一个关键抽象概念,它允许你以声明方式处理数据集合(包括数组、集合等)。Stream API可以让你以一种高效且易于表达的方式来对数据进行过滤、排序、映射等复杂操作。使用Stream API可以极大地提高代码的可读性和可维护性,同时可以利用多核处理器的优势进行并行处理。
9.1.Stream的基本概念
- 流(Stream):是一个来自数据源的元素队列并支持聚合操作。
- 数据源:流的来源,可以是数组、集合、I/O通道等。
- 聚合操作:在一个流上进行的操作,如过滤、映射、排序、归约等。
9.2.Stream的特点
- 非破坏性:流操作不会修改源数据集合。
- 中间操作与终端操作:流操作分为中间操作和终端操作。中间操作返回流本身,可以链式调用;终端操作返回某种结果或副作用,并结束流。
- 惰性求值:中间操作不会立即执行,直到遇到终端操作才开始计算整个流的操作链。
- 并行与串行:流操作可以是串行的,也可以是并行的。并行流利用多核处理器来加速处理过程。
9.3.Stream的基本操作
9.3.1. filter()
- 过滤
过滤出满足条件的元素。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
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); // 输出: [2, 4, 6, 8, 10]
}
}
9.3.2. map()
- 映射
将流中的每个元素映射到另一个元素上。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperCaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseNames); // 输出: [ALICE, BOB, CHARLIE]
}
}
9.3.3. sorted()
- 排序
对流中的元素进行排序。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Bob", "Alice", "Charlie");
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNames); // 输出: [Alice, Bob, Charlie]
}
}
9.3.4. collect()
- 收集
将流中的元素累积成一个汇总结果,如列表、集合等。
已在上面的示例中使用。
9.3.5. forEach()
- 遍历
对流中的每个元素执行操作。
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.forEach(name -> System.out.println(name));
// 输出:
// Alice
// Bob
// Charlie
}
}
9.3.6. reduce()
- 归约
通过反复结合流中的元素,得到一个值,如求和、求最大值等。
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println(sum); // 输出: 15
}
}
这些示例展示了 Stream API 的一些基本用法,你可以根据自己的需求组合使用这些方法来处理数据。
9.4.示例
以下是一个简单的Stream API使用示例,演示了如何过滤集合中的元素并计算其和:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 过滤出偶数,映射为字符串,并收集到一个新的列表中
List<String> evenNumbersAsString = numbers.stream()
.filter(n -> n % 2 == 0)
.map(String::valueOf)
.collect(Collectors.toList());
System.out.println(evenNumbersAsString); // 输出: [2, 4, 6, 8, 10]
// 计算偶数之和
int sumOfEvens = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
System.out.println(sumOfEvens); // 输出: 30
}
}
在这个示例中,我们首先通过.stream()
方法将集合转换为流,然后通过.filter()
方法过滤出偶数,再通过.map()
方法将每个偶数映射为字符串,最后通过.collect(Collectors.toList())
方法收集到一个新的列表中。我们还展示了如何使用流来计算偶数之和。
9.5.注意事项
- 尽量避免在流操作中创建复杂的状态,因为这可能会破坏流的惰性求值和并行处理的优势。
- 在使用并行流时,需要注意线程安全问题,因为并行流会在多个线程上执行操作。
- 流操作的结果通常是不可变的,因此不能修改流中的元素。如果需要修改,可以在终端操作中使用集合的修改方法。