JDK9~JDK17新特性

144 阅读10分钟
  1. JDK9

  1. 接口私有方法

在JDK8之前,接口是抽象方法和常量值的定义的集合,并且只包含常量和方法的定义,而没有变量和方法的实现。

在JDK8中,接口引入了default关键字以添加默认实现,对接口进行了增强。

而在JDK9中,接口引入了private关键字,使得接口更加灵活。

public interface TestInterface {

    default void test() {
        System.out.println("this is test method");
        privateMethod();
    }

    private void privateMethod() {
        System.out.println("this is private method");
    }
}

class PrivateTestInterface implements TestInterface {
    
    public static void main(String[] args) {
        PrivateTestInterface privateTest = new PrivateTestInterface();
        privateTest.test();
    }
    
}

其中,私有方法必须要提供方法体,并且此方法只能被接口中的其他私有方法或是默认实现调用。

  1. 集合类工厂方法

在JDK9之前,如果我们想创建一个集合,例如HashMap,只能先创建对象,再向其中填充对应的值。

而在JDK9之后,我们可以通过Map.of方法快速创建并初始化对象。

//JDK9之前
Map<Integer, Integer> map1 = new HashMap<>();
map1.put(1, 2);
map1.put(3, 4);

//JDK9之后
Map<Integer, Integer> map2 = Map.of(1, 2, 3, 4)

其他集合类,例如List、Set,也有对应的of方法。

查看List接口的of方法,发现JDK是通过将of方法重载了若干次以达到快速填充对象的效果,List、Map、Set通过这种方式均至多能够一次性填充10个元素。

不过通过这种方式创建的集合类都是不可变集合,无法对其中的元素进行更改。如果需要使用可变集合,还是只能老老实实手动创建。

  1. stream功能增强

  1. stream.of增强

在JDK8中,如果使用stream.of传入了null,对其操作可能会造成NPE。

而JDK9中提供了Stream.ofNullable方法,可以对null对象进行过滤,返回空stream。

  1. stream.iterate增强
//JDK8只能像这样生成无限的流,并使用limit()进行限制
Stream.iterate(0, i -> i + 1).limit(20).forEach(System.out::println);

//JDK9可以直接指定 i < 20以终止流
Stream.iterate(0, i -> i < 20, i -> i + 1).forEach(System.out::println);
  1. stream数据截断功能

提供了takeWhile和dropWhile功能,可以对stream流进行截断处理,类似for循环加上break。

List<Integer> list = List.of(1, 2, 3, 4, 1, 2, 3, 4);

//元素满足小于4时通过,大于等于4时直接将流截断
list.stream().takeWhile(i -> i < 4).forEach(System.out::println);

//元素满足大于等于4时通过,小于等于4的部分直接截断
list.stream().dropWhile(i -> i < 4).forEach(System.out::println);

分别输出1、2、3和4、1、2、3、4。

  1. try-with-resources功能增强

//JDK9之前
try (InputStream inputStream = Files.newInputStream(Paths.get("pom.xml"));) {
    for (int i = 0; i < 100; i++)
        System.out.print((char) inputStream.read());
}

//JDK9以后
InputStream inputStream = Files.newInputStream(Paths.get("pom.xml"));
try (inputStream) {
    for (int i = 0; i < 100; i++)
        System.out.print((char) inputStream.read());
}
  1. CompletableFuture功能增强

在JDK9中,主要引入了对CompletableFuture支持超时和延迟的新方法,弥补了缺少设置异步超时时间的缺憾。

其中,orTimeout方法是指定超时时间,如果超时则抛出TimeoutException

completeOnTimeout方法是指定默认值和超时时间,如果超时则采用默认值,不抛出异常

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> timeout = CompletableFuture.supplyAsync(() -> calc(50))
        .orTimeout(100, TimeUnit.MILLISECONDS)
        .exceptionally(ex -> 0);
    //输出0
    System.out.println(timeout.get());

    CompletableFuture<Integer> completeOnTimeout = CompletableFuture.supplyAsync(() -> calc(50))
            .completeOnTimeout(123, 100, TimeUnit.MILLISECONDS);
    //输出123
    System.out.println(completeOnTimeout.get());
}

public static Integer calc(Integer para) {
    try {
        // 模拟一个长时间的执行
        Thread.sleep(1000);
    } catch (InterruptedException ignored) {}
    return para * para;
}
  1. String类型存储优化

