跳转至

Advanced Language Feature

前置知识

  • 基本 Java 语法。
关于JEP

目前,Java 的新特性、功能均需要先提出并起草 JEP(JDK Enhancement Proposals)草案。在 OpenJDK 社区充分审核、修改、投票后成为正式的、带编号 JEP 提案。在开发 JEP 对应的功能分支,通过所有平台测试、代码审查后,择期并入下一个发行版本的主分支。本章提到的 JEP* 都标注了并入的 Java 版本和提案原文连接,便于同学们学习参考。

Switch Expressions [JEP361, Java14]

Switch Expressions [JEP361, Java14] 与传统的switch不同,使用->连接case子句和具体代码块,并且case子句可以一次匹配多个,代码块也不需要break来防止连续执行。

1
2
3
4
5
6
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

switch表达式,顾名思义可以产生表达式的值:

1
2
3
4
5
6
7
8
9
static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one";
            case  2 -> "two";
            default -> "many";
        }
    );
}

如果需要复杂代码块,使用{}包围,并使用yield关键字产生case子句对应的值:

1
2
3
4
5
6
7
8
9
int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

Pattern Matching for instanceof [JEP394, Java16]

使用instanceof判断一个对象是否为一个类的实例。一般来说,如果判断出确实是某一个类的实例,那下面一般都要向下转型为那个类的对象后再使用:

1
2
3
4
5
Object o = createObject();
if (o instanceof MyClass) {
    var myobj = (MyClass) o;
    // do something with myobj
}

为了简化这这一过程,引入了instanceof的模式匹配语法:

1
2
3
4
Object o = createObject();
if (o instanceof MyClass myobj) {
    // do something with myobj
}

模式匹配出的变量名的作用域会由Java自动推导:

例如:

1
2
3
4
5
Object o = createObject();
if (o instanceof MyClass myobj) {
    // myobj is available
}
// myobj is NOT available
1
2
3
4
5
Object o = createObject();
if (!(o instanceof MyClass myobj)) {
    // myobj is NOT available
}
// myobj is available
1
2
3
Object o = createObject();
if (o instanceof String s || s.length() > 5) { // CE!
}

Records [JEP395, Java16]

对于纯数据类,我们仍然要定义一大堆构造函数、赋值、Getter&Setter、toStringhashCode()equals(Object)等一大堆方法。尽管这些方法都可以让 IDE 帮忙生成,但是一大堆没营养的代码堆在那里都让人觉得异常繁琐。

记录(英语:Record) 就是为了解决这个问题而生。Record是一个只读的数据类,并且 Java 编译器会自动生成上面说的那一堆方法。

1
record Point(int x, int y) {}

这就是一个典型的Record。它大多数行为都与普通类没有区别,除了它的 Getter 方法不叫getXXX 而直接和变量名相同。

1
2
3
4
5
6
jshell> var p1 = new Point(1, 2)
p1 ==> Point[x=1, y=2]
jshell> p1.toString()
$35 ==> "Point[x=1, y=2]"
jshell> p1.hashCode()
$36 ==> 33
1
2
3
4
5
6
jshell> p1.x() //getter 方法
$37 ==> 1
jshell> var p2 = new Point(2, 3)
p2 ==> Point[x=2, y=3]
jshell> p1.equals(p2)
$38 ==> false

Compact Canonical Constructor 紧凑型构造器

可以使用紧凑型构造器来预处理或验证输入参数:

1
2
3
4
5
6
7
record Rational(int num, int denom) {
    Rational {
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
    }
}

紧凑型构造器(英语: Compact Canonical Constructor)运行完后,Java 会再根据改变后的参数值对成员域进行一遍初始化。

相当于传统的构造器:

1
2
3
4
5
6
7
8
9
record Rational(int num, int denom) {
    Rational(int num, int demon) {
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
        this.num = num;
        this.denom = denom;
    }
}

record的使用有以下规则:

  • record 类不应该继承其他的类,因为它默认继承java.lang.Record
  • record 类隐式为final,并不允许为abstract
  • record 类的成员变量隐式为final

Annotation

