Java-8新特性

函数的第一条规则是要短小,第二条规则是还要更短小。——《Clean Code》

Lambda表达式

Lambda表达式是Java 8最重要、最具革命性的特性之一,它让Java能够编写更简洁、更灵活的代码,尤其是在处理集合和并发时。

简单来说,Lambda表达式是一个匿名函数。它没有名称,可以作为参数传递给方法,也可以向普通对象一样被赋值给变量。它的主要目的是简化那些只包含一个抽象方法的接口(我们称之为函数式接口)的实现。

我们以多线程中的Runnable接口(只有一个 run()方法)为例:

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run(){
System.out.println("Hello from a thread (old way)!");
}
}).start();

使用Lambda表达式,同样的逻辑可以这样写:

1
new Thread(() -> System.out.println("Hello from a thread (new way)!")).start();

Lambda表达式的语法

Lambda表达式的基本语法结构是:

1
(参数列表) -> {表达式或语句块};

(参数列表)

  • 无参数()
    • 示例:() -> System.out.println("Hello")
  • 一个参数(param)
    • 示例:(int a)->a * a
    • 简写:a -> a * a(编译器自动判断a的类型)
  • 多个参数(param1, parm2)
    • 示例:(int a, int b) -> a+b
    • 简写:(a,b) -> a+b

->(箭头符号):

  • 这是一个非常重要的分隔符,它将参数列表和Lambda体分开。

{ 表达式或语句块 }

  • 单个表达式:如果Lambda体只有一行表达式,可以省略大括号和分号,表达式的结果自动作为返回值。
    • 示例:a -> a*a 自动返回 a*a的值
  • 多行语句块:如果Lambda体包含多行语句,则必须使用大括号,并且需要明确使用 return语句返回值(如果Lambda表达式有返回值)。
    • 示例:(int a) -> {int sum = a*a; return sum;}

函数式接口

Lambda表达式不能独立存在,它必须依附于一个函数式接口。

  • 定义:函数式接口是只包含一个抽象方法的接口。
  • 注解:Java 8引入了一个新的注解 @FunctionalInterface,它用于标识一个接口是函数式接口。这个注解是可选的,但是强烈建议使用,因为它可以帮助编译器检查接口是否符合函数式接口的定义,如果接口包含多余一个抽象方法,编译器会报错。
  • 作用:Lambda表达式就是函数式接口的实例。当你写一个Lambda表达式时,实际上实在提供一个函数式接口的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@FunctionalInterface
interface MyConverter {
// 唯一的抽象方法
double convert(double input);

// 可以有默认方法(default method)
default void printInfo() {
System.out.println("This is a converter.");
}

// 可以有静态方法(static method)
static String getVersion() {
return "1.0";
}
}

public class LambdaDemo {
public static void main(String[] args) {
// 使用Lambda表达式实现 MyConverter 接口
MyConverter celsiusToFahrenheit = (celsius) -> (celsius * 9/5) + 32;

System.out.println("0摄氏度 = " + celsiusToFahrenheit.convert(0) + "华氏度"); // 输出 32.0

celsiusToFahrenheit.printInfo(); // 调用默认方法
System.out.println("Converter Version: " + MyConverter.getVersion()); // 调用静态方法
}
}

Java内置函数式接口

