Java新版本特性(9到17)

近几年来,JDK秉承着“半年一个release,三年一个LTS”的发布原则,目前最新的LTS版本是Java 17(2021/09)。

注: LTS 即Long Term Support,长期支持版本。

最近一段时间,项目组终于打破了“你发任你发,我用Java8”的传统,准备升级到Java 17,所以我也来学习一下Java新版本的特性,看看Java 17(严格讲是从9到17)给我们带来了哪些新特性。

Java语言升级的官方文档,请参考: https://docs.oracle.com/en/java/javase/17/language/java-language-changes.html

record (Java 16)

record 是一种特殊的类,它是简单的数据聚集的建模,比传统意义上的数据类减少了很多“繁文缛节”。

这些繁文缛节,包含了访问器(Getter/Setting方法),构造器, equals() 方法, hashCode() 方法, toString() 方法等。使用 record 时无需关心这些,应该有的东西它会自动帮你弄好。

例如:

public record MyRecord(int id, String name) {
}
MyRecord myRecord1 = new MyRecord(1, "Tom");

在构造器中定义好数据,接下来就可以用 myRecord1.id()myRecord.name() 来访问数据了。

注意,不是 myRecord1.id ,也不是 myRecord1.getId()

我们并没有给 MyRecord 定义构造器,系统会自动把id和name保存到MyRecord里。

当然也可以自定义构造器,例如:

public record MyRecord(int id, String name) {
    public MyRecord(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

这种构造器称为canonical(规范) constructor。

注意:上例中的构造器完全可以省略。IntelliJ IDEA会提示“Redundant canonical constructor”。

这是因为构造器只做了赋值操作。如果还做一些其它事情,比如:

public record MyRecord(int id, String name) {
    public MyRecord(int id, String name) {
        System.out.println("hello " + name);
        this.id = id;
        this.name = name;
    }
}

现在,IntelliJ IDEA会提示“Canonical constructor can be converted to compact form”。确认后,会变成如下称为compact(紧凑) constructor的形式:

public record MyRecord(int id, String name) {
    public MyRecord {
        System.out.println("hello " + name);
    }
}

注意,record的所有field都是final的,不能改变,因为record的意图只是简单的“数据载体”。

instanceof (Java 16)

我们都很熟悉以下代码:

    public void f(Object obj) {
        if (obj instanceof MyObj) {
            MyObj myObj = (MyObj) obj;
            myObj.doSth();
        }
    }

这种“先判断,再强转”的方式,现在改进如下:

    public void f(Object obj) {
        if (obj instanceof MyObj myObj) {
            myObj.doSth();
        }
    }

注意,新旧方式都无需判断null,如果 obj 是null,不会出现空指针异常。

myObj 被称为 Pattern variable

可以用 final 修饰 myObj ,标识其不可更改:

    public void f(Object obj) {
        if (obj instanceof final MyObj myObj) {
            myObj.doSth();
//            myObj = new MyObj(); // 编译错误
        }
    }

上面的例子只是减少了一行代码,下面的代码改写后,可读性更佳:

    public void f(Object obj) {
        if (obj instanceof MyObj) {
            MyObj myObj = (MyObj) obj;
            if ("aaa".equals(myObj.getName())) {
                myObj.doSth();
            }
        }
    }

改写后如下:

    public void f(Object obj) {
        if (obj instanceof MyObj myObj && "aaa".equals(myObj.getName())) {
            myObj.doSth();
        }
    }

Stream API (Java 16)

假如有一个MyObj的list,需要过滤出age大于20的元素,代码如下:

List<MyObj> list2 = list.stream().filter(e -> e.getAge() > 20).collect(Collectors.toList());

如果我们需要一个不可修改的list,则代码改写如下:

List<MyObj> list2 = Collections.unmodifiableList(list.stream().filter(e -> e.getAge() > 20).collect(Collectors.toList()));

或者简写如下:

List<MyObj> list2 = list.stream().filter(e -> e.getAge() > 20).collect(Collectors.toUnmodifiableList());

甚至还可以更进一步简写如下:

List<MyObj> list2 = list.stream().filter(e -> e.getAge() > 20).toList();

Text block (Java 15)

即文本块。对于一段email模板或者JSON数据,例如:

{
	"info": [
		{
			"name": "张三",
			"sex": "男",
			"age": 20
		},
		{
			"name": "李四",
			"sex": "女",
			"age": 30
		},
		{
			"name": "王五",
			"sex": "男",
			"age": 40
		}
	]
}

这是一段格式良好的JSON数据,在Java中用字符串表示为:

String str = "{\n" +
    "\t\"info\": [{\n" +
    "\t\t\t\"name\": \"张三\",\n" +
    "\t\t\t\"sex\": \"男\",\n" +
    "\t\t\t\"age\": 20\n" +
    "\t\t},\n" +
    "\t\t{\n" +
    "\t\t\t\"name\": \"李四\",\n" +
    "\t\t\t\"sex\": \"女\",\n" +
    "\t\t\t\"age\": 30\n" +
    "\t\t},\n" +
    "\t\t{\n" +
    "\t\t\t\"name\": \"王五\",\n" +
    "\t\t\t\"sex\": \"男\",\n" +
    "\t\t\t\"age\": 40\n" +
    "\t\t}\n" +
    "\t]\n" +
    "}";

像这样把转义符 \n\t\" 和字符串内容混杂在一起的方式,非常不直观。

从Java 15开始,引入了“文本块”的概念,例如:

String str = """
    {
        "info": [
            {
                "name": "张三",
                "sex": "男",
                "age": 20
            },
            {
                "name": "李四",
                "sex": "女",
                "age": 30
            },
            {
                "name": "王五",
                "sex": "男",
                "age": 40
            }
        ]
    }
    """;

简言之,就是“所见即所得”,大大提高了直观性。

注意事项:

  1. 字符串和开头的 """ 之间必须换行,否则报错;
  2. 字符串和结尾的 """ 之间可以不换行。事实上如果有换行,则字符串末尾也会多出一个空行;
  3. 文本的缩进,取决于它们和第一行文本的相对位置。在本例中,第二行的 "info": [ 前面有8个空格,但是相对于第一行的 { ,只有4个空格,所以实际上第二行文本前面是4个空格;
  4. 每行文本末尾的空格将被自动移除。例如,对本例的本文块,如果把第三行的 { 之后加几个空格,则实际得到的字符串里并没有这些空格;
  5. 文本块里面也支持像 %s%d 之类的格式化;

sealed class (Java 17)

在父类/子类继承关系中,通常是子类在定义中指明其派生自哪个父类,而父类则并不知道自己被哪些子类所继承。

如果你想明确指定某一个类可被哪些子类所继承,则可以用 sealed class

例如,对于 Fruit 类,只允许 AppleBananaOrange 这3个子类继承:

在这里插入图片描述
Fruit 类的定义如下:

public sealed class Fruit permits Apple, Banana, Orange {
}

Fruit 的子类在定义时,必须包含以下三个修饰符之一:

  • sealed :子类也是sealed class,必须显式指定其子类;
  • non-sealed :普通类;
  • final:不可再被继承;

Apple 类被定义为 sealed

public sealed class Apple extends Fruit permits Fuji {
}

此处 Fuji 也同理,其定义必须包含三个修饰符之一。

Banana 类被定义为 non-sealed

public non-sealed class Banana extends Fruit {
}

Orange 类被定义为 final

public final class Orange extends Fruit {
}

Switch Expression (Java 14)

看下面的代码:

    public String getDay(int day) {
        String result = "";
        switch (day) {
            case 0:
                result = "Sunday";
                break;
            case 1:
                result = "Monday";
                break;
            case 2:
                result = "Tuesday";
                break;
            case 3:
                result = "Wednesday";
                break;
            case 4:
                result = "Thursday";
                break;
            case 5:
                result = "Friday";
                break;
            case 6:
                result = "Saturday";
                break;
            default:
                result = "Unknown";
        }
        return result;
    }

这是一个典型的switch语句,注意别忘了 break

新的写法如下:

    public String getDay(int day) {
        String result = switch (day) {
            case 0 -> "Sunday";
            case 1 -> "Monday";
            case 2 -> "Tuesday";
            case 3 -> "Wednesday";
            case 4 -> "Thursday";
            case 5 -> "Friday";
            case 6 -> "Saturday";
            default -> "Unknown";
        };
        return result;
    }

这看起来类似于Lamda表达式,注意在各个case分支之间不需要 break

另外,如果一个case分支里有多个语句,写法如下:

......
            case 0 -> {
                System.out.println(day);
                yield "Sunday";
            }
......

同样,这跟Lamda写法是一致的。但要注意这里不是 return 而是 yield

Local Variable Type Inference (Java10)

局部变量类型推断。看下面的代码:

        List<String> list1 = List.of("aaa", "bbb");
        List<String> list2 = List.of("ccc", "ddd");
        List<List<String>> list3 = List.of(list1, list2);

现在,这段代码可以简写为:

        var list1 = List.of("aaa", "bbb");
        var list2 = List.of("ccc", "ddd");
        var list3 = List.of(list1, list2);

相比较一下,是不是清爽了很多呢。

注意,类型推断发生在编译期,这意味着编译器知道变量的确切类型,所以仍然可以把 list3 看作一个List:

        list3.forEach(e -> e.forEach(System.out::println));

结果如下:

aaa
bbb
ccc
ddd

不能给var变量赋null值,因为编译器无法推断 null 的类型:

        var obj = null; // 编译错误

var变量的类型实际上是确定的,不能自相矛盾:

        var obj = 1;
        obj = "aaa"; // 编译错误

obj 不能一会儿是数值,一会儿是字符串。

注意: var 并不是一个关键字(为啥呢?):

        var var = 1; // OK

此处把变量命名为 var ,这是OK的(好像是自找麻烦……)。

copyOf() (Java 10)

    public void consumeList(List<String> list) {
        list.add("ccc");
    }

    public void test() {
        List<String> list = new ArrayList<String>();
        list.add("aaa");
        list.add("bbb");

        consumeList(list);

        System.out.println(list);
    }

test() 方法里创建了一个List,然后调用 consumeList() 方法来消费该list,然而, consumeList() 方法改变了List的内容,这可能不是期望的行为。为了避免这样的行为,可用List的 copyOf() 方法来生成一个不可改变的List:

        consumeList(List.copyOf(list));

运行程序,则会在 list.add("ccc"); 处抛出 java.lang.UnsupportedOperationException 异常。

注意,用 List.of() 方法创建的List,也是不可改变的List。

        List<String> list = List.of("xxx", "yyy");
        list.add("zzz"); // 运行期报错:aUnsupportedOperationException

其它集合类,比如 SetMap ,也有类似的方法,不再赘述。

Predicate.not() (Java 11)

假定你有一个数值的List:

        List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

现在要过滤出所有的偶数:

        list = list.stream().filter(e -> e % 2 == 0).toList();

如果反过来,要过滤所有的非偶数(即奇数,这么说是为了表示取反)。

        list = list.stream().filter(((Predicate<Integer>) (e -> e % 2 == 0)).negate()).toList();

这里用到了Predicate的 negate() 方法来取反。

注意,Predicate前面加了强制类型转换,否则就报错。。。可以换种写法,看的更清楚:

        Predicate<Integer> predicate = e -> e % 2 == 0;
        list = list.stream().filter(predicate.negate()).toList();

这里显式定义了一个Predicate变量,并调用其 negate() 方法来取反。

使用 Predicate.not() 方法的写法如下:

        list = list.stream().filter(Predicate.not(e -> e % 2 == 0)).toList();

如果Predicate实际上是一个方法:

    public static boolean isEven(Integer e) {
        return e % 2 == 0;
    }

则过滤偶数的代码如下:

        list = list.stream().filter(TestPredicateNot::isEven).toList();

注:此处 TestPredicateNot 是该方法定义所在的类。

过滤奇数的代码如下:

        list = list.stream().filter(((Predicate<Integer>)(TestPredicateNot::isEven)).negate()).toList();

同理,此处必须强转,否则报错。

如果使用 Predicate.not() 方法,代码如下:

        list = list.stream().filter(Predicate.not(TestPredicateNot::isEven)).toList();

CompletableFuture.completeOnTimeout (Java 9)

main() 方法中启动一个异步线程来做一些事情,在本例中只是sleep了5秒,同时在主线程中sleep 10秒,为的是让异步线程先结束:

    public static void handleResult(String result) {
        System.out.println(new Date() + ": ==========result: " + result + "===========");
    }

    public static void main(String[] args) {
        System.out.println(new Date() + ": main start");
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println(new Date() + ": thread start");
            try {
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new Date() + ": thread end");
            return "hello";
        });

        future.thenAccept(e -> handleResult(e));

        try {
            Thread.sleep(10* 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(new Date() + ": main end");
    }

程序运行结果如下:

Sat Feb 26 22:05:31 CST 2022: main start
Sat Feb 26 22:05:31 CST 2022: thread start
Sat Feb 26 22:05:36 CST 2022: thread end
Sat Feb 26 22:05:36 CST 2022: ==========result: hello===========
Sat Feb 26 22:05:41 CST 2022: main end

OK,一切都是期望结果。

现在,给异步线程设置一个3秒钟的timeout时间。把下面的代码加到 future.thenAccept(e -> handleResult(e)); 之前:

        future.completeOnTimeout("default timeout result", 3, TimeUnit.SECONDS);

程序运行结果如下:

Sat Feb 26 22:10:26 CST 2022: main start
Sat Feb 26 22:10:27 CST 2022: thread start
Sat Feb 26 22:10:30 CST 2022: ==========result: default timeout result===========
Sat Feb 26 22:10:32 CST 2022: thread end
Sat Feb 26 22:10:37 CST 2022: main end

可见,timeout并不会真正停止task的运行,也不会给task发interrupt信号。

关于CompletableFuture的详细介绍,参见我另一篇文档。

String.formatted() (Java 15)

假定字符串模板定义如下:

        String template = "Hello, my name is %s, and I am %d years old.";

要想填充姓名和年龄:

        String result = String.format(template, "Tom", 20);

新方法要更简洁一些:

        String result = template.formatted("Tom", 20);

参考

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值