Java8到Java17之间的主要特性描述

[TOC]

Java8

主要特性:

  1. lambda表达式与Stream API(Lambda Expression and Stream API)
  2. 方法引用(Method Reference)
  3. 接口默认方法(Default Methods)
  4. 类型注解(Type Annotations)
  5. 可重复注解(Repeating Annotations)
  6. 方法参数反射(Method Parameter Reflection)

lambda表达式与Stream API

一般集合数据的处理示例:

1
2
3
4
5
6
7
8
9
public List<Car> findCheapCar(List<Car> cars) {
List<Car> result = new ArrayList<>();
for (Car car : cars) {
if (car.getPrice() < 50000) {
result.add(car);
}
}
return result;
}

lambda表达式与Stream API搭配的示例:

1
2
3
4
public List<Car> findCheapCar(List<Car> cars) {
return cars.stream().filter(car -> car.getPrice() < 50000)
.collect(Collectors.toList());
}

通过调用stream()方法将集合类型的cars变量转换为stream,内部通过filter()方法设定对应的过滤条件,最后通过colletct()方法将最后的结果集包装为List返回。

方法引用(Method Reference)

一般lambda中引用方法示例:

1
2
3
4
public List<String> mapCarName(List<Car> cars) {
return cars.stream().map(car -> car.getName())
.collect(Collectors.toList());
}

方法引用使得代码更加简洁并且可读性更好,使用两个冒号::来引用方法:

1
2
3
4
public List<String> mapCarName(List<Car> cars) {
return cars.stream().map(Car::toString)
.collect(Collectors.toList());
}

接口默认方法(Default Methods)

当一个接口新增了方法,又不希望调用方端有任何感知。这时候,就可以使用接口默认方法(Default Methods)

来实现这个功能。

1
2
3
4
5
6
7
8
9
10
public interface IElectricCar {
void charge();
}

public class BYD implements IElectricCar {
@Override
public void charge() {
System.out.println("比亚迪开始充电了");
}
}

添加一个新方法到接口中:

1
2
3
4
public interface IElectricCar {
void charge();
void charge(Date date);
}

这时候IElectricCar接口所有实现类都会出现编译报错:

1
2
Class 'BYD' must either be declared abstract 
or implement abstract method 'charge(Date)' in 'IElectricCar'

接口默认方法可以使用关键词default在接口中默认实现方法解决这个问题:

1
2
3
4
5
6
public interface IElectricCar {
void charge();
default void charge(Date date) {
System.out.println(date.toString() + ":比亚迪开始充电了");
}
}

类型注解(Type Annotations)

Field.getAnnotations()是用来获取Target为ElementType.FIELD的注解;

Field.getAnnotatedType().getAnnotations()用来获取Target为ElementType.TYPE_USE的注解,即类型注解。

类型注解可以在使用类型的任意地方使用它们,使用场景包括但不限于:

  • 本地变量定义
1
2
3
public static void main(String[] args) {
@NotNull String userName = args[0];
}
  • 构造器调用
1
2
3
4
public static void main(String[] args) {
List<String> request = new @NotEmpty ArrayList<>(Arrays.stream(args)
.collect(Collectors.toList()));
}
  • 泛型
1
2
3
public static void main(String[] args) {
List<@Email String> emails;
}

可重复注解(Repeating Annotations)

创建可重复注解

1
2
3
4
5
6
7
8
@Repeatable(Notifications.class)
public @interface Notify {
String phone();
}

public @interface Notifications {
Notify[] value();
}

在普通注解@Notify上增加了@Repeatable元注解,说明将@Notify注解当做@Notifications注解的对象数组元素来使用。

使用可重复注解

1
2
3
4
5
6
7
8
@Notify(phone = "15880000000")
@Notify(phone = "18900000000")
public class expressTimeoutException extends RuntimeException {
final String username;
public expressTimeoutException(String username) {
this.username = username;
}
}

Java9

主要特性:

  1. Java模块化(Java Module System)
  2. 带有内部匿名类的菱形语法(Diamond Syntax with Inner Anonymous Classes)
  3. 私有接口方法(Private Interface Methods)

Java模块化(Java Module System)

模块化是一组包(package)以及包的依赖与资源,相对于包而言,它提供了更加广泛的功能。

模块的是通过module-info.java进行定义,编译后打包后,就成为一个模块的实体;在模块的定义文件中,我

们需要指定模块之间的依赖靠关系,可以exports给那些模块用,需要使用那些模块requires

模块声明文件通常放置在模块源码的根目录:

1
2
3
4
5
6
7
8
9
10
# 声明
module com.foo.bar {
requires org.baz.qux;
exports com.foo.bar.alpha;
exports com.foo.bar.beta;
}
# 位置
module-info.java
com/foo/bar/alpha/AlphaFactory.java
com/foo/bar/alpha/Alpha.java

