Java-8新特性
函数的第一条规则是要短小,第二条规则是还要更短小。——《Clean Code》
Lambda表达式
Lambda表达式是Java 8最重要、最具革命性的特性之一,它让Java能够编写更简洁、更灵活的代码,尤其是在处理集合和并发时。
简单来说,Lambda表达式是一个匿名函数。它没有名称,可以作为参数传递给方法,也可以向普通对象一样被赋值给变量。它的主要目的是简化那些只包含一个抽象方法的接口(我们称之为函数式接口)的实现。
我们以多线程中的Runnable接口(只有一个 run()
方法)为例:
1 |
|
使用Lambda表达式,同样的逻辑可以这样写:
1 |
|
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 |
|
Java内置函数式接口
java 8在 java.util.function
包中提供了许多预定义的函数式接口,这些接口覆盖率常见的函数类型,方便我们在Stream API等地方使用。
Consumer<T>
:消费者。接收一个参数,没有返回值。void accept(T t);
示例:
1
2List<String> names = Arrays.asList("Alice","Bob");
names.forEach(name -> System.out.println(name));
Predicate<T>
:断言。接收一个参数,返回一个布尔值。boolean test(T t);
示例:
1
2List<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
2Function<String,Integer> stringLength = s -> s.length();
System.out.println(stringLength.apply("Hello"));
Supplier<T>
:供应商。不接受参数,返回一个T类型结果。T get();
示例:
1
2Supplier<Double> randomValue = () -> Math.random();
System.out.println(randomValue.get());
UnaryOperator<T>
:一元运算。接收一个T类型参数,返回一个T类型的结果(输入和输出类型相同)。T apply(T t);
继承自Function<T,T>
示例:
1
2UnaryOperator<Integer> square = n -> n * n;
System.out.println(square.apply(5));
BinaryOperator<T>
:二元运算。接收两个T类型参数,返回一个T类型结果T apply(T t1, T t2);
示例:
1
2BinaryOperator<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
循环来遍历集合:
1 |
|
循环的过程完全暴露在外部,forEach
方法提供了一种将迭代过程隐藏在函数内部,而由参数提供方法体的遍历方法:
1 |
|
原理
我们查看forEach的实现源码:
1 |
|
它接收一个Consumer 类型的参数。我们编写的Lambda表达式即为一个实现了 Consumer
接口 accept()
方法的匿名实现类。 forEach()
在内部遍历names的每一个元素,并将元素作为参数传递给这个Lambda表达式的 accept()
方法,从而执行我们定义的操作。
Map中的 forEach()
Map接口本身没有实现 Iterable
,但他有自己的 forEach()
方法,接收一个 BiConsumer
函数式接口(接收两个参数,无返回值),用于遍历键值对:
1 |
|
排序(sort)
1 |
|
线程(Runnable)
1 |
|
Lambda表达式的注意事项
变量捕获(Variable Capture)
Lambda表达式可以访问其定义范围内的局部变量,但这些变量必须是effectively final的(即它们在初始化后不会再被修改,即使没有明确声明 final
)。
Lambda表达式也不能修改其捕获的局部变量
1 |
|
方法引用
当Lambda表达式只是简单的调用一个现有方法时,可以使用方法引用来进一步简化代码,提高可读性。
1 |
|
接下来我们来看看什么时候可以这么做:
条件:
- 被引用的方法的参数列表和返回类型,与它所实现的(或兼容)的函数式接口的抽象方法的参数列表和返回类型完全匹配,就可以使用方法引用。
示例:Consumer.accept
方法的参数列表与 Syetem.out.println
方法的参数列表是兼容的。
1 |
|
所以在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)。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
2List<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
2list.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
2list.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
2list.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的创建方式
从集合创建:
Collection.stream()
:为任何集合(List
,Set
等)创建串行Sream。Collection.parallelStream()
:为任何集合创建并行Stream。
1
2
3List<String> myNames = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream1 = myNames.stream();
Stream<String> parallelStream = myNames.parallelStream();从数组创建:
Arrays.stream(T[] array
1
2String[] myArr = {"Apple", "Banana"};
Stream<String> stream2 = Arrays.stream(myArr);使用
Stream.of()
:- 直接指定一系列元素来创建Stream。
1
Stream<String> stream3 = Stream.of("one", "two", "three");
使用
Stream.generate()
- 生成无限Stream,需要一个
Supplier
。通常需要与limit()
结合使用。
1
2Supplier<Double> randomValue = () -> Math.random();
Stream<Double> stream4 = Stream.generate(randomValue).limit(3); //限制生成三个,否则会无限生成- 生成无限Stream,需要一个
使用
Stream.iterate()
:- 生成无线Stream,可以基于前一个元素进行计算。
1
2Stream<Integer> stream5 = Stream.iterate(1, n -> n * 2).limit(3); //创建2的幂次流
stream5.forEach(System.out::println);基本类型Stream:
IntStream
、LongStream
、DoubleStream
,用于避免基本类型的自动装箱,拆箱的性能开销。
1
IntStream.range(1, 5).forEach(System.out::println); // 输出 1, 2, 3, 4
实战示例
需求:从一个学生列表中,找出所有年龄大于20岁,并且名字以”J”开头的学生,然后将他们的名字转换为大写并收集到一个新的列表中。
Java 7 及之前(命令式编程):
1 |
|
Java 8 Stream API(声明式编程):
1 |
|
Stream API编程的优点:
- 声明式:你告诉Stream你想要什么结果(过滤、映射、收集),而不是如何一步步实现这个结果。
- 易于并行化:只需要将student.stream()改为student.parallelStream(),就可以将整个操作链并行执行,而无需手动编写复杂的线程代码。
接口的增强
在Java8之前,接口中只能有抽象方法和常量。Java8引入了两种可以在接口中带有实现的方法:默认方法(Default Methods)和静态方法(Static Methods)。
静态方法
- 定义方式:在接口中,你可以使用static关键字来定义一个静态方法。静态方法和类中的静态方法行为类似,它们属于接口本身,而不是实现该接口的任何对象。
1 |
|
- 调用方法:
- 类方法可以通过类名、子类名或者对象名来调用静态类方法。
- 接口的静态方法只能通过接口名来调用。
默认方法
默认方法,在Java8中也被称为”虚拟扩展方法”(virtual extension methods),是接口的一项重要增强。在Java8之前,如果你想要向一个已有的接口中添加新方法,所有实现了这个接口的类都必须同时修改,否则代码将无法编译通过。
默认方法的引入正是为了”接口演进”的问题。
- 定义方式:使用
default
关键字修饰接口中的默认方法。
1 |
|
- 后向兼容:这是默认方法最核心的价值。当你在一个已发布的接口中添加新的默认方法时,所有已有的实现类不需要做任何修改就能兼容运行。它们会自动继承并使用这个默认实现。
- 子类可以选择性的重写:实现接口的类可以自由的选择重写还是使用默认方法。
默认方法的冲突规则
当一个类实现了多个接口,并且这些接口包含同名的默认方法时,Java8 有一套明确的规则来解决冲突。
类优先:如果实现类中有一个与接口默认方法同名的方法(无论是继承自父类还是自己定义),那么总是使用类中的实现。接口的默认方法会被忽略。
子接口优先:如果一个接口继承了另一个接口,并且它们都定义了同名的默认方法,那么子接口的默认方法会优先于父接口的默认方法。
显示解决:如果以上两条规则都无法解决冲突,那么编译器会报错,要求实现类碧玺明确的重写这个方法,并在重写的方法中选择调用哪个接口的默认实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15interface 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 |
|
为了避免 name
为 null
时抛出异常,我们不得不进行繁琐的 null
检查。当对象链很长时,这会变得更加麻烦和难以阅读,比如:user.getProfile().getAddress().getCity()
。
1 |
|
如何创建Optional对象
Optional
提供了几种静态工厂方法来创建实例:
Optional.of(T value)
:- 用于创建一个包含非
null
值的Optional
对象。 - 如果传入
null
会立即抛出异常NullPointerException
。
1
Optional<String> optionalName = Optional.of("Alice");
- 用于创建一个包含非
Optional.ofNullable(T value)
:- 这是最常用的方法,用于创建一个可能包含
null
值的Optional
对象。 - 如果传入的是null,它会返回一个空的
Optional
实例(Optional.empty()
)
- 这是最常用的方法,用于创建一个可能包含
Optional.empty()
:- 用于创建一个空的
Optional
实例
1
Optional<String> optionalEmpty = Optional.empty();
- 用于创建一个空的
Optional的常用方法
Optional
类提供了许多方法来安全的操作和获取值,而不需要显式的 if (value != null)
检查。
isPresent()
和isEmpty()
isPresent()
:检查Optional
对象是否包含值,返回boolean
isEmpty()
:Java11新增,与isPresent()
相反,检查Optional
是否为空
1
2
3
4Optional<String> optional = Optional.of("Hello");
if (optional.isPresent()) {
System.out.println("值存在!");
}ifPresent(Consumer action)
:- 如果值存在,就执行
Consumer
中的动作(Lambda表达式)。 - 这是处理值最常见的方式之一,优雅的取代了
if-not-null
检查。
1
2
3
4
5Optional<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
。
1
2Optional<String> name = Optional.ofNullable(null);
String result = name.orElse("默认值"); // result 将是 "默认值"- 如果
orElseGet(Supplier other)
:- 与
orElse
类似,但它在Optional
为空时,才会执行Supplier
提供的代码块来生成默认值。 - 如果生成默认值的操作很耗时,
orElseGet()
比orElse()
更高效,因为orElse
不管Optional
是否为空,都会先执行other
参数。
1
String result = name.orElseGet(() -> "从数据库获取的默认值"); // 只有在 name 为空时,才会执行 lambda
- 与
orElseThrow()
和orElseThrow(Supplier exceptionSupplier)
:- 如果
Optional
为空,则抛出异常。 orElseThrow()
(Java10新增)默认抛出NoSuchElementException
- 你可以通过
orElseThrow(Supplier)
指定要抛出的异常类型。
1
2Optional<String> name = Optional.ofNullable(null);
String result = name.orElseThrow(() -> new IllegalArgumentException("值不能为空"));- 如果
map(Function mapper)
:- 如果
Optional
包含值,就对该值应用Function
函数,然后返回一个包含新值的Optional
。 - 这使得你可以链式地进行一系列操作。
1
2Optional<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 时转为链表 |