java 8在 java.util.function包中提供了许多预定义的函数式接口,这些接口覆盖率常见的函数类型,方便我们在Stream API等地方使用。

  • Consumer<T>消费者。接收一个参数,没有返回值。
    • void accept(T t);

    • 示例:

      1
      2
      List<String> names = Arrays.asList("Alice","Bob");
      names.forEach(name -> System.out.println(name));
  • Predicate<T>断言。接收一个参数,返回一个布尔值。
    • boolean test(T t);

    • 示例:

      1
      2
      List<Integer> numbers = Arrays.asList(1,2,3,4,5);
      numbers.stream().filter(n -> n % 2 == 0).forEach(System.out::println);
  • Function<T,R>函数。接收一个T类型的参数,返回一个R类型的参数。
    • R apply(T t);

    • 示例:

      1
      2
      Function<String,Integer> stringLength = s -> s.length();
      System.out.println(stringLength.apply("Hello"));
  • Supplier<T>供应商。不接受参数,返回一个T类型结果。
    • T get();

    • 示例:

      1
      2
      Supplier<Double> randomValue = () -> Math.random();
      System.out.println(randomValue.get());
  • UnaryOperator<T>一元运算。接收一个T类型参数,返回一个T类型的结果(输入和输出类型相同)。
    • T apply(T t);继承自 Function<T,T>

    • 示例:

      1
      2
      UnaryOperator<Integer> square = n -> n * n;
      System.out.println(square.apply(5));
  • BinaryOperator<T>二元运算。接收两个T类型参数,返回一个T类型结果
    • T apply(T t1, T t2);

    • 示例:

      1
      2
      BinaryOperator<Integer> sum = (a, b) -> a + b;
      System.out.println(sum.apply(10,20));

还有针对基本数据类型(如 IntConsumerLongFunctionDoublePredicate等)的特化版本,以避免自动装箱/拆箱的性能开销。

java.util.function包的官方文档:Package java.util.function

常见的运用场景

集合遍历

Java 8 在 Iterable接口中引入 forEach方法,提供了一种非常简洁、易读的方式来遍历集合。

适用对象ListSetQueue、实现 Collection接口、实现 Iterable接口

在java 8之前,我们通常使用传统的 for循环或增强型 for循环来遍历集合:

1
2
3
4
5
6
7
8
9
10
11
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

//传统for
for(int i = 0; i < names.size(); i++){
System.out.println(names.get(i));
}

//增强型for循环
for(String name : names){
System.out.println(name);
}

循环的过程完全暴露在外部,forEach方法提供了一种将迭代过程隐藏在函数内部,而由参数提供方法体的遍历方法:

1
names.forEach(name -> System.out.println(name));

原理

我们查看forEach的实现源码:

1
2
3
4
5
6
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}

它接收一个Consumer 类型的参数。我们编写的Lambda表达式即为一个实现了 Consumer接口 accept()方法的匿名实现类。 forEach()在内部遍历names的每一个元素,并将元素作为参数传递给这个Lambda表达式的 accept()方法,从而执行我们定义的操作。

Map中的 forEach()

Map接口本身没有实现 Iterable,但他有自己的 forEach()方法,接收一个 BiConsumer函数式接口(接收两个参数,无返回值),用于遍历键值对:

1
2
3
4
5
6
7
8
9
10
Map<String, Integer> fruitPrices = new HashMap<>();
fruitPrices.put("Apple", 10);
fruitPrices.put("Banana", 5);
fruitPrices.put("Orange", 8);

fruitPrices.forEach((fruit, price) -> System.out.println(fruit + " costs $" + price));
// 输出:
// Apple costs $10
// Banana costs $5
// Orange costs $8

排序(sort)

1
2
3
4
List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9);
// 降序排序
Collections.sort(numbers, (a, b) -> b.compareTo(a));
System.out.println(numbers); // 输出: [9, 8, 5, 2, 1]

线程(Runnable)

1
2
3
4
5
6
7
Runnable task = () -> {
System.out.println("异步任务执行中...");
// 模拟耗时操作
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("异步任务完成。");
};
new Thread(task).start();

Lambda表达式的注意事项

变量捕获(Variable Capture)

Lambda表达式可以访问其定义范围内的局部变量,但这些变量必须是effectively final的(即它们在初始化后不会再被修改,即使没有明确声明 final)。

Lambda表达式也不能修改其捕获的局部变量

1
2
3
4
int factor = 10; // 这是一个 effectively final 变量
// factor++; // 如果取消注释这一行,下面的Lambda表达式会报错
Function<Integer, Integer> multiplier = n -> n * factor;
System.out.println(multiplier.apply(5)); // 输出 50