注解(英语: Annotation)为代码提供了一些附加信息,如@Override。通过提供响应的注解处理程序,可以实现以下等功能:

  1. 编译时代码检查;
  2. 编译时代码生成;
  3. 运行时动态发现与注册。

本文不讲解反射系统,因此也不会设计注解处理程序的设计。本节的目的在于如何使用注解语法。

Java 标准库内提供的注解有:

  • @Override: 检查是否重载方法。
  • @Deprecated: 表示注解的类、常量、方法被废弃。
  • @FunctionalInterface: 表示注解的接口是函数式接口。
  • @SuppressWarnings: 用于让编译器忽略特定的警告。

注解也可以有参数值,如@DeprecatedforRemoval(警告后续版本会被移除)和since(从哪个版本开始废弃),语法如下:

1
2
@Deprecated(forRemoval=true, since="v233")
class OldClass { /* ... */ }

声明注解的语法如下:

1
2
3
4
5
6
7
8
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE,
                METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
    String since() default "";
    boolean forRemoval() default false;
}
  • @Document: 提示这个注解应该被收入 JavaDoc。
  • @Retention(RetentionPolicy.RUNTIME): 表示这个注解的信息应该保留到什么时候。
  • RetentionPolicy.RUNTIME: 保留至运行时,反射系统可以读取。
  • RetentionPolicy.CLASS: 保留至类文件。
  • RetentionPolicy.SOURCE: 编译后即被丢弃。
  • @Target(value={ /* ... */ }): 提示这个注解可以应用给谁。

  • @interface: 声明注解。

  • <Type> name() [default <value>]: 声明某一类型的参数,可以提供默认值。

注解系统在 Java 标准库中应用较少,但是在其他库,如 Lombok ,Spring 等中,都会起到举足轻重的作用。如果有时间,我们也会接触到这些类库。

Exception Handling

想要向外部抛出异常表示程序出错,使用throw:

1
2
3
if (!SUPPORTED_FILETYPE.contains(filetype)) {
    throw new IllegalArgumentException(filetype + " is not supported");
}

异常均支持在实例化的时候提供自定义的错误信息以便调试查错。

想要捕获异常,使用try-catch:

1
2
3
4
5
6
try {
    var content = Files.readString(Path.of(location));
} catch (IOException e) {
    System.err.println("Failed to read " + location);
    e.printStackTrace();
}

使用printStackTrace()函数打印异常栈信息,描述了异常传播的途径和对应的源代码位置等信息。

你可以利用捕获的异常去构建其他类型等异常并重新抛出,原来捕获等异常就称为新异常的原因。这样可以提升异常系统的语义,也防止单一异常栈过长:

1
2
3
4
5
try {
    var config = Files.readString(Path.of(configFile));
} catch (IOException e) {
    throw new GameInitException("Failed to read config file", e);
}

你可以一次捕获多个异常:

1
2
3
4
5
6
7
8
try {
    var config = Files.readString(Path.of(configFile));
    var jsonConfig = parseJSON(config);
} catch (IOException e) {
    throw new GameInitException("Failed to read config file", e);
} catch (JSONException e) {
    throw new GameInitException("Config file format error", e);
}

如果有一些系统资源,如文件句柄等,无论后续读取是否正常,都应该将其关闭。这时就需要finally子句进行资源回收:

1
2
3
4
5
6
7
8
var input = new FileReader("file.txt");
try {
    // read data using input
} catch (IOException e) {
    // error handling
} finally {
    input.close();
}

对于实现了AutoCloseable接口的类,如所有输入输出组件,支持try-with-resources自动调用close()而无需finally:

1
2
3
4
5
try (var input = new FileReader("file.txt")) {
    // read data using input
} catch (IOException e) {
    // error handling
}

异常分为两类:

  • java.lang.RuntimeException: 运行时异常,又称为非检查异常。通常表示程序逻辑错误,比如数组越界,空指针等问题。
  • 其他java.lang.Exception子类但是不是java.lang.RuntimeException子类的其他类: 受检查异常。通常表示外部环境诱导的错误,程序应该考虑这些情况并进行处理,比如文件不存在,网络连接超时等问题。

受检查异常得名的原因在于:

  • 可能抛出受检查异常的代码必须被try-catch处理,或者向外部声明自己可能传播受检查异常。

