Java 8 的新特性
1. Lambda表达式和函数式接口
参考链接:https://openjdk.org/jeps/126
Lambda 表达式是一个匿名函数(指的是没有函数名的函数),直接对应于其中的 Lambda 抽象,Lambda 表达式可以表示闭包。我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码作为实参),也可以理解为函数式编程,将一个函数作为参数进行传递。Lambda 表达式允许把函数作为一个方法的参数,Lambda 表达式的基本语法如下:
(parameters) -> expression 或 (parameters) -> {statements;}
例子:
// 使用 Lambda 表达式计算两个数的和
MathOperation addition = (a, b) -> a + b;
// 调用 Lambda 表达式
int result = addition.operation(5, 3);
System.out.println("5 + 3 = " + result);
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 使用 Lambda 表达式遍历列表
names.forEach(name -> System.out.println(name));
// 传统的匿名内部类
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello World!");
}
};
// 使用 Lambda 表达式
Runnable runnable2 = () -> System.out.println("Hello World!");
Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数式接口是一种特殊类型的接口,它仅包含一个抽象方法(可隐式地转换为Lambda表达式)。java.lang.Runnable
和java.util.concurrent.Callable
是这类接口的典型示例。
然而,函数式接口的脆弱性在于其定义的严格性:一旦接口中增加了额外的方法,它就会失去函数式接口的特性,并可能导致编译错误。为了增强代码的健壮性和明确性,Java 8 引入了@FunctionalInterface
注解,专门用于标记那些设计为函数式接口的接口。这个注解不仅帮助开发者快速识别函数式接口,还能在接口不再符合函数式接口定义时提供编译时检查。
下面是一个简单的函数式接口定义示例,使用了@FunctionalInterface
注解:
@FunctionalInterface
interface SimpleFunction {
void performAction();
}
这样,任何尝试向SimpleFunction
接口中添加额外方法的尝试都会立即被编译器捕获,从而防止了因违反函数式接口规则而导致的潜在问题。
2. 接口默认方法和静态方法
参考链接:https://openjdk.org/jeps/126
Java 8 新增了接口的默认方法和静态方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。我们只需在方法名前面加个 default 关键字即可实现默认方法。
默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要实现,接口提供的默认方法会被接口的实现类继承或者重写(非必须)。
// 默认方法
public interface Singleton{
default String getName(){
return "默认方法";
}
}
public static class SingletonImpl implements Singleton{
// 可以重写getName方法
}
// 静态方法
public interface SingletonFactory{
// Interfaces now allow static methods
static Singleton getInstance(Supplier<Singleton> supplier) {
return supplier.get();
}
}
// 使用例子
public static void main( String[] args ) {
Singleton singleton = SingletonFactory.getInstance( SingletonImpl::new );
System.out.println( singleton.getName() );
}
3. 方法引用
参考链接:https://openjdk.org/jeps/160
方法引用通过方法的名字来指向一个方法。方法引用可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 :: 。
下面,我们在 Car 类中定义了 4 个方法作为例子来区分 Java 中 4 种不同方法的引用。
@FunctionalInterface
public interface Supplier<T> {
T get();
}
public class Car {
//Supplier是jdk1.8的接口,这里和lamda一起使用了
public static Car create(final Supplier<Car> supplier) {
return supplier.get();
}
public static void collide(final Car car) {
System.out.println("Collided " + car.toString());
}
public void follow(final Car another) {
System.out.println("Following the " + another.toString());
}
public void repair() {
System.out.println("Repaired " + this.toString());
}
}
// 1.构造器引用:它的语法是Class::new,或者更一般的Class< T >::new
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
// 2.静态方法引用:它的语法是Class::static_method
cars.forEach( Car::collide );
// 3.特定类的任意对象的方法引用:它的语法是Class::method,注意,这个方法没有定义入参
cars.forEach( Car::repair );
// 特定对象的方法引用:它的语法是instance::method
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
4. 重复注解
参考链接:https://openjdk.org/jeps/120
自从Java 5版本引入了注解功能后,这一特性迅速流行开来,广泛应用于各种编程框架和项目之中。然而,早期注解存在一个显著限制:相同位置不能重复使用同一注解。为了突破这一限制,Java 8引入了重复注解的概念,允许在相同位置多次应用同一注解,极大地增强了注解的灵活性和表达能力。
在Java 8中,通过@Repeatable
注解来定义可重复的注解,这并非直接对Java语言层面的修改,而是编译器采用的一种技术手段,其底层实现机制保持不变。以下是一个简单的示例来说明如何使用@Repeatable
注解:
// 首先,定义一个容器注解来存储多次使用的注解实例
@interface MyAnnotations {
MyAnnotation[] value();
}
// 使用@Repeatable注解标记MyAnnotation为可重复注解,并指定其容器注解为MyAnnotations
@Repeatable(MyAnnotations.class)
@interface MyAnnotation {
String value();
}
// 现在可以在同一个地方多次使用MyAnnotation注解了
@MyAnnotation(value = "Hello")
@MyAnnotation(value = "World")
public class MyClass {
// 类体
}
// 编译器会将上述用法转换成使用容器注解的形式,但代码编写时无需关心这一转换
上述代码展示了如何使用@Repeatable
注解定义一个可重复的注解MyAnnotation
,并通过容器注解MyAnnotations
来存储这些重复的注解实例。这样,开发者就可以在同一个位置多次使用MyAnnotation
注解,而无需担心之前的限制。需要注意的是,虽然@Repeatable
提供了这种便利,但其背后的实现细节(如编译器如何转换代码)对大多数开发者来说是透明的。
5. 拓宽了注解的应用场景
参考链接:https://openjdk.org/jeps/104
Java 8显著扩展了注解的应用范围,使得注解现在几乎可以应用于编程语言的各个元素之上。无论是局部变量、接口类型、超类声明、接口实现类,还是函数的异常定义,注解都能发挥其作用。以下是一些具体的应用示例,展示了这种广泛的适用性:
局部变量:注解可以直接附加在局部变量上,为局部变量提供额外的元数据信息。
接口类型:在接口定义上使用注解,可以为接口添加特定的说明或配置信息。
超类(父类):注解也可以应用于类的继承声明中,即可以标记一个类是如何扩展自另一个类的。
接口实现类:在实现接口时,注解可以用来标记实现类或与实现相关的特定配置。
函数异常定义:在方法签名中定义异常时,也可以使用注解来提供关于这些异常的额外信息或处理指示。
这些例子展示了Java 8中注解功能的强大和灵活性,使得开发者能够以更加丰富的方式利用注解来增强代码的可读性、可维护性和可扩展性。
6. 在运行时获得Java程序中方法的参数名称
参考链接:https://openjdk.org/jeps/118
Java 8之前,为了在运行时获取Java方法中参数的名称,经验丰富的Java开发者不得不依赖像Paranamer这样的第三方库。不过,Java 8正式将这一功能标准化,从两个层面提供了支持:在语言层面,通过反射API新增的Parameter.getName()方法,可以直接访问方法参数的名称;在字节码层面,新的javac编译器增加了-parameters选项,以保留并生成包含参数名信息的字节码。这样,开发者就能更方便地在运行时获取到方法参数的名称了。
例如:
package com.javacodegeeks.java8.parameter.names;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class ParameterNames {
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod("main", String[].class );
for( final Parameter parameter: method.getParameters() ) {
System.out.println( "Parameter: " + parameter.getName() );
}
}
}
在Java 8中这个特性是默认关闭的,因此如果不带 -parameters 参数编译上述代码并运行,则会输出:Parameter: arg0
如果带 -parameters 参数,则会输出(正确的结果):Parameter: args
7. Stream API
Java 8 引入了流(Stream)这一新概念,它提供了一种声明式的方式来处理数据集合。流API的设计灵感源自SQL查询,为Java集合操作提供了高级抽象,让数据处理更加直观。
通过使用流,Java程序员能够显著提升开发效率,编写出既高效又简洁的代码。在流的处理过程中,数据元素被视作一个流动的序列,在管道内传输,并在各个节点上经历如筛选、排序、聚合等中间操作。最终,这些操作通过终端操作汇总成处理结果。
简而言之,流API让Java集合的操作变得像编写SQL查询一样直观,从而实现了代码的优雅与效率的双重提升。流主本身并不存储元素,流要是用来计算的。
+--------------------+ +------+ +------+ +---+ +-------+
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
+--------------------+ +------+ +------+ +---+ +-------+
以上的流程转换为 Java 代码为:
List<Integer> transactionsIds =
widgets.stream()
.filter(b -> b.getColor() == RED)
.sorted((x,y) -> x.getWeight() - y.getWeight())
.mapToInt(Widget::getWeight)
.sum();
Java 8 提供了stream
和parallelStream
两种流:
stream() − 为集合创建串行流
parallelStream() − 为集合创建并行流
parallelStream与Stream的区别在于parallelStream开启了多线程的处理方式,parallelStream采用了Fork/Join线程池,在我的另一篇文章里详细讲了fork/join线程池的原理和使用
用例:
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// filter
strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
// forEach
strings.stream().forEach(System.out::println);
// limit
strings.stream().limit(10).forEach(System.out::println);
// sorted
strings.stream().sorted().forEach(System.out::println);
// map
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 获取对应的平方数
numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
// Collectors
strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
// 统计
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : " + stats.getMax());
System.out.println("列表中最小的数 : " + stats.getMin());
System.out.println("所有数之和 : " + stats.getSum());
System.out.println("平均数 : " + stats.getAverage());
// 并行(parallel)程序
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();
其中Collectors 是一个非常有用的工具类,它提供了各种方法来将 Stream 中的元素收集到一个集合中,或者进行其他类型的聚合操作。以下是一些常见的 Collectors 方法及其用法:
// 收集到列表(List)
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
// 收集到集合(Set)
Set<String> set = Stream.of("a", "b", "c").collect(Collectors.toSet());
// 收集到特定类型的集合
ArrayList<String> arrayList = Stream.of("a", "b", "c").collect(Collectors.toCollection(ArrayList::new));
// 收集到映射(Map)
Map<String, Integer> map = Stream.of("a", "b", "c").collect(Collectors.toMap(s -> s, s -> s.length()));
// 分组(Grouping)
Map<Integer, List<String>> groupedByLength = Stream.of("a", "bb", "ccc").collect(Collectors.groupingBy(String::length));
// 分区(Partitioning)
Map<Boolean, List<String>> partitionedByLength = Stream.of("a", "bb", "ccc").collect(Collectors.partitioningBy(s -> s.length() > 1));
// 连接字符串(Joining)
String joined = Stream.of("a", "b", "c").collect(Collectors.joining(", "));
// 计算总和(Summing)
int sum = Stream.of(1, 2, 3).collect(Collectors.summingInt(Integer::intValue));
// 计算平均值(Averaging)
double average = Stream.of(1, 2, 3).collect(Collectors.averagingInt(Integer::intValue));
// 计算统计信息(Summarizing)
IntSummaryStatistics stats = Stream.of(1, 2, 3).collect(Collectors.summarizingInt(Integer::intValue));
8. Optional
参考链接:https://docs.oracle.com/javase/8/docs/api/
Java应用中,空值异常(NullPointerException)是一个频繁出现的问题。为了解决这一问题,避免代码中遍布繁琐的null检查,从而在编写时保持代码的整洁性,Google Guava在Java 8之前率先引入了Optional类。这个类允许开发者以更优雅的方式处理可能为null的值。
随后,Java 8官方也采纳了这一思想,将Optional作为标准库的一部分,使得所有Java开发者都能利用这一特性。Optional的核心功能是提供一种容器,用于安全地持有或不持有某个类型的值(T),而无需直接使用null。它提供了一系列方法,帮助开发者在不进行显式null检查的情况下,安全地访问和操作这些值。例如,通过使用Optional,开发者可以更加清晰地表达意图,减少空指针异常的风险。
例如:
Optional<String> fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
Optional.of(T t) : 创建一个 Optional 实例
Optional.empty() : 创建一个空的 Optional 实例
Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
isPresent() : 判断是否包含值
orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
flatMap(Function mapper):与 map 类似,要求返回值必须是Optional
9. 新时间日期API
参考链接:https://jcp.org/en/jsr/detail?id=310
在旧版Java中,日期时间处理API饱受诟病,主要问题包括:
线程安全性缺失:
java.util.Date
及其相关日期类均设计为可变类型,不具备线程安全特性,这成为了Java日期处理中的一个显著缺陷。设计不一致与冗余:Java的日期时间类设计显得杂乱无章,分散在
java.util
、java.sql
以及java.text
等多个包中,缺乏统一性和一致性。尤为突出的是,java.util.Date
与java.sql.Date
虽然名称相同,但功能各异(前者包含日期和时间,后者仅含日期),且将日期类置于java.sql
包中显得不合逻辑,这样的设计无疑增加了使用的复杂性和混淆。时区与国际化支持不足:原始的日期类缺乏对时区处理的直接支持,也未充分考虑国际化需求。尽管后来引入了
java.util.Calendar
和java.util.TimeZone
来试图弥补这一不足,但这些类同样继承了旧有API的诸多问题,未能从根本上解决问题。
Java 8通过发布新的Date-Time API (JSR 310)来解决以上日期与时间的问题,新的Date-Time API具有:不可变性(线程安全)、概念清晰便于理解、更好的时区支持、强大的时间间隔和持续时间处理能力、灵活的日期时间格式化与解析、更好的性能和兼容性等优势。
例子:
//当前时间
LocalDateTime now = LocalDateTime.now();
// 设置时间
LocalDateTime of = LocalDateTime.of(2020, 2, 01, 5, 10, 53);
// 计算:加/减两年
LocalDateTime of2 = of.plusYears(2);
LocalDateTime of3 = of.minusYears(2);
// 获取年月日等值
of.getYear() | of.getMonthValue() | of.getDayOfMonth()
//获取秒数时间戳(10位)|(13位)
now.toEpochSecond(ZoneOffset.of("+8")) | now.toEpochSecond(ZoneOffset.of("+8")).toEpochMilli()
// 时间间隔
Instant i1 = Instant.now();
TimeUnit.SECONDS.sleep(1);
Instant i2 = Instant.now();
long l = Duration.between(i1, i2).toMillis();
System.out.println(l);
// 时间格式化
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format = dtf.format(now);
// Date 转为 LocalDateTime
Date date = new Date();
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
...
10. Base64
参考链接:https://www.javacodegeeks.com/2014/04/base64-in-java-8-its-not-too-late-to-join-in-the-fun.html
对Base64编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行Base64编码。Base64工具类提供了一套静态方法获取下面三种BASE64编解码器:
基本:输出被映射到一组字符A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持A-Za-z0-9+/。
URL:输出映射到一组字符A-Za-z0-9+_,输出是URL和文件。
MIME:输出映射到MIME友好格式。输出每行不超过76字符,并且使用'\r'并跟随'\n'作为分割。编码输出最后没有行分割。
例如:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Base64Example {
public static void main(String[] args) {
// Original text to be encoded
final String text = "Base64 finally in Java 8!";
// Encoding the text to Base64
final String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
System.out.println("Encoded: " + encoded);
// Decoding the Base64 encoded string back to the original text
final String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
System.out.println("Decoded: " + decoded);
}
}
ps:什么是MIME编码格式,我的另一篇文章有详细介绍
11. 并行数组
参考链接:https://openjdk.org/jeps/103
JDK1.8增加了对数组并行处理的方法(parallelXxx),使用JSR 166 Fork/Join并行公共池提供并行数组排序的数组。
Arrays.parallelSort 是 Java 8 引入的一个方法,用于对数组进行并行排序。这个方法直接作用于数组,而不需要先将其转换为流(Stream)。它利用了多核处理器的能力来加速排序过程,但请注意,并行排序并不总是比传统的顺序排序更快,特别是在数据量较小或数组已经接近排序完成的情况下。
例如:
// 定义一个未排序的整数数组
int[] numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 打印原始数组
System.out.println("原始数组: " + Arrays.toString(numbers));
// 使用Arrays.parallelSort方法对数组进行并行排序
Arrays.parallelSort(numbers);
// 打印排序后的数组
System.out.println("排序后的数组: " + Arrays.toString(numbers));
12. JVM 变化
使用 Metaspace (JEP 122)代替持久代(PermGen space)。
在JVM参数方面,使用 -XX:MetaSpaceSize和 -XX:MaxMetaspaceSize 代替原来的 -XX:PermSize 和 -XX:MaxPermSize
评论区