32-JDK8 新特性
1. 函数式接口#
只声明一个抽象方法的接口,称为"函数式接口"。
通过 Lambda 表达式来创建该接口的对象(若 Lambda 表达式抛出一个受检异常,即:非运行时异常,那么该异常需要在目标接口的抽象方法上进行声明)。
我们可以在一个接口上使用 @FunctionalInterface
注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
如何理解函数式接口?
我们知道使用 Lambda 表达式的前提是需要有函数式接口。而 Lambda 使用时不关心接口名、抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用 Lambda 方便,JDK 提供了大量常用的函数式接口。
它们主要在 java.util.function 包中。下面是最常用的几个接口。
1.1 Supplier#
供给型接口,通过 Supplier#get 可以得到一个值,无参有返回的接口。
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
1.2 Consumer#
Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
}
默认方法:andThen
如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操
作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的 default 方法 andThen:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> {
accept(t);
after.accept(t);
};
}
java.util.Objects 的 requireNonNull 静态方法将会在参数为 null 时主动抛出 NPE 异常。这省去了重复编写 if 语句和抛出空指针异常的麻烦。
要想实现组合,需要两个或多个 Lambda 表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况:
public class Demo {
public static void main(String[] args) {
test((String s) -> { System.out.println(s.toLowerCase()); },
(String s) -> { System.out.println(s.toUpperCase()); });
test( s -> System.out.println(s.toLowerCase()),
s -> System.out.println(s.toUpperCase())
);
}
public static void test(Consumer<String> c1, Consumer<String> c2) {
String str = "Hello World";
c1.andThen(c2).accept(str);
}
}
运行结果将会首先打印完全小写的 hello,然后打印完全大写的 HELLO。当然,通过链式写法可以实现更多步骤的组合。
1.3 Function#
Function<T,R> 转换型接口,对 apply 方法传入的 T 类型数据进行处理,返回 R 类型的结果,有参数有返回的接口。
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
}
默认方法:andThen
Function 接口中有一个默认的 andThen 方法,用来进行组合操作:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:
public class Demo {
public static void main(String[] args) {
test((String s) -> { return Integer.parseInt(s); },
(Integer i) -> { return i * 10; }
);
}
public static void test(Function<String, Integer> f1, Function<Integer, Integer> f2) {
Integer in3 = f1.andThen(f2).apply("66");
System.out.println("in3: " + in3); // 660
}
}
1.4 Predicate#
有时候我们需要对某种类型的数据进行判断,从而得到一个 bool 值结果。这时可以使用 Predicate 接口。
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
}
默认方法:and
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用默认方法 and:
// e.g. p1.and(p2).test(str);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
// e.g. p1.negate().test(str);
default Predicate<T> negate() {
return (t) -> !test(t);
}
// e.g. p1.or(p2).test(str);
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
2. Lambda 表达式#
2.1 简述#
- Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了提升。
- Lambda 表达式:在 Java 8 语言中引入的一种新的语法元素和操作符。这个操作符为
->
,该操作符被称为 Lambda 操作符或箭头操作符。它将 Lambda 分为两个部分:- 左侧:指定了 Lambda 表达式需要的参数列表。
- 右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即 Lambda 表达式要执行的功能。
- Lambda 本质:[函数式接口] 的实例
2.2 使用#
拷贝小括号,写死右箭头,落地大括号。
// 1. 无参,无返回值
public void test1() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("白世珠");
}
};
Runnable r2 = () -> {
System.out.println("周星智");
};
}
// 2. 一个参数,没有返回值
public void test2() {
Consumer<String> c1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
Consumer<String> c2 = (String s) -> {
System.out.println(s);
};
}
// 3. 数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
public void test3() {
Consumer<String> c1 = (String s) -> {
System.out.println(s);
};
Consumer<String> c2 = (s) -> {
System.out.println(s);
};
}
// 4. 一个参数时,参数的小括号可以省略
public void test4() {
Consumer<String> c2 = s -> {
System.out.println(s);
};
}
// 5. 需要两个或以上的参数,多条执行语句,并且可以有返回值
public void test5() {
Comparator<Integer> c1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
};
Comparator<Integer> c2 = (o1, o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};
}
// 6. 当方法体只有一条语句时,return 与大括号若有,都可以省略
public void test6() {
Comparator<Integer> c1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
};
Comparator<Integer> c2 = (o1, o2) -> o1.compareTo(o2);
}
书写规范:
- 类型推断
- 上述 Lambda 表达式中的参数类型都是由编译器推断得出的。
- Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。
- Lambda 形参列表
- 参数类型可以省略(类型推断)
- 如果 Lambda 形参列表只有一个参数,包裹它的一对 () 也可以省略
- Lambda 体
- 应该使用一对 {} 包裹
- 如果 Lambda 体只有一条执行语句(也可能是 return 语句),则省略这一对 {} 和 return 关键字
注意事项:
- 方法的参数或局部变量类型必须为接口才能使用 Lambda 表达式
- 接口中有且仅有一个抽象方法 => 函数式接口 @FunctionalInterface
2.3 原理#
首先声明一个函数式接口:
@FunctionalInterface
interface Swimmable {
public abstract void swimming();
}
先来看下匿名内部类的表现形式:
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("使用匿名内部类实现游泳");
}
});
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
我们可以看到匿名内部类会在编译后产生一个类: Demo04LambdaImpl$1.class
使用XJad反编译这个类,得到如下代码:
package com.itheima.demo01lambda;
import java.io.PrintStream;
// Referenced classes of package com.itheima.demo01lambda:
// Swimmable, Demo04LambdaImpl
static class Demo04LambdaImpl$1 implements Swimmable {
public void swimming()
{
System.out.println("使用匿名内部类实现游泳");
}
Demo04LambdaImpl$1() {}
}
我们再来看看 Lambda 的效果,修改代码如下:
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(() -> {
System.out.println("使用匿名内部类实现游泳");
});
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说 Lambda 并没有在编译的时候产生一个新的类。使用 XJad 对这个类进行反编译,发现 XJad 报错。使用了 Lambda 后 XJad 反编译工具无法反编译。我们使用 JDK 自带的一个工具:javap ,对字节码进行反汇编,查看字节码指令。
在命令行输入:
$ javap -c -p FILE_NAME.class
# -c:表示对代码进行反汇编
# -p:显示所有类和成员
反汇编后效果如下:
C:\Users\>javap -c -p Demo04LambdaImpl.class
Compiled from "Demo04LambdaImpl.java"
public class com.itheima.demo01lambda.Demo04LambdaImpl {
public com.itheima.demo01lambda.Demo04LambdaImpl();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:()Lcom/itheima/demo01lambda/Swimmable;
5: invokestatic #3 // Method goSwimming:(Lcom/itheima/demo01lambda/Swimmable;)V
8: return
public static void goSwimming(com.itheima.demo01lambda.Swimmable);
Code:
0: aload_0
1: invokeinterface #4, 1 // InterfaceMethod com/itheima/demo01lambda/Swimmable.swimming:()V
6: return
private static void lambda$main$0();
Code:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String Lambda游泳
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
可以看到在类中多出了一个私有的静态方法 lambda$main$0 。这个方法里面放的是什么内容呢?我们通过断点调试来看看:
可以确认 lambda$main$0
里面放的就是 Lambda 中的内容,我们可以这么理解 lambda$main$0
方法:
public class Demo04LambdaImpl {
public static void main(String[] args) {
...
}
private static void lambda$main$0() {
System.out.println("Lambda游泳");
}
}
关于这个方法 lambda$main$0
的命名:以 lambda
开头,因为是在 main() 函数里使用了 Lambda 表达式,所以带有 $main
表示,因为是第一个,所以 $0
。
如何调用这个方法呢?其实 Lambda 在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加上 -Djdk.internal.lambda.dumpProxyClasses
,加上这个参数后,运行时会将生成的内部类 class 码输出到一个文件中。使用 java 命令如下:
$ java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
根据上面的格式,在命令行输入以下命令:
$ java -Djdk.internal.lambda.dumpProxyClasses com.itheima.demo01lambda.Demo04LambdaImpl
Lambda游泳
执行完毕,可以看到生成一个新的类,效果如下:
反编译 Demo04LambdaImpl$$Lambda$1.class 这个字节码文件,内容如下:
// Referenced classes of package com.itheima.demo01lambda:
// Swimmable, Demo04LambdaImpl
final class Demo04LambdaImpl$$Lambda$1 implements Swimmable {
public void swimming()
{
Demo04LambdaImpl.lambda$main$0();
}
private Demo04LambdaImpl$$Lambda$1()
{
}
}
可以看到这个匿名内部类实现了 Swimmable 接口,并且重写了 swimming 方法, swimming 方法调用 Demo04LambdaImpl.lambda$main$0() 方法,也就是调用 Lambda 中的内容。
【小结】
- 匿名内部类在编译的时候会一个 class 文件
- Lambda 在程序运行的时候也会形成一个类
- 在主类中会新增一个方法,这个方法的方法体就是 Lambda 表达式中的代码;
- 还会形成一个匿名内部类:实现接口,重写抽象方法(方法内调用新生成的方法)
3. 方法引用#
当要传递给 Lambda 体的操作,就是对一个方法的调用,那么此时就可以使用“方法引用”来再次书写。
方法引用形式 | 说明 |
---|---|
instanceName::methodName | 对象::实例方法 |
ClassName::staticMethodName | 类名::静态方法 |
ClassName::methodName | 类名::普通方法 |
ClassName::new | 类名::new(调用的构造器) |
TypeName[]::new | 类型[]::new(调用类型的构造器) |
3.1 方法引用#
使用操作符 ::
将类(或对象) 与方法名分隔开来,有如下 3 种主要使用情况:
- 对象 :: 实例方法名
- 类 :: 静态方法名
- 类 :: 实例方法名
[情况1,情况2] 实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致。
/*
[情况1] 对象::实例方法
Consumer - void accept(T t)
PrintStream - void println(T t)
*/
public void test1() {
Consumer<String> cons1 = str -> System.out.println(str);
Consumer<String> cons2 = System.out :: println;
}
/*
[情况1] 对象::实例方法
Supplier - T get()
Employee - String getName()
*/
public void test2() {
Employee emp = new Employee(1101, "LJQ", 22, 3000);
Supplier<String> sup1 = () -> emp.getName();
Supplier<String> sup2 = emp :: getName;
}
/*
[情况2] 类 :: 静态方法
Comparator - int compare(T t1, T t2)
Integer - int compare(T t1, T t2)
*/
public void test3() {
Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2);
Comparator<Integer> com2 = Integer :: compare;
}
/*
[情况2] 类 :: 静态方法
Function - R apply(T t)
Math - Long round(Double d)
*/
public void test4() {
Function<Double, Long> func1 = d -> Math.round(d);
Function<Double, Long> func2 = Math :: round;
}
[情况3] Java 面向对象规定:类名只能调用静态方法。在这里,类名引用实例方法是有前提的:接口的抽象方法的第 1 个参数是〈需要引用方法〉的调用者,第 2 个参数是〈需要引用方法〉的参数。
/*
[情况3] 类 :: 实例方法
Comparator - int compare(T t1, T t2)
String - t1.compareTo(t2)
*/
public void test5() {
Comparator<String> com1 = (t1, t2) -> t1.compareTo(t2);
Comparator<String> com2 = String :: compareTo;
}
/*
[情况3] 类 :: 实例方法
BiPredicate - boolean test(T t1, T t2)
String - boolean t1.equals(t2)
*/
public void test6() {
BiPredicate<String, String> bi1 = (t1, t2) -> t1.equals(t2);
BiPredicate<String, String> bi2 = String :: equals;
}
/*
[情况3] 类 :: 实例方法
Function - R apply(T t)
Employee - String getName()
*/
public void test7() {
Employee emp = new Employee(1101, "LJQ", 22, 3000);
Function<Employee, String> func1 = e -> e.getName();
Function<Employee, String> func2 = Employee :: getName;
}
3.2 构造器引用#
- 格式:
className :: new
- 与函数式接口相结合,自动与函数式接口中方法兼容。
- 和方法引用类似,要求构造器参数列表要与接口中抽象方法的参数列表一致,方法的返回值即为构造器对应类的对象。
/*
Supplier - T get()
Employee - new Employee()
*/
public void test1() {
Supplier<Employee> sup1 = () -> new Employee();
Supplier<Employee> sup2 = Employee :: new;
}
/*
Function - R apply(T t)
Employee - new Employee(id)
*/
public void test2() {
Function<Integer, Employee> func1 = id -> new Employee(id);
Function<Integer, Employee> func2 = Employee :: new;
}
/*
BiFunction - R apply(T t, U u)
Employee - new Employee(id, name)
*/
@Test
public void test3() {
BiFunction<Integer, String, Employee> bi1 = (id, name) -> new Employee(id, name);
BiFunction<Integer, String, Employee> bi2 = Employee :: new;
}
3.3 数组引用#
格式: type[] :: new
/*
Function - R apply(T t)
Type[] - new Type[int]
*/
public void test() {
Function<Integer, String[]> func1 = length -> new String[length];
Function<Integer, String[]> func2 = String[] :: new;
}
4. interface 扩展#
JDK8 以前的接口:
interface 接口名 {
静态常量;
抽象方法;
}
JDK8 对接口的增强,接口还可以有默认方法和静态方法。
4.1 默认方法#
在 JDK 8 以前接口中只能有抽象方法。存在以下问题:如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法。不利于接口的扩展。
接口中的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写。这样就方便接口的扩展。
interface 接口名 {
修饰符 default 返回值类型 方法名() {
// CODE
}
}
接口默认方法的使用
- 实现类直接调用接口默认方法
- 实现类重写接口默认方法
public class Demo02UserDefaultFunction {
public static void main(String[] args) {
BB b = new BB();
// 方式一:实现类直接调用接口默认方法
b.test02();
CC c = new CC();
// 调用实现类重写接口默认方法
c.test02();
}
}
interface AA {
public abstract void test1();
public
default void test02() {
System.out.println("AA test02");
}
}
class BB implements AA {
@Override
public void test1() {
System.out.println("BB test1");
}
}
class CC implements AA {
@Override
public void test1() {
System.out.println("CC test1");
}
// 方式二:实现类重写接口默认方法
@Override
public void test02() {
System.out.println("CC实现类重写接口默认方法");
}
}
4.2 静态方法#
定义格式:
interface 接口名 {
修饰符 static 返回值类型 方法名() {
// CODE
}
}
直接使用接口名调用即可:接口名.静态方法名();
接口默认方法和静态方法的区别:
- 默认方法通过实例调用,静态方法通过接口名调用。
- 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
- 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。
5. Optional 类#
到目前为止,空指针异常是导致 Java 应用程序失败的最常见原因。以前,为了解决空指针异常,Google 公司著名的 Guava 项目引入了 Optional 类,Guava 通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到 Google Guava 的启发,Optional 类已经成为 JDK8 类库的一部分。
java.util.Optional<T>
是一个容器类,它可以保存类型 T 的值,代表这个值存在。或者仅仅保存 null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。
Optional 类的 Javadoc 描述如下:这是一个可以为 null 的容器对象。如果值存在则 isPresent()
会返回 true,调用 get()
会返回该对象。
Optional 提供很多有用的方法,这样我们就不用显式进行空值检测。
使用示例:
public void test01() {
// > 只能传入具体值,不能传 null,否则会抛异常
// Optional<String> userNameO = Optional.of("abc");
// Optional<String> userNameO = Optional.of(null);
// > 可以传 null,传 null 就会创建空实例
Optional<String> userNameO = Optional.ofNullable(null);
// Optional<String> userNameO = Optional.empty();
// isPresent() : 判断是否包含值,包含值返回true,不包含值返回false。
if (userNameO.isPresent()) {
// get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException。
String userName = userNameO.get();
System.out.println("用户名:" + userName);
} else {
System.out.println("用户名不存在");
}
}
public void test02() {
Optional<String> userNameO = Optional.of("ab");
// Optional<String> userNameO = Optional.empty();
// 存在做的什么
// userNameO.ifPresent(s -> System.out.println("用户名为" + s));
// 存在做的什么, 不存在做点什么
userNameO.ifPresentOrElse(
s -> System.out.println("用户名为" + s),
() -> System.out.println("用户名不存在"));
}
public void test03() {
// Optional<String> userNameO = Optional.of("ab");
Optional<String> userNameO = Optional.empty();
// 如果调用对象包含值,返回该值,否则返回参数t
System.out.println("用户名为" + userNameO.orElse("null"));
// 如果调用对象包含值,返回该值,否则返回参数Supplier得到的值
String s1 = userNameO.orElseGet(() -> {
return "未知用户名";
});
System.out.println("s1 = " + s1);
}
6. 日期和时间 API#
旧版日期时间 API 存在的问题:
- 设计很差:在 java.util 和 java.sql 的包中都有日期类,java.util.Date 同时包含日期和时间,而 java.sql.Date 仅包含日期。此外用于格式化和解析的类在 java.text 包中定义。
- 非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
- 时区处理麻烦:日期类并不提供国际化,没有时区支持,因此 Java 引入了 java.util.Calendar 和 java.util.TimeZone 类,但他们同样存在上述所有的问题。
JDK 8 中增加了一套全新的日期时间 API,这套 API 设计合理,是线程安全的。新的日期及时间 API 位于 java.time 包中,下面是一些关键类。
- LocalDate :表示日期,包含年月日,格式为 2019-10-16
- LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300
- LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
- DateTimeFormatter :日期时间格式化类。
- Instant:时间戳,表示一个特定的时间瞬间。
- Duration:用于计算 2 个时间(LocalTime,时分秒)的距离
- Period:用于计算 2 个日期(LocalDate,年月日)的距离
- ZonedDateTime :包含时区的时间
Java 中使用的历法是 ISO 8601 日历系统,它是世界民用历法,也就是我们所说的公历。平年有 365 天,闰年是 366 天。此外 Java 8 还提供了 4 套其他历法,分别是:
- ThaiBuddhistDate:泰国佛教历
- MinguoDate:中华民国历
- JapaneseDate:日本历
- HijrahDate:伊斯兰历
(1)LocalDate、LocalTime、LocalDateTime
上述类的实例是不可变的对象,分别表示使用 ISO-8601 日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
public static void test01() {
// 创建指定日期
LocalDate fj = LocalDate.of(1998, 10, 18);
// fj = 1998-10-18
System.out.println("fj = " + fj);
// 得到当前日期
LocalDate nowDate = LocalDate.now();
// nowDate = 2024-09-25
System.out.println("nowDate = " + nowDate);
// 获取日期信息
// 年: 2024
System.out.println("年: " + nowDate.getYear());
// 月: 9
System.out.println("月: " + nowDate.getMonthValue());
// 日: 25
System.out.println("日: " + nowDate.getDayOfMonth());
// 星期: WEDNESDAY
System.out.println("星期: " + nowDate.getDayOfWeek());
}
public static void test02() {
// 得到指定的时间
LocalTime time = LocalTime.of(12, 15, 28, 129_900_000);
// time = 12:15:28.129900
System.out.println("time = " + time);
// 得到当前时间
LocalTime nowTime = LocalTime.now();
// nowTime = 13:29:09.324007700
System.out.println("nowTime = " + nowTime);
// 获取时间信息
// 小时: 13
System.out.println("小时: " + nowTime.getHour());
// 分钟: 29
System.out.println("分钟: " + nowTime.getMinute());
// 秒: 9
System.out.println("秒: " + nowTime.getSecond());
// 纳秒: 324007700
System.out.println("纳秒: " + nowTime.getNano());
}
public static void test03() {
// fj = 1985-09-23T09:10:20
LocalDateTime fj = LocalDateTime.of(1985, 9, 23, 9, 10, 20);
System.out.println("fj = " + fj);
// 得到当前日期时间
// now = 2024-09-25T13:29:09.325077200
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
System.out.println(now.getYear());
System.out.println(now.getMonthValue());
System.out.println(now.getDayOfMonth());
System.out.println(now.getHour());
System.out.println(now.getMinute());
System.out.println(now.getSecond());
System.out.println(now.getNano());
}
(2)对日期时间的修改
对已存在的 LocalDate 对象,创建它的修改版,最简单的方式是使用 withAttribute 方法。withAttribute 方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对象,他们不会影响原来的对象。
public static void test04() {
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
// 修改日期时间
LocalDateTime setYear = now.withYear(2049);
System.out.println("修改年份: " + setYear);
System.out.println("now == setYear: " + (now == setYear));
System.out.println("修改月份: " + now.withMonth(10));
System.out.println("修改小时: " + now.withHour(8));
System.out.println("修改分钟: " + now.withMinute(30));
// 再当前对象的基础上加上或减去指定的时间
LocalDateTime localDateTime = now.plusDays(5);
System.out.println("5天后: " + localDateTime);
System.out.println("now == localDateTime: " + (now == localDateTime));
System.out.println("10年后: " + now.plusYears(10));
System.out.println("20月后: " + now.plusMonths(20));
System.out.println("20年前: " + now.minusYears(20));
System.out.println("5月前: " + now.minusMonths(5));
System.out.println("100天前: " + now.minusDays(100));
}
(3)日期时间的比较
public static void test05() {
// 在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2021, 5, 1);
System.out.println(now.isBefore(date)); // false
System.out.println(now.isAfter(date)); // true
}
(4)时间格式化与解析
public static void test06() {
// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 将日期时间格式化为字符串
String format = now.format(formatter);
System.out.println("format = " + format);
// 将字符串解析为日期时间
LocalDateTime parse = LocalDateTime.parse("1985-09-23 10:12:22", formatter);
System.out.println("parse = " + parse);
}
(5)Instant 时间戳,内部保存了从 1970 年 1 月 1 日 00:00:00 以来的秒和纳秒
public static void test07() {
Instant now = Instant.now();
System.out.println("当前时间戳 = " + now);
// 获取从1970年1月1日 00:00:00的秒
System.out.println(now.getNano());
System.out.println(now.getEpochSecond());
System.out.println(now.toEpochMilli());
System.out.println(System.currentTimeMillis());
Instant instant = Instant.ofEpochSecond(5);
System.out.println(instant); // 1970-01-01T00:00:05Z
}
(6)Duration/Period 类:计算日期时间差
public static void test08() {
// Duration 计算【时间】的距离
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(14, 15, 20);
Duration duration = Duration.between(time, now);
System.out.println("相差的天数:" + duration.toDays());
System.out.println("相差的小时数:" + duration.toHours());
System.out.println("相差的分钟数:" + duration.toMinutes());
System.out.println("相差的秒数:" + duration.toSeconds());
// Period 计算【日期】的距离
LocalDate nowDate = LocalDate.now();
LocalDate date = LocalDate.of(1998, 10, 18);
// 后面的时间减去前面的时间
Period period = Period.between(date, nowDate);
System.out.println("相差的年:" + period.getYears());
System.out.println("相差的月:" + period.getMonths());
System.out.println("相差的天:" + period.getDays());
}
(7)时间校正器
有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。
-
TemporalAdjuster:时间校正器,用来自定义调整时间。
@FunctionalInterface public interface TemporalAdjuster { Temporal adjustInto(Temporal temporal); }
public static void test09() {
LocalDateTime now = LocalDateTime.now();
// 得到下一个月的第一天
TemporalAdjuster firsWeekDayOfNextMonth = temporal -> {
LocalDateTime dateTime = (LocalDateTime) temporal;
LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1);
// LocalDateTime implements Temporal, TemporalAdjuster ...
return nextMonth;
};
LocalDateTime nextMonth = now.with(firsWeekDayOfNextMonth);
System.out.println("Now turns to nextMonth = " + nextMonth);
}
(8)设置日期时间的时区
public static void test10() {
// 1.获取所有的时区ID
// ZoneId.getAvailableZoneIds().forEach(System.out::println);
// 2. 时区操作
// 不带时区地获取计算机的当前时间
// 中国使用的东八区的时区 比标准时间早8h
LocalDateTime now = LocalDateTime.now();
// now = 2024-09-25T13:55:06.625750100
System.out.println("now = " + now);
// 操作带时区的类
// now(Clock.systemUTC()) 创建世界标准时间
ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC());
// bz = 2024-09-25T05:55:06.636218800Z
System.out.println("bz = " + bz);
// now() 使用计算机的默认的时区 创建日期时间
ZonedDateTime now1 = ZonedDateTime.now();
// now1 = 2024-09-25T13:55:06.637447200+08:00[Asia/Shanghai]
System.out.println("now1 = " + now1);
// 使用指定的时区创建日期时间
ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Vancouver"));
// now2 = 2024-09-24T22:55:06.637952400-07:00[America/Vancouver]
System.out.println("now2 = " + now2);
}
7. Stream API#
7.1 流式思想#
集合处理数据的弊端
当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。我们来体验集合操作数据的弊端,需求如下:
一个 List 集合中存储有以下数据:[张无忌,周芷若,赵敏,张强,张三丰]
需求:① 拿到所有姓张的 ② 拿到名字长度为3个字的 ③ 打印这些数据
处理这个需求需要含有三个循环,每一个作用不同:
- 首先筛选所有姓张的人
- 然后筛选名字有三个字的人
- 最后进行对结果进行打印输出。
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?
不是。循环是做事情的方式,而不是目的。每个需求都要循环一次,还要搞一个新集合来装数据,如果希望再次遍历,只能再使用另一个循环从头开始。
那 Stream 能给我们带来怎样更加优雅的写法呢?
下面来看一下借助 Java 8 的 Stream API,修改后的代码:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(System.out::println);
}
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为 3、逐一打印。我们真正要做的事情内容被更好地体现在代码中。
Stream 流式思想
注意:Stream 和 IO 流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统 IO 流的固有印象!
Stream 流式思想类似于工厂车间的“生产流水线”,Stream 流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream 可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
【Stream 和 Collection 的区别】 Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合讲的是数据,Stream 讲的是计算!
Stream API 能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。
Stream 操作的 3 个步骤:
- Stream 自己不会存储元素。
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。
- Stream 操作是延迟执行的,这意味着他们会等到需要结果的时候才执行。
7.2 获取 Stream#
- 所有的 Collection 集合都可以通过 stream 默认方法获取流;
- Stream 接口的静态方法 of 可以获取数组对应的流。
(1)根据 Collection 获取流
通过 Collection<I>
的 stream()
将任何集合转换为一个流。
default Stream<E> stream()
返回一个顺序 Stream 与此集合作为其来源。default Stream<E> parallelStream()
返回可能并行的 Stream 与此集合作为其来源,该方法允许返回顺序流。
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
测试代码:
public static void main(String[] args) {
// 集合获取流
// Collection接口中的方法: default Stream<E> stream() 获取流
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
// ...
Stream<String> stream2 = set.stream();
Vector<String> vector = new Vector<>();
// ...
Stream<String> stream3 = vector.stream();
// Map获取流
Map<String, String> map = new HashMap<>();
// ...
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
}
java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分 key&value 或 entry 等情况。
(2)使用 Arrays
的静态方法 stream()
获取数组 Stream
static IntStream stream(int[] array)
// 返回顺序 IntStream 与指定的数组作为源
static IntStream stream(int[] array, int startInclusive, int endExclusive)
// 返回顺序 IntStream 与指定的数组作为源的指定范围
static LongStream stream(long[] array)
// 返回顺序 IntStream 与指定的数组作为源的指定范围
static LongStream stream(long[] array, int startInclusive, int endExclusive)
// 返回顺序 LongStream 与指定的数组作为源的指定范围
static DoubleStream stream(double[] array)
// 返回顺序 DoubleStream 与指定的数组作为源
static DoubleStream stream(double[] array, int startInclusive, int endExclusive)
// 返回顺序 DoubleStream 与指定的数组作为源的指定范围
static <T> Stream<T> stream(T[] array)
// 返回顺序 Stream 与指定的数组作为源
static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive)
// 返回顺序 Stream 与指定的数组作为源的指定范围
测试代码:
@Test
public void test2() {
IntStream intStream = Arrays.stream(new int[]{1, 2, 3, 4, 5});
Stream<Employee> stream = Arrays.stream(new Employee[10]);
}
(3)Stream 中的静态方法 of 获取流
Stream
类静态方法 of()
通过显示值创建一个流。它可以接收任意数量的参数。
static <T> Stream<T> of(T... values)
返回其元素是指定值的顺序排序流
测试代码:
public static void main(String[] args) {
// Stream 中的静态方法: static Stream of(T... values)
Stream<String> stream6 = Stream.of("aa", "bb", "cc");
String[] arr = {"aa", "bb", "cc"};
Stream<String> stream7 = Stream.of(arr);
Integer[] arr2 = {11, 22, 33};
Stream<Integer> stream8 = Stream.of(arr2);
// 注意!基本数据类型的数组不行
int[] arr3 = {11, 22, 33};
Stream<int[]> stream9 = Stream.of(arr3);
}
(4)创建无限流
可以使用静态方法 Stream.iterate()
和 Stream.generate()
创建无限流。
[生成] static <T> Stream<T> generate(Supplier<T> s)
// 返回无限顺序无序流,其中每个元素由提供的 Supplier // Supplier<T>: T get()
[迭代] static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
// 返回有序无限连续 Stream 由函数的迭代应用产生 f 至初始元素 seed,产生 Stream
// 包括 seed/f(seed)/f(f(seed))等。UnaryOperator<T>: T apply(T t)
测试代码:
@Test
public void tes4() {
// 迭代,遍历前 10 个偶数
Stream.iterate(0, t -> t+2).limit(10).forEach(System.out :: println);
// 生成,输出 10 个随机数
Stream.generate(Math::random).limit(10).forEach(System.out :: println);
}
7.3 Stream 常用方法#
Stream 流模型的操作很丰富,这里介绍一些常用的 API。这些方法可以被分成两种:
- 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和 forEach 方法(本小节之外的更多方法,请自行参考 API 文档)。
- 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用(除了终结方法外,其余方法均为非终结方法)。
Stream 注意事项:
- Stream 只能操作一次
- Stream 方法返回的是新的流
- Stream 不调用终结方法,中间的操作不会执行
a. forEach#
forEach 用来遍历流中的数据。
方法签名:
void forEach(Consumer<? super T> action);
该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。
@Test
public void testForEach() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "1", "2", "3", "4", "5", "6");
// one.stream().forEach(s -> System.out.println(s));
one.stream().forEach(System.out::println);
}
b. count#
Stream 流提供 count 方法来统计其中的元素个数。
方法签名:
long count();
该方法返回一个 long 值代表元素个数。
@Test
public void testCount() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "1", "2", "3", "4", "5", "6");
System.out.println(one.stream().count());
}
c. filter#
filter 用于过滤数据,返回符合过滤条件的数据。可以通过 filter 方法将一个流转换成另一个子集流。
方法签名:
Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个 Predicate 函数式接口参数(可以是一个 Lambda 或方法引用)作为筛选条件。
@Test
public void testFilter() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "NAYEON", "JeongYeon", "MOMO", "Sana", "Jihyo", "Mina");
one.stream().filter(s -> s.length() == 4).forEach(System.out::println);
}
d. limit#
limit 方法可以对流进行截取,只取用前 N 个。
方法签名:
Stream<T> limit(long maxSize);
参数是一个 long 型,如果集合当前长度大于参数则进行截取。否则不进行操作。
@Test
public void testLimit() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "NAYEON", "JeongYeon", "MOMO", "Sana", "Jihyo", "Mina");
one.stream().limit(2).forEach(System.out::println);
}
e. skip#
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:
方法签名:
Stream<T> skip(long n);
如果流的当前长度大于 n,则跳过前 n 个;否则将会得到一个长度为 0 的空流。
@Test
public void testSkip() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "NAYEON", "JeongYeon", "MOMO", "Sana", "Jihyo", "Mina");
one.stream().skip(2).forEach(System.out::println);
}
f. map#
如果需要将流中的元素映射到另一个流中,可以使用 map 方法。
方法签名:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个 Function 函数式接口参数,可以将当前流中的 T 类型数据转换为另一种 R 类型的流。
@Test
public void testMap() {
Stream<String> original = Stream.of("11", "22", "33");
Stream<Integer> result = original.map(Integer::parseInt);
result.forEach(s -> System.out.println(s + 10));
}
这段代码中,map 方法的参数通过方法引用,将字符串类型转换成为了 int 类型(并自动装箱为 Integer 类对象)。
g. sorted#
如果需要将数据排序,可以使用 sorted 方法。
方法签名:
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
示例:
@Test
public void testSorted() {
// sorted(): 根据元素的自然顺序排序
// sorted(Comparator<? super T> comparator): 根据比较器指定的规则排序
Stream.of(33, 22, 11, 55)
.sorted()
.sorted((o1, o2) -> o2 - o1)
.forEach(System.out::println);
}
h. distinct#
如果需要去除重复数据,可以使用 distinct 方法。
方法签名:
Stream<T> distinct();
示例代码:
@Test
public void testDistinct() {
Stream.of(22, 33, 22, 11, 33)
.distinct()
.forEach(System.out::println);
}
自定义类型是根据对象的 hashCode 和 equals 来去除重复元素的。
i. match#
如果需要判断数据是否匹配指定的条件,可以使用 match 相关方法。
方法签名:
boolean allMatch(Predicate<? super T> predicate);
boolean anyMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
示例代码:
@Test
public void testMatch() {
boolean b = Stream.of(5, 3, 6, 1)
// .allMatch(e -> e > 0); // allMatch 元素是否全部满足条件
// .anyMatch(e -> e > 5); // anyMatch 元素是否任意有一个满足条件
.noneMatch(e -> e < 0); // noneMatch 元素是否全部不满足条件
System.out.println("b = " + b);
}
j. find#
如果需要找到某些数据,可以使用 find 相关方法。
方法签名:
Optional<T> findFirst();
// 作用:返回流中的第一个元素,如果流为空,则返回一个空的 Optional。
// 特性:在遇到第一个符合条件的元素时,findFirst() 会立即返回该元素。
// 顺序性:对于有序流,它总是返回第一个元素;对于无序流,也会返回一个元素,但不保证是第一个。
Optional<T> findAny();
// 作用:返回流中的任意一个元素,同样地,如果流为空,则返回一个空的 Optional。
// 特性:findAny() 可能会返回任何符合条件的元素,通常在并行流中使用时性能更优,因为它不需要保证返回的是第一个元素。
// 顺序性:对于有序流,findAny() 仍然会返回一个元素,但在无序流中,它可以返回任意元素。
k. max&min#
如果需要获取最大和最小值,可以使用 max 和 min 方法。
方法签名:
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
l. reduce#
是 Java Stream API 提供的一个终止操作,用于将流中的元素结合成一个单一的结果。它通过指定的累加器函数对流中的所有元素进行归约,使用一个初始值(identity)作为起始点。
方法签名:
T reduce(T identity, BinaryOperator<T> accumulator);
// - identity: 这是归约操作的初始值,作为计算的起点。
// - accumulator: 这是一个二元操作符,定义了如何将两个元素结合起来。它接收两个参数
// (当前累积的结果和流中的下一个元素),并返回一个新的累积结果。
T reduce(BinaryOperator b)
// 可以将流中元素反复结合起来,得到一个值,返回 Optional<T>
使用场景:
- 当你需要将流中的多个元素合并成一个结果时,例如求和、求最大值、拼接字符串等。
- 特别适用于需要从多个值生成一个单一值的场景。
示例代码:
public static void main(String[] args) {
// 1. 使用 reduce 求和
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum);
// 输出: Sum: 15
// 2. 使用 reduce 拼接字符串
List<String> words = Arrays.asList("Hello", "World", "Java");
String result = words.stream().reduce("", (a, b) -> a + " " + b).trim();
System.out.println("Concatenated String: " + result);
// 输出: Concatenated String: Hello World Java
// 3. 使用 reduce 计算最大值
numbers = Arrays.asList(10, 20, 30, 40, 50);
int max = numbers.stream().reduce(Integer.MIN_VALUE, (a, b) -> a > b ? a : b);
System.out.println("Max: " + max);
// 输出: Max: 50
}
map
和 reduce
的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。下面是一个使用 Java Stream API 结合 map
和 reduce
的示例代码。
import java.util.Arrays;
import java.util.List;
public class MapReduceExample {
public static void main(String[] args) {
// Test1: 计算 1~10 的自然数的和
// reduce(T identity, BinaryOperator) 可以将流中元素反复结合起来得到一个值。返回 T
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum = list.stream().reduce(0, Integer::sum);
System.out.println(sum);
// Test2: 计算公司所有员工的工资总和
// reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 Optional<T>
List<Employee> employees = EmployeeData.getEmployees();
// employees.stream().map(Employee::getSalary).reduce(Double::sum);
Optional<Double> sumSalary = employees.stream().map(Employee::getSalary).reduce((d1, d2) -> d1 + d2);
System.out.println(sumSalary);
// Test3: 计算一个整数列表的平方和
// 创建一个整数列表
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 map 和 reduce 计算平方和
int sumOfSquares = numbers.stream()
.map(n -> n * n) // 将每个数字映射为它的平方
.reduce(0, (a, b) -> a + b); // 将所有平方数相加
System.out.println("Sum of squares: " + sumOfSquares); // 输出: Sum of squares: 55
}
}
此示例展示了如何使用 map
来转换流中的元素,然后使用 reduce
来聚合这些转换后的结果。这种组合非常常见,在处理数据时能够高效而简洁地完成任务。
m. mapToInt#
是 Java Stream API 中的一个中间操作,主要用于将流中的元素映射为 int 类型的值。这个方法适用于处理对象流,将每个元素通过指定的函数转换为 int 类型,并返回一个 IntStream。
方法签名:
IntStream mapToInt(ToIntFunction<? super T> mapper);
// 【参数】 一个 ToIntFunction 接口的实例,它接收流中的元素并返回一个 int 值。
// 【返回】返回一个新的 IntStream,包含了通过 mapper 函数转换后的所有 int 值。
示例代码:
@Test
public void testIntStream() {
// Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱
Stream<Integer> stream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5});
// 先将流中的Integer数据转成IntStream
IntStream intStream = stream.mapToInt(Integer::intValue);
// 过滤、聚合
int reduce = intStream.filter(i -> i > 3).reduce(0, Integer::sum);
System.out.println(reduce);
// 将IntStream转化为Stream<Integer>
// 1. 生成一个包含 1 到 10(包括 10)的整数流。
IntStream intStream1 = IntStream.rangeClosed(1, 10);
// 2. 将基本类型的 IntStream 转换为对象类型的 Stream<Integer>。这一步是必要的,因为 IntStream 不能直接处理泛型。
Stream<Integer> boxed = intStream1.boxed();
// 3. 遍历并打印每个元素的类名和内容
boxed.forEach(s -> System.out.println(s.getClass() + ", " + s));
}
n. concat#
如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b);
这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。
示例代码:
@Test
public void testContact() {
Stream<String> streamA = Stream.of("twice");
Stream<String> streamB = Stream.of("MiSaMo");
Stream<String> result = Stream.concat(streamA, streamB);
result.forEach(System.out::println);
}
【综合案例】现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求依次进行以下若干操作步骤:
- 第一个队伍只要名字为3个字的成员姓名;
- 第一个队伍筛选之后只要前3个人;
- 第二个队伍只要姓张的成员姓名;
- 第二个队伍筛选之后不要前2个人;
- 将两个队伍合并为一个队伍;
- 根据姓名创建 Person 对象;
- 打印整个队伍的Person对象信息。
public class DemoStreamNames {
public static void main(String[] args) {
List<String> one = List.of("绫濑遥", "夏帆", "广濑铃子", "长泽雅美", "满岛光");
List<String> two = List.of("崇柏原", "坂口健太郎", "福士苍汰", "山崎贤人");
// 第一个队伍只要名字为3个字的成员姓名;
// 第一个队伍筛选之后只要前3个人;
Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);
// 第二个队伍只要姓张的成员姓名;
// 第二个队伍筛选之后不要前2个人;
Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);
// 将两个队伍合并为一个队伍;
// 根据姓名创建Person对象;
// 打印整个队伍的Person对象信息。
Stream.concat(streamOne, streamTwo).map(Person::new).forEach(System.out::println);
}
}
class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{name='" + name + "'}";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
7.4 收集 Stream 结果#
collect(Collector c)
将流转换为其他形式。接收一个 Collector<I>
的实现,用于给 Stream 中元素做汇总的方法。
Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。
另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:
a. 流中数据保存#
对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据。
Stream 流提供 collect 方法,其参数需要一个 java.util.stream.Collector<T,A,R>
接口对象来指定收集到哪种集合中。java.util.stream.Collectors 类提供一些方法,可以作为 Collector 接口的实例:
public static <T> Collector<T, ?, List<T>> toList() :转换为 List 集合。
public static <T> Collector<T, ?, Set<T>> toSet() :转换为 Set 集合。
Stream 提供 toArray 方法来将结果放到一个数组中,返回值类型是 Object[] 的:
Object[] toArray();
<A> A[] toArray(IntFunction<A[]> generator);
示例代码:
@Test
public void test() {
Stream<String> stream = Stream.of("aa", "bb", "cc");
// List<String> list = stream.collect(Collectors.toList());
// Set<String> set = stream.collect(Collectors.toSet());
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));
String[] strings = stream.toArray(String[]::new);
}
b. 流中数据聚合计算#
当我们使用 Stream 流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值、获取最小值、求总和、平均值、统计数量。
@Test
public void testStreamToOther() {
Stream<Student> studentStream = Stream.of(
new Student("a", 58, 95),
new Student("b", 56, 88),
new Student("c", 56, 99),
new Student("d", 52, 77));
// 获取最大值
Optional<Student> collect1 = studentStream.collect(Collectors.maxBy((o1, o2) -> o1.getSocre() - o2.getSocre()));
System.out.println(collect1.get());
// 获取最小值
Optional<Student> collect2 = studentStream.collect(Collectors.minBy((o1, o2) -> o1.getSocre() - o2.getSocre()));
System.out.println(collect2.get());
// 求总和
int sumAge = studentStream.collect(Collectors.summingInt(s -> s.getAge()));
System.out.println("sumAge = " + sumAge);
// 平均值
double avgScore = studentStream.collect(Collectors.averagingInt(s -> s.getSocre()));
System.out.println("avgScore = " + avgScore);
// 统计数量
Long count = studentStream.collect(Collectors.counting());
System.out.println("count = " + count);
}
c. 流中数据进行分组#
当我们使用 Stream 流处理数据后,可以根据某个属性将数据分组。
public class Test {
public static void main(String[] args) {
Stream<Student> studentStream = Stream.of(
new Student("a", 12, 95),
new Student("b", 17, 88),
new Student("c", 21, 55),
new Student("d", 25, 33));
// Map<Integer, List<Student>> map = studentStream.collect(Collectors.groupingBy(Student::getAge));
// 将分数大于60的分为一组,小于60分成另一组
Map<String, List<Student>> map = studentStream.collect(Collectors.groupingBy(s -> {
if (s.getScore() > 60) {
return "及格";
} else {
return "不及格";
}
}));
map.forEach((k, v) -> System.out.println(k + "::" + v));
}
}
class Student {
private String name;
private int age;
private int score;
public Student(String name, int age, int socre) {
this.name = name;
this.age = age;
this.score = socre;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public int getScore() {
return score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
d. 流中数据多级分组#
public static void main(String[] args) {
Stream<Student> studentStream = Stream.of(
new Student("a", 12, 95),
new Student("b", 17, 88),
new Student("c", 21, 55),
new Student("d", 25, 33));
Map<Integer, Map<String, List<Student>>> map =
studentStream.collect(
Collectors.groupingBy(s -> s.getAge() / 10,
Collectors.groupingBy(s -> {
if (s.getScore() >= 90) {
return "优秀";
} else if (s.getScore() >= 80 && s.getScore() < 90) {
return "良好";
} else if (s.getScore() >= 80 && s.getScore() < 80) {
return "及格";
} else {
return "不及格";
}
})));
map.forEach((k, v) -> System.out.println(k + " == " + v));
}
控制台打印:
1 == {优秀=[Student{name='a', age=12, score=95}], 良好=[Student{name='b', age=17, score=88}]}
2 == {不及格=[Student{name='c', age=21, score=55}, Student{name='d', age=25, score=33}]}
e. 流中数据进行分区#
Collectors.partitioningBy
会根据值是否为 true,把集合分割为两个列表,一个 true 列表,一个 false 列表。
public static void main(String[] args) {
Stream<Student> studentStream = Stream.of(
new Student("a", 12, 95),
new Student("b", 17, 88),
new Student("c", 21, 55),
new Student("d", 25, 33));
Map<Boolean, List<Student>> map = studentStream.collect(Collectors.partitioningBy(s -> s.getScore() > 90));
map.forEach((k, v) -> System.out.println(k + " == " + v));
}
控制台打印:
false == [Student{name='b', age=17, score=88}, Student{name='c', age=21, score=55}, Student{name='d', age=25, score=33}]
true == [Student{name='a', age=12, score=95}]
f. 流中数据进行拼接#
Collectors.joining
会根据指定的连接符,将所有元素连接成一个字符串。
public static void main(String[] args) {
Stream<Student> studentStream = Stream.of(
new Student("a", 12, 95),
new Student("b", 17, 88),
new Student("c", 21, 55),
new Student("d", 25, 33));
String collect = studentStream.map(Student::getName).collect(Collectors.joining(",", ">>> ", " <<<"));
System.out.println(collect);
}
控制台打印:
>>> a,b,c,d <<<
7.5 并行的 Stream 流#
并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。相比较串行的流,并行的流可以很大程度上提高程序的执行效率。
Java 8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过 parallel()
与 sequential()
在并行流与顺序流之间进行切换。
a. API#
串行的 Stream 流
目前我们使用的 Stream 流是串行的,就是在一个线程上执行。
public static void main(String[] args) {
long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
.filter(s -> {
System.out.println(Thread.currentThread() + ", s = " + s);
return true;
})
.count();
System.out.println("count = " + count);
}
控制台打印:
Thread[main,5,main], s = 4
Thread[main,5,main], s = 5
Thread[main,5,main], s = 3
Thread[main,5,main], s = 9
Thread[main,5,main], s = 1
Thread[main,5,main], s = 2
Thread[main,5,main], s = 6
count = 7
并行的 Stream 流
parallelStream 其实就是一个并行执行的流。它通过默认的 ForkJoinPool,可能提高多线程任务的速度。
获取并行 Stream 流的两种方式:① 直接获取并行的流 parallelStream() ② 将串行流转成并行流 parallel()
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
// 直接获取并行的流
// Stream<Integer> stream = list.parallelStream();
// 将串行流转成并行流
Stream<Integer> stream = list.stream().parallel();
}
并行操作代码:
public static void main(String[] args) {
long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
.parallel() // 将流转成并发流,Stream处理的时候将才去
.filter(s -> {
System.out.println(Thread.currentThread() + ", s = " + s);
return true;
})
.count();
System.out.println("count = " + count);
}
控制台打印:
Thread[ForkJoinPool.commonPool-worker-19,5,main], s = 5
Thread[ForkJoinPool.commonPool-worker-9,5,main], s = 2
Thread[ForkJoinPool.commonPool-worker-5,5,main], s = 6
Thread[ForkJoinPool.commonPool-worker-23,5,main], s = 4
Thread[main,5,main], s = 1
Thread[ForkJoinPool.commonPool-worker-13,5,main], s = 9
Thread[ForkJoinPool.commonPool-worker-27,5,main], s = 3
count = 7
parallelStream 线程安全问题
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(i);
}
List<Integer> newList = new ArrayList<>();
// 使用并行的流往集合中添加数据
list.parallelStream().forEach(newList::add);
System.out.println("newList = " + newList.size());
}
控制台打印:
newList = 837
我们明明是往集合中添加 1000 个元素,而实际上只有 837 个元素。
【解决方法】加锁、使用线程安全的集合或者调用 Stream 的 toArray() / collect() 操作就是满足线程安全的了。
b. Fork/Join#
parallelStream 背后的技术:Fork/Join
parallelStream 使用的是 Fork/Join 框架。Fork/Join 框架自 JDK 7 引入。Fork/Join 框架可以将一个大任务拆分为很多小任务来异步执行。框架主要包含三个模块:
ForkJoinPool 主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。
典型的应用比如快速排序算法,ForkJoinPool 需要使用相对少的线程来处理大量的任务。比如要对 1000w 个数据进行排序,那么会将这个任务分割成两个 500w 的排序任务和一个针对这两组 500w 数据的合并任务。以此类推,对于 500w 的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于 10 时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概 2000000+ 个。
问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
Fork/Join 原理 — 工作窃取算法
Fork/Join 最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的 CPU,那么如何利用好这个空闲的 CPU 就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个 Fork/Join 框架的核心理念。
Fork/Join工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
那么为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如 A 线程负责处理 A 队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。
而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
上文中已经提到了在 Java 8 引入了自动并行化的概念。它能够让一部分 Java 代码自动地以并行的方式执行,也就是我们使用了 ForkJoinPool 的 ParallelStream。
对于 ForkJoinPool 通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。可以通过设置系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N (N 为线程数量),来调整 ForkJoinPool 的线程数量,可以尝试调整成不同的参数来观察每次的输出结果。
【案例】使用 Fork/Join 计算 1-10000 的和,当一个任务的计算数量大于 3000 时拆分任务,数量小于 3000 时计算。
public class ForkJoinTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
SumRecursiveTask task = new SumRecursiveTask(1, 10000L);
Long result = pool.invoke(task);
System.out.println("result = " + result);
long end = System.currentTimeMillis();
System.out.println("消耗的时间为: " + (end - start));
}
}
class SumRecursiveTask extends RecursiveTask<Long> {
private static final long THRESHOLD = 3000L;
private final long start;
private final long end;
public SumRecursiveTask(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
// 任务不用再拆分了 可以计算了
if (length <= THRESHOLD) {
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
System.out.println("[CALC] (" + start + "," + end + ") = " + sum);
return sum;
} else {
// 数量大于预定的数量 任务还需要再拆分
long middle = (start + end) / 2;
System.out.println("[SPLIT] LEFT(" + start + "," + middle + ") RIGHT(" + (middle + 1) + "," + end + ")");
SumRecursiveTask left = new SumRecursiveTask(start, middle);
left.fork();
SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
right.fork();
return left.join() + right.join();
}
}
}
控制台打印:
[SPLIT] LEFT(1,5000) RIGHT(5001,10000)
[SPLIT] LEFT(1,2500) RIGHT(2501,5000)
[SPLIT] LEFT(5001,7500) RIGHT(7501,10000)
[CALC] (1,2500) = 3126250
[CALC] (2501,5000) = 9376250
[CALC] (7501,10000) = 21876250
[CALC] (5001,7500) = 15626250
result = 50005000
消耗的时间为: 41
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?