32-JDK8 新特性

1. 函数式接口#

只声明一个抽象方法的接口,称为"函数式接口"。

通过 Lambda 表达式来创建该接口的对象(若 Lambda 表达式抛出一个受检异常,即:非运行时异常,那么该异常需要在目标接口的抽象方法上进行声明)。

我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。

如何理解函数式接口?

我们知道使用 Lambda 表达式的前提是需要有函数式接口。而 Lambda 使用时不关心接口名、抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用 Lambda 方便,JDK 提供了大量常用的函数式接口。

它们主要在 java.util.function 包中。下面是最常用的几个接口。

  • Java 内置 4 个核心函数式接口
  • 其他接口

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 关键字

注意事项:

  1. 方法的参数或局部变量类型必须为接口才能使用 Lambda 表达式
  2. 接口中有且仅有一个抽象方法 => 函数式接口 @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 中的内容。

【小结】

  1. 匿名内部类在编译的时候会一个 class 文件
  2. 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
  }
}

接口默认方法的使用

  1. 实现类直接调用接口默认方法
  2. 实现类重写接口默认方法
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
  }
}

直接使用接口名调用即可:接口名.静态方法名();

接口默认方法和静态方法的区别:

  1. 默认方法通过实例调用,静态方法通过接口名调用。
  2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
  3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。

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 存在的问题:

  1. 设计很差:在 java.util 和 java.sql 的包中都有日期类,java.util.Date 同时包含日期和时间,而 java.sql.Date 仅包含日期。此外用于格式化和解析的类在 java.text 包中定义。
  2. 非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
  3. 时区处理麻烦:日期类并不提供国际化,没有时区支持,因此 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);
    }
    
  • TemporalAdjusters:该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。

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个字的 ③ 打印这些数据

处理这个需求需要含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人
  2. 然后筛选名字有三个字的人
  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 注意事项:

  1. Stream 只能操作一次
  2. Stream 方法返回的是新的流
  3. 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

}

mapreduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。下面是一个使用 Java Stream API 结合 mapreduce 的示例代码。

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 集合存储队伍当中的多个成员姓名,要求依次进行以下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;
  2. 第一个队伍筛选之后只要前3个人;
  3. 第二个队伍只要姓张的成员姓名;
  4. 第二个队伍筛选之后不要前2个人;
  5. 将两个队伍合并为一个队伍;
  6. 根据姓名创建 Person 对象;
  7. 打印整个队伍的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
posted @   tree6x7  阅读(156)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示
主题色彩