全栈开发那些事

全栈开发那些事

JDK17新特性

103
2024-06-28
JDK17新特性

JDK17新特性

有些特性其实在JDK17之前就出现了。

1.文本块

在Java中,通常需要使用String类型表达HTML,XML,SQL或JSON等格式的字符串,在进行字符串赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护。

在JDK13中使用"""作为文本块的开始符和结束符,在其中就可以放置多行的字符串,不需要进行任何转义。因此,文本块将提高Java程序的可读性和可写性。

旧版写法:

public static String getHtmlJDK8() {
    return "<html>\n" +
            " <body>\n" +
            " <p>HelloWorld</p>\n" +
            " </body>\n" +
            "</html>";
}

新版写法:

public static String getHtmlJDK17() {
    return """
    <html>
        <body>
            <p>HelloWorld</p>
        </body>
    </html>
    """;
}

2.增强的空指针异常(NullPointerException)

在 JDK 14 中引入的增强的空指针异常(NullPointerException)消息功能极大地帮助了开发人员调试和解决代码中的 NPE 问题。这一特性也在 JDK17 中得到了保留和完善。

当发生空指针异常时,JVM 会提供详细的信息,指出哪个变量或表达式是 null,从而帮助开发人员快速定位问题。以下是一些示例,展示了增强的 NPE 消息是如何工作的:

简单的NPE:

try {
    //简单的空指针
    String str = null;
    System.out.println(str.length());
} catch (Exception e) {
    e.printStackTrace();
}

image-20240628122024869

在上述代码中,传统的 NPE 消息可能只是简单地告诉你发生了空指针异常,而增强的 NPE 消息会告诉你 strnull

复杂一点的NPE:

try {
    //复杂一点的空指针
    var arr = List.of(null);
    String str = (String) arr.get(0);
    System.out.println(str.length());
} catch (Exception e) {
    e.printStackTrace();
}

image-20240628122132715

3.Record

**JDK14中预览特性:神说要用record,于是就有了。**实现一个简单的数据载体类,为了避免编写:构造函数,访问器,equals(),hashCode () ,toString ()等,Java 14推出record。

record 是一种全新的类型,它本质上是一个 final 类,同时所有的属性都是 final 修饰,它会自动编译出 public get 、hashcode 、equals、toString、构造器等结构,减少了代码编写量。

具体来说:当你用record 声明一个类时,该类将自动拥有以下功能

  • 获取成员变量的简单方法,比如例题中的 name() 和 partner() 。注意区别于我们平常getter()的写法。

  • 一个 equals 方法的实现,执行比较时会比较该类的所有成员属性。

  • 重写 hashCode() 方法。

  • 一个可以打印该类所有成员属性的 toString() 方法。

  • 只有一个构造方法。

此外:

  • 还可以在record声明的类中定义静态字段、静态方法、构造器或实例方法。

  • 不能在record声明的类中定义实例字段;类不能声明为abstract;不能声明显式的父类等。

传统的写法:

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}

使用record:

public record PointRecord(int x,int y) {

    public static void main(String[] args) {
        Point point = new Point(2,3);
        PointRecord record = new PointRecord(2, 3);
        System.out.println(point);
        System.out.println(record);
    }
}

image-20240628123007510

4.全新的switch表达式

传统switch声明语句的弊端:

  • 匹配是自上而下的,如果忘记写break,后面的case语句不论匹配与否都会执行; --->case穿透

  • 所有的case语句共用一个块范围,在不同的case语句定义的变量名不能重复;

  • 不能在一个case里写多个执行结果一致的条件;

  • 整个switch不能作为表达式返回值;

旧版switch写法:

public static int getByJDK8(Week week){
    int i=0;
    switch (week){
        case MONDAY:
            i=1;
            break;
        case TUESDAY:
            i=2;
            break;
        case WEDNESDAY:
            i=3;
            break;
        case THURSDAY:
            i=4;
            break;
        case FRIDAY:
            i=5;
            break;
        case SATURDAY:
            i=6;
            break;
        default:
            i=7;
            break;
    }
    return i;
}
public enum Week {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

JDK13中引入了yield语句,用于返回值。这意味着,switch表达式(返回值)应该使用yield,switch语句(不返回值)应该使用break。

yield和return的区别在于:return会直接跳出当前循环或者方法,而yield只会跳出当前switch块。

新版写法:

public static int getByJDK17Demo(Week week){
    int day=switch (week){
        case null:
            yield -1;
        case MONDAY:
            yield 1;
        case TUESDAY:
            yield 2;
        case WEDNESDAY:
            yield 3;
        case THURSDAY:
            yield 4;
        case FRIDAY:
            yield 5;
        case SATURDAY:
            yield 6;
        default:
            yield 7;
    };
    return day;
}

或者:

public static int getByJDK17(Week week){
     int day= switch (week){
        case null->-1;
        case MONDAY -> 1;
        case TUESDAY -> 2;
        case WEDNESDAY -> 3;
        case THURSDAY -> 4;
        case FRIDAY -> 5;
        case SATURDAY -> 6;
        default -> 7;
     };
     return day;
}

image-20240628123617512

5.私有接口方法

在 Java 9 中引入的私有接口方法允许接口提供一些共用的私有帮助方法,这些方法不能被接口的实现类访问。私有接口方法有助于减少代码重复,提高代码可读性和维护性。私有接口方法可以是普通方法,也可以是静态方法。

示例1:

public interface MyInterface {
    default void defaultMethod1() {
        System.out.println("Default Method 1");
        privateMethod();
    }

    default void defaultMethod2() {
        System.out.println("Default Method 2");
        privateMethod();
    }

    private void privateMethod() {
        System.out.println("Private Method");
    }
}

 class MyClass implements MyInterface {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.defaultMethod1();
        myClass.defaultMethod2();
    }
}

image-20240628124206689

示例2:

public interface Calculator {
    default int add(int a, int b) {
        return a + b;
    }

    default int subtract(int a, int b) {
        return a - b;
    }

    default int multiply(int a, int b) {
        return a * b;
    }

    default int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Division by zero");
        }
        return a / b;
    }

    default void printOperations() {
        System.out.println("Available operations: add, subtract, multiply, divide");
        printSeparator();
    }

    private void printSeparator() {
        System.out.println("------------------------------");
    }
}

class CalculatorImpl implements Calculator {
    public static void main(String[] args) {
        CalculatorImpl calc = new CalculatorImpl();
        calc.printOperations();
        System.out.println("Add: " + calc.add(10, 5));
        System.out.println("Subtract: " + calc.subtract(10, 5));
        System.out.println("Multiply: " + calc.multiply(10, 5));
        System.out.println("Divide: " + calc.divide(10, 5));
    }
}

image-20240628124308946

6.instanceof的模式匹配

这一特性允许你在 instanceof 检查的同时声明一个变量,从而避免冗余的类型转换代码。

旧版写法:

if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.length());
}

新版写法:

if (obj instanceof String str) {
    System.out.println(str.length());
}

示例:

public class InstanceofPatternMatching {
    public static void main(String[] args) {
        Object obj = "Hello, world!";
        
        if (obj instanceof String str) {
            System.out.println(str.length()); // 输出: 13
        }
    }
}

7.局部变量类型推断

局部变量的显示类型声明,常常被认为是不必须的,给一个好听的名字反而可以很清楚的表达出下面应该怎样继续。本新特性允许开发人员省略通常不必要的局部变量类型声明,以增强Java语言的体验性、可读性。

//1.局部变量的实例化
var list = new ArrayList<String>();

var set = new LinkedHashSet<Integer>();

//2.增强for循环中的索引
for (var v : list) {
    System.out.println(v);
}
//3.传统for循环中
for (var i = 0; i < 100; i++) {
    System.out.println(i);
}
//4. 返回值类型含复杂泛型结构
var iterator = set.iterator();
//Iterator<Map.Entry<Integer, Student>> iterator = set.iterator();

不适用场景:

  • 声明一个成员变量
  • 声明一个数组变量,并为数组静态初始化(省略new的情况下)
  • 方法的返回值类型
  • 方法的参数类型
  • 没有初始化的方法内的局部变量声明
  • 作为catch块中异常类型
  • Lambda表达式中函数式接口的类型
  • 方法引用中函数式接口的类型

8.String存储结构和API变更

String 再也不用 char[] 来存储啦,改成了 byte[] 加上编码标记,节约了一些空间

(1)isBlank():判断字符串是否为空白

//2.isBlank 字符串判空
//如果字符串为空或仅包含空白字符。则返回true,否则返回false
String s="java";
System.out.println(s.isBlank());    //false
System.out.println("".isBlank());   //true
System.out.println("   ".isBlank());//true

(2)repeat:重复生成字符串

//1.repeat:重复生成字符串
String s="java";
System.out.println(s.repeat(5));//javajavajavajavajava

(3)lines:从给定多行字符串中提取的行流,并用终止符分隔,行终止符可以是换行符\n 回车符\r 回车符后紧跟换行符\r\n

try {
    String str="H\nE\nL\nL\rO";
    Stream<String> lines = str.lines();
    lines.forEach(System.out::println);
} catch (Exception e) {
   e.printStackTrace();
}

image-20240628160720031

(4)transform:接收一个转换函数,实现字符串的转换

//transform
String str1="java";
//使用transform将字符全部转换为大写
String transform = str1.transform(String::toUpperCase);
System.out.println(transform);//JAVA

9.集合类的工厂方法

旧版写法:

List<String> list=new ArrayList<>();
list.add("石昊");
list.add("萧炎");
list.add("柳神");
list.forEach(System.out::println);

新版写法:

List<String> list1 = List.of("石昊", "萧炎", "柳神");
list1.forEach(System.out::println);

10.Stream API增强

takewhile:从Stream中依次获取满足条件的元素,直到不满足条件为止

//takewhile:从Stream中依次获取满足条件的元素,直到不满足条件为止
//一旦条件不满足的时候,后面的所有元素都会被忽略
List<Integer> list = Stream.of(2, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .takeWhile(x -> x % 2 == 0)
        .toList();
//2,2
list.forEach(System.out::println);

dropwhile:顺序去掉符合条件的值,直到不满条件时终止判断

//dropwhile:顺序去掉符合条件的值,直到不满条件时终止判断
List<Integer> dropList = Stream.of(2, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .dropWhile(x -> x % 2 == 0)
        .toList();
//3,4,5,6,7,8,9,10
dropList.forEach(System.out::println);

ofNullable :支持传入空流,若没有这个且传入一个空流,会抛出NPE

//ofNullable 支持传入空流,若没有这个且传入一个空流,会抛出NPE
var count = Stream.ofNullable(null).count();
System.out.println(count);//0

11.密封类

在 Java 中如果想让一个类不能被继承和修,这时我们应该使用 final 关键字对类进行修饰。不过这种要么可以继承,要么不能继承的机制不够灵活,有些时候我们可能想让某个类可以被某些类型继承,但是又不能随意继承,是做不到的。Java 15 尝试解决这个问题,引入了 sealed 类,被 sealed 修饰的类可以指定子类。这样这个类就只能被指定的类继承。

JDK15的预览特性:

通过密封的类和接口来限制超类的使用,密封的类和接口限制其它可能继承或实现它们的其它类或接口。

  • 使用修饰符sealed,可以将一个类声明为密封类。密封的类使用保留关键字permits列出可以直接扩展(即extends)它的类。

  • sealed 修饰的类的机制具有传递性,它的子类必须使用指定的关键字进行修饰,且只能是 final、sealed、non-sealed 三者之一。

public abstract sealed class Shape permits Circle,Rectangle,Square{
}
//final表示Circle不能再被继承了
final class Circle extends Shape { }
sealed class Rectangle extends Shape permits TransparentRectangle,FilledRectangle {}

final class TransparentRectangle extends Rectangle {}

final class FilledRectangle extends Rectangle {}
non-sealed class Square extends Shape {} //non-sealed表示可以允许任何类继承

12.Java的REPL工具: jShell命令

Java 终于拥有了像Python 和 Scala 之类语言的REPL工具(交互式编程环境,read - evaluate - print - loop):jShell。以交互式的方式对语句和表达式进行求值。即写即得、快速运行。

13.革命性的 ZGC

官方宣称ZGC的垃圾回收停顿时间不超过10ms,能支持高达16TB的堆空间,并且停顿时间不会随着堆的增大而增加。
JDK11:引入革命性的 ZGC
ZGC,这应该是JDK11最为瞩目的特性,没有之一。
ZGC是一个并发、基于region、压缩型的垃圾收集器。
ZGC的设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%。 将来还可以扩展实现机制,以支持不少令人兴奋的功能,例如多层堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。
JDK13:ZGC:将未使用的堆内存归还给操作系统

ZGC是Java 11引入的新的垃圾收集器,经过了多个实验阶段,在JDK15中终于成为正式特性。
JDK16:ZGC 并发线程处理

14 更简化的编译运行程序

我们的认知里,要运行一个 Java 源代码必须先编译,再运行。

// 编译
javac JavaStack.java

// 运行
java JavaStack

而在 Java 11 版本中,通过一个 java 命令就直接搞定了,如下所示:

image-20240630221626817

结尾

Java 代码虽然进行了一些类型推断等改进,更易用的集合 API 等,但仍然给开发者留下了过于刻板、形式主义的印象,这是一个长期的改进方向。虽然标榜面向对象编程,却毫不顾忌的加入面向接口编程思想,又扯出匿名对象的概念,每增加一个新的东西,都是对Java根本(面向对象思想)的一次冲击。