模块之间的关系被称作可读性(readability),代表一个模块是否可以找到这个模块文件,并且读入系统中(注

意:并非代表可以访问其中的类型)。在实际的代码,一个类型对于另外一个类型的调用,我们称之为可访问性

(Accessible),这意味着可以使用这个类型;可访问性的前提是可读性,换句话说,现有模块可读,然后再进

一步检测可访问性(安全)。在Java9中,Public不再意味着任意的可访问性。

transitive可以可以提供一个间接可读性:

间接引用

带有内部匿名类的菱形语法(Diamond Syntax with Inner Anonymous Classes)

在Java9之前不能在匿名内部类中使用<>操作符,如下面代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
public static abstract class StringAppender<T> {
public abstract T append(String a, String b);
}

public static void main(String[] args) {
StringAppender<String> appending = new StringAppender<>() {
@Override
public String append(String a, String b) {
return new StringBuilder(a).append("-").append(b).toString();
}
};
}

在Java8中,会出现编译错误:

1
2
'<>' cannot be used with anonymous classes
#无法推断StringAppender<T>的泛型,必须指定<String>

将环境切换为Java9能正确编译通过。

私有接口方法(Private Interface Methods)

在Java9中可以在接口中定义私有方法,私有方法的主要目的在于通用方法的封装以提升其在别的默认方法中的可复用性。

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
public static void main(String[] args) {
TestingNames names = new TestingNames();
System.out.println(names.fetchInitialData());
}

public static class TestingNames implements NamesInterface {
public TestingNames() {
}
}

public interface NamesInterface {
default List<String> fetchInitialData() {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(this.getClass().getResourceAsStream("/names.txt")))) {
return readNames(br);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

private List<String> readNames(BufferedReader br) throws IOException {
ArrayList<String> names = new ArrayList<>();
String name = null;
while ((name = br.readLine()) != null) {
names.add(name);
}
return names;
}
}

Java10

局部变量类型推断(Local Variable Type Inference)

Java需要在局部变量中显式地定义类型,代码显得非常冗余。Java10中的var类型允许省略语句左侧的类型声明,使得语句干练简洁。

Java10前的旧写法:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Person Roland = new Person("Roland", "Deschain");
Person Susan = new Person("Susan", "Delgado");
Person Eddie = new Person("Eddie", "Dean");
Person Detta = new Person("Detta", "Walker");
Person Jake = new Person("Jake", "Chambers");

List<Person> persons = List.of(Roland, Susan, Eddie, Detta, Jake);
for (Person person : persons) {
System.out.println(person.name + " - " + person.lastname);
}
}

使用var后的写法:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
var Roland = new Person("Roland", "Deschain");
var Susan = new Person("Susan", "Delgado");
var Eddie = new Person("Eddie", "Dean");
var Detta = new Person("Detta", "Walker");
var Jake = new Person("Jake", "Chambers");

var persons = List.of(Roland, Susan, Eddie, Detta, Jake);
for (var person : persons) {
System.out.println(person.name + " - " + person.lastname);
}
}

Java11

Lambda表达式中的局部变量类型推断(Local Variable Type in Lambda Expressions)

在Java11中针对Java10中的局部变量类型推断这个特性进行了改善,可以在lambda表达式中使用var进行类型推断而不是进行明确的类型声明。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
var Roland = new Person("Roland", "Deschain");
var Susan = new Person("Susan", "Delgado");
var Eddie = new Person("Eddie", "Dean");
var Detta = new Person("Detta", "Walker");
var Jake = new Person("Jake", "Chambers");

var filteredPersons = List.of(Roland, Susan, Eddie, Detta, Jake).stream()
.filter((var x) -> x.name.contains("a"))
.collect(Collectors.toList());
System.out.println(filteredPersons);
}

Java14

Switch表达式(Switch Expressions)

Java14前的旧写法:

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
public static void main(String[] args) {
int days = 0;
Month month = Month.APRIL;

switch (month) {
case JANUARY:
case MARCH:
case MAY:
case JULY:
case AUGUST:
case OCTOBER:
case DECEMBER:
days = 31;
break;
case FEBRUARY :
days = 28;
break;
case APRIL:
case JUNE:
case SEPTEMBER:
case NOVEMBER:
days = 30;
break;
default:
throw new IllegalStateException();
}
}

Java14的新写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
int days = 0;
Month month = Month.APRIL;

days = switch (month) {
case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER :
days = 31;
break;
case FEBRUARY :
days = 28;
break;
case APRIL, JUNE, SEPTEMBER, NOVEMBER :
days = 30;
break;
default:
throw new IllegalStateException();
}
}