方法引用

当Lambda表达式只是简单的调用一个现有方法时,可以使用方法引用来进一步简化代码,提高可读性。

1
2
3
names.forEach(name -> System.out.println(name));
//可以化简为
names.forEach(System.out::println);

接下来我们来看看什么时候可以这么做:

条件

  • 被引用的方法的参数列表和返回类型,与它所实现的(或兼容)的函数式接口的抽象方法的参数列表和返回类型完全匹配,就可以使用方法引用。

示例:Consumer.accept方法的参数列表与 Syetem.out.println方法的参数列表是兼容的。

1
2
void accept(T t);
void println(Object x);

所以在forEach()中可以直接引用

Stream API

Stream API 与 Lambda表达式是java8 的黄金搭档,它为Java中的集合带来了函数式编程的范式。它的主要目的是让我们能够以一种更声明式、更简洁、更高效的方式来处理集合数据

在Java 8 中,Stream(流)像是一个数据管道。它允许你以声明式的方式处理数据集合(如 ListSetMap等),而无需关心背后的具体实现细节。

你可以把Stream理解为对集合数据执行一系列操作的序列。这些操作额可以被链接起来(链式调用),形成一个处理数据的流水线。

Stream特点

  • 非侵入性:Stream不会修改原始数据源。每次操作都会生成一个新的Stream,原始集合保持不变。
  • 惰性执行(Lazy Evaluation):Stream的中间操作(Intermediate Operation)是惰性执行的。它们不会立即执行,而是等到终端操作(Terminal Operation)被调用时才一起执行。这有助于优化性能。例如:你只需要前几个元素,Stream可能会短路(short-circuiting)而无需处理所有数据。
  • 一次性消费:一个Stream只能被消费一次。一旦执行了终端操作,Stream就被关闭了,不能再对其进行任何操作。如果想再次处理数据,就只能重新从数据源获取一个新的Stream。

Stream操作类型

Stream操作官方文档:Stream()方法

Stream操作可以分为两大类:

中间操作(Intermediate Operations)

中间操作会返回一个新的Stream,因此你可以将多个中间操作串联起来形成一个链式调用。它们是惰性的,不会执行计算,只会构建一个操作的管道。

常见的中间操作包括:

  • filter(Predicate<T> predicate):过滤元素,只保留符合条件的元素(返回值为True)。

    1
    list.stream().filter(n -> n % 2 == 0);
  • map(Function<T, R> mapper):将Stream中的每个元素T转换(映射)成另一种类型R。

    1
    list.stream.map(String::toUpperCase);	//将字符串转化为大写
  • flatMap(Function<T, Stream<R>> mapper):将Stream中的每个元素转化为一个Stream,然后将这些Stream合并成一个Stream。用于处理嵌套集合。

    1
    2
    List<List<String>> nestedList = Arrays.asList(Arrays.asList("a", "b"),Arrays.asList("c", "d"));
    nestedList.stream().flatMap(Collection::Stream);// 将 [[a,b], [c,d]] 扁平化为 [a,b,c,d]
  • distinct():去除重复元素(基于equals()方法)。

    1
    list.stream().distinct();
  • sorted()/sorted(Consumer<T> action):对元素进行排序

    1
    2
    list.stream().sorted();	//自然排序
    list.stream().sorted((a,b) -> b.compareTo(a)); //自定义降序排序
  • peek(Consumer<T> action):对Stream中的每个元素执行一个操作,但不改变Stream,主要用于测试

    1
    list.stream().peek(System.out::println);	//打印每个元素,但不影响后续操作
  • limit(long maxSize):截断Stream,使其元素不超过给定数量。

    1
    list.stream.limit(5);	//只取前5个元素
  • skip(long n):跳过Stream中的前n个元素。

    1
    list.stream().skip(2);

终端操作(Terminal Operations)

终端操作会触发中间操作的执行,并且会生成一个最终结果或产生一个副作用(例如,打印到控制台)。一个Stream在执行了终端操作后就不能再被使用了。

