函数的第一条规则是要短小,第二条规则是还要更短小。——《Clean Code》
Lambda表达式
Lambda表达式是Java 8最重要、最具革命性的特性之一,它让Java能够编写更简洁、更灵活的代码,尤其是在处理集合和并发时。
简单来说,Lambda表达式是一个匿名函数。它没有名称,可以作为参数传递给方法,也可以向普通对象一样被赋值给变量。它的主要目的是简化那些只包含一个抽象方法的接口(我们称之为函数式接口)的实现。
我们以多线程中的Runnable接口(只有一个 run()方法)为例:
new Thread(new Runnable() {
@Override
public void run(){
System.out.println("Hello from a thread (old way)!");
}
}).start();
使用Lambda表达式,同样的逻辑可以这样写:
new Thread(() -> System.out.println("Hello from a thread (new way)!")).start();
Lambda表达式的语法
Lambda表达式的基本语法结构是:
(参数列表) -> {表达式或语句块};
(参数列表):
- 无参数:
()- 示例:
() -> 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表达式时,实际上实在提供一个函数式接口的实现。
@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); -
示例:
List<String> names = Arrays.asList("Alice","Bob"); names.forEach(name -> System.out.println(name));
-
Predicate<T>:断言。接收一个参数,返回一个布尔值。-
boolean test(T t); -
示例:
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); -
示例:
Function<String,Integer> stringLength = s -> s.length(); System.out.println(stringLength.apply("Hello"));
-
Supplier<T>:供应商。不接受参数,返回一个T类型结果。-
T get(); -
示例:
Supplier<Double> randomValue = () -> Math.random(); System.out.println(randomValue.get());
-
UnaryOperator<T>:一元运算。接收一个T类型参数,返回一个T类型的结果(输入和输出类型相同)。-
T apply(T t);继承自Function<T,T> -
示例:
UnaryOperator<Integer> square = n -> n * n; System.out.println(square.apply(5));
-
BinaryOperator<T>:二元运算。接收两个T类型参数,返回一个T类型结果-
T apply(T t1, T t2); -
示例:
BinaryOperator<Integer> sum = (a, b) -> a + b; System.out.println(sum.apply(10,20));
-
还有针对基本数据类型(如 IntConsumer、LongFunction、DoublePredicate等)的特化版本,以避免自动装箱/拆箱的性能开销。
java.util.function包的官方文档:Package java.util.function
常见的运用场景
集合遍历
Java 8 在 Iterable接口中引入 forEach方法,提供了一种非常简洁、易读的方式来遍历集合。
适用对象:List、Set、Queue、实现 Collection接口、实现 Iterable接口
在java 8之前,我们通常使用传统的 for循环或增强型 for循环来遍历集合:
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方法提供了一种将迭代过程隐藏在函数内部,而由参数提供方法体的遍历方法:
names.forEach(name -> System.out.println(name));
原理
我们查看forEach的实现源码:
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函数式接口(接收两个参数,无返回值),用于遍历键值对:
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)
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)
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表达式也不能修改其捕获的局部变量
int factor = 10; // 这是一个 effectively final 变量
// factor++; // 如果取消注释这一行,下面的Lambda表达式会报错
Function<Integer, Integer> multiplier = n -> n * factor;
System.out.println(multiplier.apply(5)); // 输出 50
方法引用
当Lambda表达式只是简单的调用一个现有方法时,可以使用方法引用来进一步简化代码,提高可读性。
names.forEach(name -> System.out.println(name));
//可以化简为
names.forEach(System.out::println);
接下来我们来看看什么时候可以这么做:
条件:
- 被引用的方法的参数列表和返回类型,与它所实现的(或兼容)的函数式接口的抽象方法的参数列表和返回类型完全匹配,就可以使用方法引用。
示例:Consumer.accept方法的参数列表与 Syetem.out.println方法的参数列表是兼容的。
void accept(T t);
void println(Object x);
所以在forEach()中可以直接引用
Stream API
Stream API 与 Lambda表达式是java8 的黄金搭档,它为Java中的集合带来了函数式编程的范式。它的主要目的是让我们能够以一种更声明式、更简洁、更高效的方式来处理集合数据。
在Java 8 中,Stream(流)像是一个数据管道。它允许你以声明式的方式处理数据集合(如 List、Set、Map等),而无需关心背后的具体实现细节。
你可以把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)。list.stream().filter(n -> n % 2 == 0); -
map(Function<T, R> mapper):将Stream中的每个元素T转换(映射)成另一种类型R。list.stream.map(String::toUpperCase); //将字符串转化为大写 -
flatMap(Function<T, Stream<R>> mapper):将Stream中的每个元素转化为一个Stream,然后将这些Stream合并成一个Stream。用于处理嵌套集合。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()方法)。list.stream().distinct(); -
sorted()/sorted(Consumer<T> action):对元素进行排序list.stream().sorted(); //自然排序 list.stream().sorted((a,b) -> b.compareTo(a)); //自定义降序排序 -
peek(Consumer<T> action):对Stream中的每个元素执行一个操作,但不改变Stream,主要用于测试。list.stream().peek(System.out::println); //打印每个元素,但不影响后续操作 -
limit(long maxSize):截断Stream,使其元素不超过给定数量。list.stream.limit(5); //只取前5个元素 -
skip(long n):跳过Stream中的前n个元素。list.stream().skip(2);
终端操作(Terminal Operations)
终端操作会触发中间操作的执行,并且会生成一个最终结果或产生一个副作用(例如,打印到控制台)。一个Stream在执行了终端操作后就不能再被使用了。
常见的终端操作:
-
forEach(Consumer<T> action):对Stream中的每一个元素执行一个操作。list.stream().forEach(System.out::println); -
collect(Collector<T, A, R> collector):将Stream中的元素收集到集合、Map或其他数据结构中。这是最常用的终端操作之一。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中的元素规约(聚合)成一个值。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中的最小值/最大值。list.stream().min(Integer::compare); //找到最小值,返回Optional<Integer> -
count():返回Stream中的元素数量。list.stream().filter(n -> n > 10).count(); -
anyMatch(Predicate<T> predicate)/allMath(Predicate<T> predicate)/noneMatch(Predicate<T> predicate):检查Stream中是否有任何元素、所有元素、没有元素匹配给定条件,返回布尔值。list.stream().anyMatch(s -> s.startsWith("A")); // 是否有元素以A开头 -
findFirst()/findAny():返回Steam中的第一个元素或任意一个元素(通常用于并行流)。它们都返回Optional<T>list.stream().filter(n -> n>5).findFirst();串行流时,findFirst()等价于findAny()
-
toArray:将Stream()中的元素转化为数组。list.stream().toArray(String[]::new);list.stream().toArray()返回的是Object[],需要强制类型转换。
Stream的创建方式
-
从集合创建:
Collection.stream():为任何集合(List,Set等)创建串行Sream。Collection.parallelStream():为任何集合创建并行Stream。
List<String> myNames = Arrays.asList("Alice", "Bob", "Charlie"); Stream<String> stream1 = myNames.stream(); Stream<String> parallelStream = myNames.parallelStream(); -
从数组创建:
Arrays.stream(T[] array
String[] myArr = {"Apple", "Banana"}; Stream<String> stream2 = Arrays.stream(myArr); -
使用
Stream.of():- 直接指定一系列元素来创建Stream。
Stream<String> stream3 = Stream.of("one", "two", "three"); -
使用
Stream.generate()- 生成无限Stream,需要一个
Supplier。通常需要与limit()结合使用。
Supplier<Double> randomValue = () -> Math.random(); Stream<Double> stream4 = Stream.generate(randomValue).limit(3); //限制生成三个,否则会无限生成 - 生成无限Stream,需要一个
-
使用
Stream.iterate():- 生成无线Stream,可以基于前一个元素进行计算。
Stream<Integer> stream5 = Stream.iterate(1, n -> n * 2).limit(3); //创建2的幂次流 stream5.forEach(System.out::println); -
基本类型Stream:
IntStream、LongStream、DoubleStream,用于避免基本类型的自动装箱,拆箱的性能开销。
IntStream.range(1, 5).forEach(System.out::println); // 输出 1, 2, 3, 4
实战示例
需求:从一个学生列表中,找出所有年龄大于20岁,并且名字以”J”开头的学生,然后将他们的名字转换为大写并收集到一个新的列表中。
Java 7 及之前(命令式编程):
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(声明式编程):
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关键字来定义一个静态方法。静态方法和类中的静态方法行为类似,它们属于接口本身,而不是实现该接口的任何对象。
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关键字修饰接口中的默认方法。
public interface MyInterface {
void abstractMethod();
default void defaultMethod(){
System.out.println("这是一个默认方法,有具体的实现。");
}
}
- 后向兼容:这是默认方法最核心的价值。当你在一个已发布的接口中添加新的默认方法时,所有已有的实现类不需要做任何修改就能兼容运行。它们会自动继承并使用这个默认实现。
- 子类可以选择性的重写:实现接口的类可以自由的选择重写还是使用默认方法。
默认方法的冲突规则
当一个类实现了多个接口,并且这些接口包含同名的默认方法时,Java8 有一套明确的规则来解决冲突。
-
类优先:如果实现类中有一个与接口默认方法同名的方法(无论是继承自父类还是自己定义),那么总是使用类中的实现。接口的默认方法会被忽略。
-
子接口优先:如果一个接口继承了另一个接口,并且它们都定义了同名的默认方法,那么子接口的默认方法会优先于父接口的默认方法。
-
显示解决:如果以上两条规则都无法解决冲突,那么编译器会报错,要求实现类碧玺明确的重写这个方法,并在重写的方法中选择调用哪个接口的默认实现。
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编程中,我们经常遇到这样的代码:
String name = getSomeName();
if (name != null) {
System.out.println(name.toUpperCase());
}
为了避免 name为 null时抛出异常,我们不得不进行繁琐的 null检查。当对象链很长时,这会变得更加麻烦和难以阅读,比如:user.getProfile().getAddress().getCity()。
// 传统方式的 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 提供了几种静态工厂方法来创建实例:
-
Optional.of(T value):- 用于创建一个包含非
null值的Optional对象。 - 如果传入
null会立即抛出异常NullPointerException。
Optional<String> optionalName = Optional.of("Alice"); - 用于创建一个包含非
-
Optional.ofNullable(T value):- 这是最常用的方法,用于创建一个可能包含
null值的Optional对象。 - 如果传入的是null,它会返回一个空的
Optional实例(Optional.empty())
- 这是最常用的方法,用于创建一个可能包含
-
Optional.empty():- 用于创建一个空的
Optional实例
Optional<String> optionalEmpty = Optional.empty(); - 用于创建一个空的
Optional的常用方法
Optional类提供了许多方法来安全的操作和获取值,而不需要显式的 if (value != null)检查。
-
isPresent()和isEmpty()isPresent():检查Optional对象是否包含值,返回booleanisEmpty():Java11新增,与isPresent()相反,检查Optional是否为空
Optional<String> optional = Optional.of("Hello"); if (optional.isPresent()) { System.out.println("值存在!"); } -
ifPresent(Consumer action):- 如果值存在,就执行
Consumer中的动作(Lambda表达式)。 - 这是处理值最常见的方式之一,优雅的取代了
if-not-null检查。
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)); // 不会输出任何内容 - 如果值存在,就执行
-
get():- 如果
Optional包含值,则返回该值;否则抛出NoSuchException。 - 不推荐直接使用
get(),应为它和传统的null检查一样危险违背了Optional设计的初衷。
- 如果
-
orElse(T other):- 如果
Optional包含值,则返回该值;否则返回传入的默认值other。
Optional<String> name = Optional.ofNullable(null); String result = name.orElse("默认值"); // result 将是 "默认值" - 如果
-
orElseGet(Supplier other):- 与
orElse类似,但它在Optional为空时,才会执行Supplier提供的代码块来生成默认值。 - 如果生成默认值的操作很耗时,
orElseGet()比orElse()更高效,因为orElse不管Optional是否为空,都会先执行other参数。
String result = name.orElseGet(() -> "从数据库获取的默认值"); // 只有在 name 为空时,才会执行 lambda - 与
-
orElseThrow()和orElseThrow(Supplier exceptionSupplier):- 如果
Optional为空,则抛出异常。 orElseThrow()(Java10新增)默认抛出NoSuchElementException- 你可以通过
orElseThrow(Supplier)指定要抛出的异常类型。
Optional<String> name = Optional.ofNullable(null); String result = name.orElseThrow(() -> new IllegalArgumentException("值不能为空")); - 如果
-
map(Function mapper):- 如果
Optional包含值,就对该值应用Function函数,然后返回一个包含新值的Optional。 - 这使得你可以链式地进行一系列操作。
Optional<String> name = Optional.of("alice"); Optional<Integer> length = name.map(String::length); // length 是 Optional<Integer>,值为 5 - 如果
-
faltMap(Function mapper):- 如果
Optional包含一个值,就将该值传递给一个函数,该函数必须返回一个Optional对象。flatMap不会再对结果进行额外包装,而是直接返回这个Optional。
- 如果
Optional的最佳实践
- 作为方法返回值,而不是参数。
- 不要用
get()。 - 不要将
Optional作为成员变量。 - 用
map或flatMap链式调用。
感觉使用
map()方法或许比较好。对象方法返回值可以不要求为Optional类型,由Map进行封装。维持了原函数不变。
HashMap升级
HashMap在JDK1.7到1.8有几个关键的升级。
数据结构
- JDK1.7:HashMap的底层数据结构是数组+链表。数组中的每一个元素都是一个链表,当多个键哈希到同一个位置时,它们就会以链表的形式存储在这个位置上。
- JDK1.8:
HashMap的底层数据结构是数组+链表+红黑树。当同一个哈希桶中的链表长度超过一个阈值(默认为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 时转为链表 |