使用箭头标识符(->)改造后的写法:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int days = 0;
Month month = Month.APRIL;

days = switch (month) {
case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;
case FEBRUARY -> 28;
case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
default -> throw new IllegalStateException();
};
}

在case块中,java14使用了箭头标识符(->)替代冒号(:),即使没有break关键字,也会在匹配到的第一个case分支后跳出switch语句。

yield关键字配合箭头标识符(->)可以处理更复杂逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
int days = 0;
Month month = Month.APRIL;

days = switch (month) {
case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> {
System.out.println(month);
yield 31;
}
case FEBRUARY -> {
System.out.println(month);
yield 28;
}
case APRIL, JUNE, SEPTEMBER, NOVEMBER -> {
System.out.println(month);
yield 30;
}
default -> throw new IllegalStateException();
};
}

Java15

文本块(Text Blocks)

文本块可以理解为对格式化字符串的一个优化操作,从Java15允许编写一个跨越多行的字符串作为常规文本。相

比于在Java15之前,跨多行需要使用”+”号进行多行字符串拼接,会更加的方便并且可读性更高。

Java15之前,需要人为的保证字符串的格式化操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
System.out.println(
"<!DOCTYPE html>\n" +
"<html>\n" +
" <head>\n" +
" <title>Example</title>\n" +
" </head>\n" +
" <body>\n" +
" <p>This is an example of a simple HTML " +
" page with one paragraph.</p>\n" +
" </body>\n" +
"</html>\n");
}

Java15使用Text Blocks之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
System.out.println(
"""
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<p>This is an example of a simple HTML
page with one paragraph.</p>
</body>
</html>
"""
);
}

Java16

instanceof 模式匹配(Pattern Matching of instanceof)

instanceof模式匹配允许我们以内联形式转换变量,并在所需的if-else块中使用它而无需进行强转。

需要强转的旧写法:

1
2
3
4
5
6
7
8
9
public static double getPrice(Car car) {
if (car instanceof BYD) {
BYD byd = (BYD) car;
return byd.getPrice();
} else if (car instanceof Tesla) {
Tesla tesla = (Tesla) car;
return tesla.getPrice();
} else throw new IllegalArgumentException();
}

使用模式匹配写法:

1
2
3
4
5
6
7
public static double getPrice(Car car) {
if (car instanceof BYD byd) {
return byd.getPrice();
} else if (car instanceof Tesla tesla) {
return tesla.getPrice();
} else throw new IllegalArgumentException();
}

需要注意的是,对应的变量范围,只在当前所属的if-else代码块中可见。

档案类(Records)

档案类是用来表示不可变数据的透明载体,用来简化不可变数据的表达,提高编码效率,降低编码错误。

  • 档案类是个终极(final)类,其父类是父类是java.lang.Record。不支持继承,不能用任何其他类来继承它。没有子类,也就意味着我们不能通过修改子类来改变档案类;
  • 档案类允许实现接口;
  • 档案类声明的变量是不可变的变量;
  • 档案类不能声明可变的变量,也不能支持实例初始化的方法。这就保证了,我们只能使用档案类形式的构造方法,避免额外的初始化对可变性的影响;
  • 档案类不能声明本地(native)方法。如果允许了本地方法,也就意味着打开了修改不可变变量的后门。

老式的POJO定义:

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
public class Car {
private String name;
private double price;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public Car(String name, String price) {
this.name = name;
this.price = price;
}

@Override
public boolean equals(Object o) {}

@Override
public int hashCode() {}

@Override
public String toString() {}
}

在类名前使用关键词record定义为档案类,一行解决:

1
public record CarRecord(String name, double price) {}

Java17

封闭类(Sealed Classes)

类的final修饰符能禁止任何类继承它,Java17封闭类带来的全新特性,封闭类允许针对permit指定的类进行继承

扩展,除此之外,封闭类对于其它类而言,可以理解为是带有final修饰符的类。

1
public sealed class Car permits BYD, Tesla {}

permits关键字指定的子类必须要增加如下几种修饰符中的一种:

  • final:表示该类不能被继承
  • sealed:修饰类/接口,用来描述这个类/接口为密封类/接口
  • non-sealed:修饰类/接口,用来描述这个类/接口为非密封类/接口,显式放弃密封
  • permits:用在extends和implements之后,指定可以继承或实现的类

封闭类会带来相应的条件约束,包括:

  • Permitted 子类在编译期必须能够被封闭类访问
  • Permitted 子类必须直接继承封闭类
  • Permitted 子类必须有如下修饰符中的一个进行修饰:final,sealed,non-sealed
  • Permitted 子类必须在同一个java模块中
1
public final class BYD extends Car {}