常见的终端操作:

  • forEach(Consumer<T> action):对Stream中的每一个元素执行一个操作。

    1
    list.stream().forEach(System.out::println);
  • collect(Collector<T, A, R> collector):将Stream中的元素收集到集合、Map或其他数据结构中。这是最常用的终端操作之一。

    1
    2
    list.stream().filter.(n -> n % 2 == 0).collect(Collectors.toList());
    list.stream().map(String::length).collect(Collectors.toSet());

    Collectors 类提供了许多静态工厂方法,方便我们进行各种收集操作。

  • reduce(T identity, BinaryOperator<T> accumulator)/reduce(BinaryOperator<T> accumulator):将Stream中的元素规约(聚合)成一个值。

    1
    2
    list.stream().reduce(0, (a, b) -> a + b);	//求和0是初始值,预设默认值,直接返回类型T。
    list.stream().map(String::length).reduce(Integer::sum) //返回Optional<T>
  • min(Comparator<T> comparator)/max(Comparator<T> comparator):返回Stream中的最小值/最大值。

    1
    list.stream().min(Integer::compare);	//找到最小值,返回Optional<Integer>
  • count():返回Stream中的元素数量。

    1
    list.stream().filter(n -> n > 10).count();
  • anyMatch(Predicate<T> predicate)/allMath(Predicate<T> predicate)/noneMatch(Predicate<T> predicate):检查Stream中是否有任何元素所有元素没有元素匹配给定条件,返回布尔值。

    1
    list.stream().anyMatch(s -> s.startsWith("A")); // 是否有元素以A开头
  • findFirst()/findAny():返回Steam中的第一个元素或任意一个元素(通常用于并行流)。它们都返回 Optional<T>

    1
    list.stream().filter(n -> n>5).findFirst();

    串行流时,findFirst()等价于findAny()

  • toArray:将Stream()中的元素转化为数组。

    1
    list.stream().toArray(String[]::new);

    list.stream().toArray()返回的是Object[],需要强制类型转换。

Stream的创建方式

  1. 从集合创建

    • Collection.stream():为任何集合(List, Set等)创建串行Sream。
    • Collection.parallelStream():为任何集合创建并行Stream。
    1
    2
    3
    List<String> myNames = Arrays.asList("Alice", "Bob", "Charlie");
    Stream<String> stream1 = myNames.stream();
    Stream<String> parallelStream = myNames.parallelStream();
  2. 从数组创建

    • Arrays.stream(T[] array
    1
    2
    String[] myArr = {"Apple", "Banana"};
    Stream<String> stream2 = Arrays.stream(myArr);
  3. 使用 Stream.of()

    • 直接指定一系列元素来创建Stream。
    1
    Stream<String> stream3 = Stream.of("one", "two", "three");
  4. 使用 Stream.generate()

    • 生成无限Stream,需要一个 Supplier。通常需要与 limit()结合使用。
    1
    2
    Supplier<Double> randomValue = () -> Math.random();
    Stream<Double> stream4 = Stream.generate(randomValue).limit(3); //限制生成三个,否则会无限生成
  5. 使用 Stream.iterate()

    • 生成无线Stream,可以基于前一个元素进行计算。
    1
    2
    Stream<Integer> stream5 = Stream.iterate(1, n -> n * 2).limit(3);	//创建2的幂次流
    stream5.forEach(System.out::println);
  6. 基本类型Stream

    • IntStreamLongStreamDoubleStream,用于避免基本类型的自动装箱,拆箱的性能开销。
    1
    IntStream.range(1, 5).forEach(System.out::println); // 输出 1, 2, 3, 4

实战示例

需求:从一个学生列表中,找出所有年龄大于20岁,并且名字以”J”开头的学生,然后将他们的名字转换为大写并收集到一个新的列表中。

Java 7 及之前(命令式编程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.ArrayList;
import java.util.List;

class Student {
String name;
int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() { return name; }
public int getAge() { return age; }
}

public class OldStyleStream {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("John", 22),
new Student("Jane", 19),
new Student("Mike", 25),
new Student("Jessie", 21),
new Student("Chris", 30)
);

List<String> resultNames = new ArrayList<>();
for (Student student : students) {
if (student.getAge() > 20 && student.getName().startsWith("J")) {
resultNames.add(student.getName().toUpperCase());
}
}
System.out.println(resultNames); // 输出: [JOHN, JESSIE]
}
}