在JDK9之前,String底层是由char数组实现的,每个char占用两个字节的内存空间。我们在日常使用中可能大部分情况下都在使用英文字母,较少使用一些中文或者其他复杂的字符,这种存储方式就会比较浪费空间了。

而在JDK9中,String类引入了一种称为"Compact Strings"的新实现方式,将字符串的表示方式从char数组改为byte数组,并使用一种编码方式将Unicode字符映射到一个或两个字节的表示方式。这种实现方式可以大大减少内存使用,尤其是对于包含大量ASCII字符的字符串。

JDK9中,引入了一个coder标识,用来区分是普通的拉丁字母还是UTF16字符,如果字符串中都是能用LATIN1就能表示的就是0,否则就是UTF-16。

static final byte LATIN1 = 0;
static final byte UTF16 = 1;

这样的优化使得大部分场景中,String占用的内存可以少50%,大大节省内存空间。

  1. JDK10~JDK11

  1. 局部变量类型推断

这个功能来自于JDK10的特性JEP 286: Local-Variable Type Inference,它允许在没有类型信息的情况下声明局部变量,并且只使用var关键字。

我们可以做到如下事项:

//String
var str = "abc";

//List<? extends Serializable>
var list = List.of(1, 2.0, "3");

//List<CompletableFuture<Integer>>
var list2 = Stream.of(1, 2, 3).map(i -> CompletableFuture.completedFuture(0)).toList();

//在JDK11中引入,对lambda表达式的增强
Consumer<String> consumer = (var str) -> {};

var节省了大量的代码长度,在多数情况下使得代码便于编写,但可能会提高阅读成本。

但需要注意的是,var 没有改变Java的本质,var只是一种简便的写法,在定义局部变量时,任意什么类型都可以用var定义变量的类型会根据所赋的值来判断。

  1. String类方法增强

JDK11中提供了String类的一些新方法,在日常开发中能够更加便利。

不过一般还是用StringUtils就可以解决了。

var empty = "";
var blank = "   ";
System.out.println(empty.isEmpty());
System.out.println(blank.isEmpty());
System.out.println(blank.isBlank()); //isBlank方法用于判断是否字符串为空或者是仅包含空格

var str = "AB\nC\nD";
//根据字符串中的\n换行符进行切割,分为多个字符串,并转换为Stream进行操作
str.lines().forEach(System.out::println);
//比如现在我们有一个ABCD,但是现在我们想要一个ABCDABCD这样的基于原本字符串的重复字符串
System.out.println(str.repeat(2));  //一个repeat就搞定了

str = " A B C D ";
System.out.println(str.trim());   //去除字符串前后的半角空白字符
System.out.println(str.strip());   //去除字符串前后的全角和半角空白字符
System.out.println(str.stripLeading());  //去除首部空白字符
System.out.println(str.stripTrailing());   //去除尾部空白字符
  1. JDK12~16

  1. switch语法增强

在Java 12引入全新的switch语法,让我们使用switch语句更加的灵活,比如我们想要编写一个根据成绩得到等级的方法:

public String grade(int score) {
    score /= 10;
    String res = null;
    switch (score) {
        case 10:
        case 9:
            res = "优秀";
            break;
        case 8:
        case 7:
            res = "良好";
            break;
        case 6:
            res = "及格";
            break;
        default:
            res = "不及格";
            break;
    }
    return res;
}

public String gradeNew(int score) {
    score /= 10;
    var res = switch (score) {
        case 10, 9 -> "优秀";
        case 8, 7 -> "良好";
        case 6 -> "及格";
        //也可以引入代码块,并用yield返回switch的值
        default -> {
            System.out.println("do something");
            yield "不及格";
        }
    };
    return res;
}
  1. 字符串文本增强

在JDK15之前,我们编写字符串文本需要在每一行都添加双引号,并用+号拼接字符串,如下所示:

String html = 
    "<html>\n" +
    "<body>\n"+
    "  <h1>Java 15 新特性:文本块 | 程序猿DD</h1>\n"+
    "  <p>didispace.com</p>\n"+
    "</body>\n"+
    "</html>\n";

但是JDK15中引入了和JavaScript及Python相同的三引号文本块,上面这段文本可以改写成这样:

String html = """
    <html>
    <body>
      <h1>Java 15 新特性:文本块 | 程序猿DD</h1>
      <p>didispace.com</p>
    </body>
    </html>
    """;
  1. instanceof增强

在JDK16之前,我们使用instanceof去判断类的类型时,如果想将类进行类型转换,需要在方法内进行强转,如下:

而在JDK16中,强化了instanceof的功能,在使用instanceof判断类型成立后,会自动强制转换类型为指定类型,简化了我们手动转换的步骤,如下所示:

  1. 空指针异常提示增强

空指针异常是日常开发中必不可少的一环,但是平时要排查空指针异常时,我们并不能简单通过异常提示来得知究竟是什么导致了空指针异常,我们必须得翻代码才能揣测是什么为null导致的异常。

而在JDK14中,对空指针异常的提示进行了增强,我们可以直接得知空指针的原因,使错误更加直观,可惜对于stream、lambda等操作还是无法给出具体的提示。

  1. JDK17

  1. switch模式匹配

如果使用instanceof判断取出的值的不同类型去做不同操作,可能会带来大量的if-else,如下:

if (data.get("key") instanceof String s) {
  log.info(s);
} else if (data.get("key") instanceof Double s) {
  log.info(s);
} else if (data.get("key") instanceof Integer s) {
  log.info(s);
}

这个时候,我们可以使用JDK17中引入的switch模式匹配功能,简化if-else操作,提升可读性:

switch (data.get("key1")) {
  case String s  -> log.info(s);
  case Double d  -> log.info(d.toString());
  case Integer i -> log.info(i.toString());
  default        -> log.info("");
}

switch模式匹配功能的特点是:

  1. case条件中直接涵盖了类型的判断和类型的转换,这个功能类似于JDK16中的instanceof增强功能
  2. 每个case的处理逻辑用lambda语法来实现,可以免去break语句(JDK14引入)
  1. 密封类

在面向对象语言中,我们可以通过继承(extend)来实现类的能力复用、扩展与增强。但有的时候,有些能力我们不希望被继承了去做一些不可预知的扩展。所以,我们需要对继承关系有一些限制的控制手段。而密封类的作用就是限制类的继承。

对于继承能力的控制,Java很早就已经有一些了,主要是这两种方式:

  • final修饰类,这样类就无法被继承了
  • package-private类(非public类),可以控制只能被同一个包下的类继承

但很显然,这两种限制方式的粒度都非常粗,如果有更精细化的限制需求的话,是很难实现的

为了进一步增强限制能力,Java 17中的密封类增加了几个重要关键词:

  • sealed:修饰类/接口,用来描述这个类/接口为密封类/接口
  • non-sealed:修饰类/接口,用来描述这个类/接口为非密封类/接口
  • permits:用在extendsimplements之后,指定可以继承或实现的类

下面我们通过一个例子来理解这几个关键词的用法。

 // 英雄基类  public  class  Hero {  }  // 坦克英雄的抽象  public  class  TankHero  extends  Hero {  }  // 输出英雄的抽象  public  class  AttackHero  extends  Hero {  }  // 辅助英雄的抽象  public  class  SupportHero  extends  Hero {  }  // 坦克英雄  public  class  Alistar  extends  TankHero {  }  // 输出英雄  public  class  Ezreal  extends  AttackHero {  }  // 辅助英雄  public  class  Soraka  extends  SupportHero {  } 

整体结构有三层,具体如下图所示:

  • 第一层:Hero是所有英雄的基类,定义英雄的基础属性
  • 第二层:按英雄的分类的三个不同抽象,定义同类英雄的公共属性
  • 第三层:具体英雄的定义

这个时候,为了避免开发人员在创建新英雄的时候,搞乱这样的三层结构。就可以通过引入密封类的特性来做限制。

假设我们希望第一、第二层是稳定的,对于第二层英雄种类的抽象不允许再增加,此时我们就可以这样写:

public sealed class Hero permits TankHero, AttackHero, SupportHero {}

通过sealed关键词和permitspermits关键来定义Hero是一个需要密封的类,并且它的子类只允许为TankHero, AttackHero, SupportHero这三个。

完成这个改造之后,Hero的三个子类也需要制定对应的密封性,即sealednon-sealedpermits

根据上面的假设需求,第一、第二层稳定,允许第三层具体英雄角色可以后期不断增加新英雄,所以三类抽象英雄的定义可以这样编写:

public non-sealed class TankHero extends Hero {}

而对于第三层的英雄角色,已经是最后的具体实现,则可以使用final定义来阻断后续的继承关系,比如这样:

public final class Ezreal extends AttackHero {}

通过这样的设置,这三层英雄的结构中第一第二层就得到了比较好的保护。