通过throws子句声明方法可能会抛出某些受检查异常:

1
2
3
public static void main(String[] args) throws IOException {
    var content = Files.readString(Path.of(args[0]));
}

你可以使用 Java 标准库中提供的异常类,请参见API Ref中的 Direct Known Subclasses 小节寻找是否有自己需要的异常类。

通过继承自java.lang.Exceptionjava.lang.RuntimeException,你也可以创建自定义的受检异常和非受检异常类。你可以让其包含更多调试信息或生成更清晰直观的toString()表示。

Generic 泛型

通过以下的例子来了解泛型(英语: Generic)的动机:

所有的列表都是java.util.List的实现类,而它是可以存储任意对象的,即java.lang.Object。如果我们想存一个文件名列表,还需要进行强制类型转换:

1
2
3
4
List filenames = new ArrayList();
filename.add("file1.txt");

String aFile = (String) filenames.get(0);

一方面,我们不期望这个filenames里面存在任何非字符串的对象,但是没有禁止我们插入:

1
filenames.add(114514);

这样在运行时,无辜的代码会执行(String)类型转换,于是会引起java.lang.ClassCastException

另一方面,在每次使用的时候都进行一遍类型转化显然是非常蠢的语法。

所以有什么办法能够将我们的列表和字符串类型绑定在一起呢?这就是泛型:

1
2
3
4
5
var filenames = new ArrayList<String>();
filename.add("file1.txt");
String aFile = filename.get(0);

// filename.add(114514);

语法

使用泛型的语法如上所示,在类型名后面的<>内依次输入类型参数即可:

1
2
List<String> // 存储String的List列表
Function<String, Integer> // 接受String类型参数返回一个整数的函数

定义泛型类或接口的方法也和使用的方法相似,我们使用类型变量来作为需要关联的类型的占位符:

1
2
3
4
5
6
public interface List<T> {
    T get(int index);
    void add(T t);
    void set(int index, T t);
    // ...
}

定义泛型方法的方式略有不同,你需要在返回值类型前加入类型变量列表。使用泛型方法时,需要在.和方法名之间插入类型参数列表:

1
2
3
4
5
6
7
class ArrayUtil {
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }
}

var middle = ArrayUtil.<String>getMiddle("one", "two", "three"); // "two"

在几乎大多数情况下,类型参数列表可以自动推导:

1
var middle = ArrayUtil.getMiddle("one", "two", "three");

对类型变量的限定

有时我们不想要所有类型都混入我们的方法中,因此需要对类型变量做一些限定,比如下面寻找数组最小值的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class ArrayUtil {
    public static <T> T getMin(T[] a) {
        if (a == null || a.length == 0) return null;
        T min = a[0];
        for(int i = 1;i<a.length;i++)
            if (min.compareTo(a[i]) > 0)
                min = a[i];
        return min;
    }
}

但是并不是所有的对象都支持正确的比较方法,因此我们限定T应当是可比较的,即Comparable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class ArrayUtil {
    public static <T extends Comparable> T getMin(T[] a) {
        if (a == null || a.length == 0) return null;
        T min = a[0];
        for(int i = 1;i<a.length;i++)
            if (min.compareTo(a[i]) > 0)
                min = a[i];
        return min;
    }
}

如果想要T同时是多个接口的实现类,那么可以使用&连接这些接口:

1
<T extends Comparable & Serializable>

通配符类型

由于 Java 限制,下面代码是不能通过编译的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Person {}
class Student extends Person {}

public static void printPersons(List<Person> persons) { /* ... */ }

public static void main(String[] args) {
    var students = new ArrayList<Student>();
    // add students
    printPersons(students);
}

简单来说,就是虽然StudentPerson的子类型,但是List<Student>不是List<Person>的子类型。这听起来确实有点反直觉,因为学生的列表就应该是人的列表。

为了解决这种情况,Java 引入了通配符类型?,用于表示类型参数的派生:

1
2
3
4
public static void printPersons(List<? extends Person> persons) {
    for(Person p : persons)
        System.out.println(p);
}

资源链接

评论

作者: 李骋昊