Java 8 Stream API(声明式编程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

// Student 类定义同上

public class StreamApiExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("John", 22),
new Student("Jane", 19),
new Student("Mike", 25),
new Student("Jessie", 21),
new Student("Chris", 30)
);

List<String> resultNames = students.stream() // 1. 获取 Stream
.filter(student -> student.getAge() > 20) // 2. 中间操作:过滤年龄大于20
.filter(student -> student.getName().startsWith("J")) // 3. 中间操作:过滤名字以J开头
.map(student -> student.getName().toUpperCase()) // 4. 中间操作:名字转大写
.collect(Collectors.toList()); // 5. 终端操作:收集结果到List

System.out.println(resultNames); // 输出: [JOHN, JESSIE]
}
}

Stream API编程的优点:

  • 声明式:你告诉Stream你想要什么结果(过滤、映射、收集),而不是如何一步步实现这个结果。
  • 易于并行化:只需要将student.stream()改为student.parallelStream(),就可以将整个操作链并行执行,而无需手动编写复杂的线程代码。

接口的增强

在Java8之前,接口中只能有抽象方法常量。Java8引入了两种可以在接口中带有实现的方法:默认方法(Default Methods)和静态方法(Static Methods)。

静态方法

  • 定义方式:在接口中,你可以使用static关键字来定义一个静态方法。静态方法和类中的静态方法行为类似,它们属于接口本身,而不是实现该接口的任何对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Calculator {
// 抽象方法
int add(int a, int b);

// 默认方法
default int subtract(int a, int b) {
return a - b;
}

// 静态方法 (Java 8 新特性)
static int multiply(int a, int b) {
return a * b;
}

static int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Divisor cannot be zero.");
}
return a / b;
}
}
  • 调用方法:
    • 类方法可以通过类名子类名或者对象名来调用静态类方法。
    • 接口的静态方法只能通过接口名来调用。

默认方法

默认方法,在Java8中也被称为”虚拟扩展方法”(virtual extension methods),是接口的一项重要增强。在Java8之前,如果你想要向一个已有的接口中添加新方法,所有实现了这个接口的类都必须同时修改,否则代码将无法编译通过。

默认方法的引入正是为了”接口演进”的问题。

  • 定义方式:使用 default关键字修饰接口中的默认方法。
1
2
3
4
5
6
7
public interface MyInterface {
void abstractMethod();

default void defaultMethod(){
System.out.println("这是一个默认方法,有具体的实现。");
}
}
  • 后向兼容:这是默认方法最核心的价值。当你在一个已发布的接口中添加新的默认方法时,所有已有的实现类不需要做任何修改就能兼容运行。它们会自动继承并使用这个默认实现。
  • 子类可以选择性的重写:实现接口的类可以自由的选择重写还是使用默认方法。

默认方法的冲突规则

当一个类实现了多个接口,并且这些接口包含同名的默认方法时,Java8 有一套明确的规则来解决冲突。

  1. 类优先:如果实现类中有一个与接口默认方法同名的方法(无论是继承自父类还是自己定义),那么总是使用类中的实现。接口的默认方法会被忽略。

  2. 子接口优先:如果一个接口继承了另一个接口,并且它们都定义了同名的默认方法,那么子接口的默认方法会优先于父接口的默认方法。

  3. 显示解决:如果以上两条规则都无法解决冲突,那么编译器会报错,要求实现类碧玺明确的重写这个方法,并在重写的方法中选择调用哪个接口的默认实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    interface A { default void doIt() { System.out.println("Do it from A"); } }
    interface B { default void doIt() { System.out.println("Do it from B"); } }

    class ConflictClass implements A, B {
    // 编译器会报错,要求强制重写 doIt()
    @Override
    public void doIt() {
    // 可以选择调用 A 的默认方法
    A.super.doIt();
    // 或者调用 B 的默认方法
    B.super.doIt();
    // 或者提供自己的全新实现
    System.out.println("Do it from ConflictClass");
    }
    }

    在重写方法中,你可以使用 InterfaceName.super.methodName()的语法来调用特定接口的默认实现。

Optional类

Optional类是一个容器对象,它可能包含一个非 null的值,也可能不包含任何值。它的核心目的是为了解决空指针异常(NullPointerException)。

在传统Java编程中,我们经常遇到这样的代码:

1
2
3
4
String name = getSomeName();
if (name != null) {
System.out.println(name.toUpperCase());
}

为了避免 namenull时抛出异常,我们不得不进行繁琐的 null检查。当对象链很长时,这会变得更加麻烦和难以阅读,比如:user.getProfile().getAddress().getCity()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 传统方式的 null 检查
public String getCitySafely(User user) {
if (user != null) {
Profile profile = user.getProfile();
if (profile != null) {
Address address = profile.getAddress();
if (address != null) {
return address.getCity();
}
}
}
return "Unknown"; // 如果任何一个环节为 null,返回默认值
}

// 使用 Optional 进行 null 检查
public String getCitySafelyWithOptional(User user) {
// 1. 将 User 对象包装成 Optional
// 2. 使用 map() 链式调用,每个 map() 都会自动处理 null
// 3. 最后使用 orElse() 或 orElseGet() 提供一个默认值
return Optional.ofNullable(user)
.flatMap(User::getProfile)
.flatMap(Profile::getAddress)
.flatMap(Address::getCity)
.orElse("Unknown");
}

如何创建Optional对象

Optional 提供了几种静态工厂方法来创建实例:

  1. Optional.of(T value)

    • 用于创建一个包含非 null值的 Optional对象。
    • 如果传入 null会立即抛出异常 NullPointerException
    1
    Optional<String> optionalName = Optional.of("Alice");
  2. Optional.ofNullable(T value)

    • 这是最常用的方法,用于创建一个可能包含 null值的 Optional对象。
    • 如果传入的是null,它会返回一个空的 Optional实例(Optional.empty())
  3. Optional.empty()

    • 用于创建一个空的 Optional实例
    1
    Optional<String> optionalEmpty = Optional.empty();

Optional的常用方法

Optional类提供了许多方法来安全的操作和获取值,而不需要显式的 if (value != null)检查。

  1. isPresent()isEmpty()

    • isPresent():检查 Optional对象是否包含值,返回 boolean
    • isEmpty():Java11新增,与 isPresent()相反,检查 Optional是否为空
    1
    2
    3
    4
    Optional<String> optional = Optional.of("Hello");
    if (optional.isPresent()) {
    System.out.println("值存在!");
    }
  2. ifPresent(Consumer action)

    • 如果值存在,就执行 Consumer中的动作(Lambda表达式)。
    • 这是处理值最常见的方式之一,优雅的取代了 if-not-null检查。
    1
    2
    3
    4
    5
    Optional<String> optional = Optional.ofNullable("Java");
    optional.ifPresent(s -> System.out.println("值是:" + s)); // 输出:值是:Java

    Optional<String> emptyOptional = Optional.ofNullable(null);
    emptyOptional.ifPresent(s -> System.out.println("值是:" + s)); // 不会输出任何内容
  3. get()

    • 如果 Optional包含值,则返回该值;否则抛出 NoSuchException
    • 不推荐直接使用 get(),应为它和传统的null检查一样危险违背了Optional设计的初衷。
  4. orElse(T other)

    • 如果 Optional包含值,则返回该值;否则返回传入的默认值 other
    1
    2
    Optional<String> name = Optional.ofNullable(null);
    String result = name.orElse("默认值"); // result 将是 "默认值"
  5. orElseGet(Supplier other)

    • orElse类似,但它在 Optional为空时,才会执行 Supplier提供的代码块来生成默认值。
    • 如果生成默认值的操作很耗时,orElseGet()orElse()更高效,因为 orElse不管 Optional是否为空,都会先执行 other参数。
    1
    String result = name.orElseGet(() -> "从数据库获取的默认值"); // 只有在 name 为空时,才会执行 lambda
  6. orElseThrow()orElseThrow(Supplier exceptionSupplier)

    • 如果 Optional为空,则抛出异常。
    • orElseThrow()(Java10新增)默认抛出 NoSuchElementException
    • 你可以通过 orElseThrow(Supplier)指定要抛出的异常类型。
    1
    2
    Optional<String> name = Optional.ofNullable(null);
    String result = name.orElseThrow(() -> new IllegalArgumentException("值不能为空"));
  7. map(Function mapper)

    • 如果 Optional包含值,就对该值应用 Function函数,然后返回一个包含新值Optional
    • 这使得你可以链式地进行一系列操作。
    1
    2
    Optional<String> name = Optional.of("alice");
    Optional<Integer> length = name.map(String::length); // length 是 Optional<Integer>,值为 5
  8. faltMap(Function mapper)

    • 如果 Optional包含一个值,就将该值传递给一个函数,该函数必须返回一个 Optional对象。flatMap不会再对结果进行额外包装,而是直接返回这个 Optional

Optional的最佳实践

  • 作为方法返回值,而不是参数
  • 不要用 get()
  • 不要将 Optional作为成员变量
  • mapflatMap链式调用

感觉使用 map()方法或许比较好。对象方法返回值可以不要求为 Optional类型,由Map进行封装。维持了原函数不变。

HashMap升级

HashMap在JDK1.7到1.8有几个关键的升级。

数据结构

  • JDK1.7:HashMap的底层数据结构是数组+链表。数组中的每一个元素都是一个链表,当多个键哈希到同一个位置时,它们就会以链表的形式存储在这个位置上。
  • JDK1.8HashMap的底层数据结构是数组+链表+红黑树。当同一个哈希桶中的链表长度超过一个阈值(默认为8)时,为了提高查找效率,这个链表会自动转化为红黑树。当红黑树的大小小于一个阈值(默认为6)时,又会变回链表。

链表插入方式

  • JDK1.7:采用头插法。新插入的元素会放在链表的头部。这种方式在多线程环境下,容易造成死循环,尤其是在resize()过程中。
  • JDK1.8:采用尾插法。虽然 HashMap本身不是线程安全的

扩容

  • JDK1.7:扩容时需要重新计算所有元素的哈希值。然后重新分配到新的数组中。这个过程比较耗时。
  • JDK1.8:扩容时优化了重新分配的过程。每次扩容都是上一次容量的2倍(2的幂),当扩容到新数组时,只需要判断每个节点的哈希值和旧数组长度进行 & 运算。如果结果为0,节点位置不变;如果不为0,节点位置为 原位置+旧数组长度。这种方式避免了重新计算哈希值。

下表可以帮助你更清晰地对比两者:

特性 JDK 1.7 JDK 1.8
底层数据结构 数组 + 链表 数组 + 链表 + 红黑树
哈希冲突 长链表,查询效率O(n) 链表过长时转为红黑树,查询效率O(logn)
链表插入方式 头插法,多线程扩容易死循环 尾插法,解决了多线程下的死循环问题
扩容效率 需要重新计算所有元素的哈希值 优化了重新分配逻辑,效率更高
红黑树阈值 链表长度 >= 8 时转为红黑树,<= 6 时转为链表

java.time包


Java-8新特性
http://blog.ulna520.com/2025/07/29/Java-8新特性_20250729_211948/
Veröffentlicht am
July 29, 2025
Urheberrechtshinweis