跳至主要內容

Java 9新特性


Java 9 新增了很多特性,针对较为突出的便于理解的特性进行说明。除了下面罗列出的新特性之外还有一些其他的内容,这些内容有的不重要或者使用较少,所以没有罗列出来。

接口私有方法

在jdk9中新增了接口私有方法,即可以在接口中声明private修饰的方法了,其实就是为了让default方法调用,当然接口外部是无法访问私有方法的。这样的话,接口越来越像抽象类

public interface MyInterface {
    //定义私有方法
    private void m1() {
        System.out.println("123");
    }
    
    //default中调用
    default void m2() {
        m1();
    }
}

改进的try with resource

Java7中新增了try with resource语法用来自动关闭资源文件,在IO流和jdbc部分使用的比较多。使用方式是将需要自动关闭的资源对象的创建放到try后面的小括号中,在jdk9中可以将这些资源对象的创建代码放到小括号外面,然后将需要关闭的对象名放到try后面的小括号中即可,示例:

/*
    改进了try-with-resources语句,可以在try外进行初始化,在括号内填写引用名,即可实现资源自动关闭
 */
public class TryWithResource {
    public static void main(String[] args) throws FileNotFoundException {
        //jdk8以前
        try (FileInputStream fileInputStream = new FileInputStream("");
             FileOutputStream fileOutputStream = new FileOutputStream("")) {

        } catch (IOException e) {
            e.printStackTrace();
        }

        //jdk9
        FileInputStream fis = new FileInputStream("");
        FileOutputStream fos = new FileOutputStream("");
        //多资源用分号隔开
        try (fis; fos) {
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

不能使用下划线命名变量

下面语句在jdk9之前可以正常编译通过,但是在jdk9(含)之后编译报错,在后面的版本中会将下划线作为关键字来使用

String _ = "seven97";

String字符串的变化

写程序的时候会经常用到String字符串,在以前的版本中String内部使用了char数组存储,对于使用英语的人来说,一个英文字符用一个字节就能存储,使用char存储字符会浪费一半的内存空间,因此在JDK9中将String内部的char数组改成了byte数组,这样就节省了一半的内存占用。

也就是说,能优化包含大量ASCII字符的字符串。

char c = 'a';//2个字节
byte b = 97;//1个字节

String中增加了下面2个成员变量

  • COMPACT_STRINGS:boolen类型,判断是否压缩,

    • 默认是true,表示开启压缩,英文字符可以节省空间

    • false,则表示不压缩,全部使用UTF16编码。也就是说,即使是英文字符,也用UTF16编码,则无法节省空间

  • coder:byte类型,用来区分使用的字符编码,分别为LATIN1(值为0)和UTF16(值为1)。

byte数组如何存储中文呢?通过源码(StringUTF16类中的toBytes方法)得知,在使用中文字符串时,1个中文会被存储到byte数组中的两个元素上,即存储1个中文,byte数组长度为2,存储2个中文,byte数组长度为4。

以如下代码为例进行分析:

String str = "好"

好 对应的Unicode码二进制为0101100101111101,分别取出高8位和低8位,放入到byte数组中{01011001,01111101},这样就利用byte数组的2个元素保存了1个中文。

stickPicture.png
stickPicture.png

当字符串中存储了中英混合的内容时,1个英文字符同样会占用2个byte数组位置,例如下面代码底层byte数组的长度为16:

String str = "猴子monkey";

在获取字符串长度时,若存储的内容存在中文,是不能直接获取byte数组的长度作为字符串长度的,String源码中有向右移动1位的操作(即除以2),这样才能获取正确的字符串长度。

new String()底层源码

public String(char value[]) {
    this(value, 0, value.length, null);
}

String(char[] value, int off, int len, Void sig) {
    if (len == 0) {
        this.value = "".value;
        this.coder = "".coder;
        return;
    }
    if (COMPACT_STRINGS) {
        // 检查是否都是英文,是的话就进行压缩
        byte[] val = StringUTF16.compress(value, off, len);
        if (val != null) {// 不为null则表示都是英文字符
            this.value = val;
            // 将编码设置为LATIN1
            this.coder = LATIN1;
            return;
        }
    }
    // 到这里则表示有其他字符,无法压缩,因此编码设置为UTF16
    this.coder = UTF16;
    // 以两字节的方式来存储一个字符
    this.value = StringUTF16.toBytes(value, off, len);
}

@HotSpotIntrinsicCandidate
public static byte[] toBytes(char[] value, int off, int len) {
    // 由于有中文字符了,因此就需要用两个字节来存储一个字符了,那就吧byte数组扩大一倍
    byte[] val = newBytesFor(len);
    for (int i = 0; i < len; i++) {
        putChar(val, i, value[off]);
        off++;
    }
    return val;
}

// 其实就是将byte数组扩大一倍
public static byte[] newBytesFor(int len) {
    if (len < 0) {
        throw new NegativeArraySizeException();
    }
    if (len > MAX_LENGTH) {
        throw new OutOfMemoryError("UTF16 String size is " + len +
                                   ", should be less than " + MAX_LENGTH);
    }
    return new byte[len << 1];
}

// 两字节字符拆分
static void putChar(byte[] val, int index, int c) {
    assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
    index <<= 1;// 每个字符占两个字节,因此每次索引都得乘2
    val[index++] = (byte)(c >> HI_BYTE_SHIFT);// 取低八位
    val[index]   = (byte)(c >> LO_BYTE_SHIFT);// 取高八位
}

// 字符合并
static char getChar(byte[] val, int index) {
    assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
    index <<= 1;
    return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
                  ((val[index]   & 0xff) << LO_BYTE_SHIFT));
}

总结:如果字符串是纯英文,则编码默认为LATIN1,是可以压缩存储空间的。只要有中文,不管是否是中英混合,那么就都需要两个字节的byte数组来存储字符。

String字符串拼接"+"

代码:

class Demo {
    public static String concatIndy(int i) {
        return  "value " + i;
    }
}

编译查看字节码:

class Demo {
  Demo();
    Code:
       0: aload_0
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return

  public static java.lang.String concatIndy(int);
    Code:
       0: iload_0
       1: invokedynamic #2,  0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
       6: areturn
}

可以看到,编译后的字节码和 JDK 8 是不一样的,不再是基于StringBuilder实现,而是基于StringConcatFactory.makeConcatWithConstants动态生成一个方法来实现。这个会比StringBuilder更快,不需要创建StringBuilder对象,也会减少一次数组拷贝。

这里由于是内部使用的数组,所以用了UNSAFE.allocateUninitializedArray的方式更快分配byte[]数组。通过:StringConcatFactory.makeConcatWithConstants而不是JavaC生成代码,是因为生成的代码无法使用JDK的内部方法进行优化,还有就是,如果有算法变化,存量的Lib不需要重新编译,升级新版本JDk就能提速。

这个字节码相当如下手工调用:StringConcatFactory.makeConcatWithConstants

import java.lang.invoke.*;

static final MethodHandle STR_INT;

static {
    try {
        STR_INT = StringConcatFactory.makeConcatWithConstants(
            MethodHandles.lookup(),
            "concat_str_int",
            MethodType.methodType(String.class, int.class),
            "value \1"
        ).dynamicInvoker();
    } catch (Exception e) {
        throw new Error("Bootstrap error", e);
    }
}

static String concat_str_int(int value) throws Throwable {
    return (String) STR_INT.invokeExact(value);
}

StringConcatFactory.makeConcatWithConstants是公开API,可以用来动态生成字符串拼接的方法,除了编译器生成字节码调用,也可以直接调用。调用生成方法一次大约需要1微秒(千分之一毫秒)。

makeConcatWithConstants动态生成方法的代码

makeConcatWithConstants使用recipe ("value \1")动态生成的方法大致如下:

import java.lang.StringConcatHelper;
import static java.lang.StringConcatHelper.mix;
import static java.lang.StringConcatHelper.newArray;
import static java.lang.StringConcatHelper.prepend;
import static java.lang.StringConcatHelper.newString;

public static String invokeStatic(String str, int value) throws Throwable {
    long lengthCoder = 0;
    lengthCoder = mix(lengthCoder, str);
    lengthCoder = mix(lengthCoder, value);
    byte[] bytes = newArray(lengthCoder);
    lengthCoder = prepend(lengthCoder, bytes, value);
    lengthCoder = prepend(lengthCoder, bytes, str);
    return newString(bytes, lengthCoder);
}

StringConcatHelper是:StringConcatFactory.makeConcatWithConstants实现用到的内部类。


package java.lang;

class StringConcatHelper {
     static String newString(byte[] buf, long indexCoder) {
        // Use the private, non-copying constructor (unsafe!)
        if (indexCoder == LATIN1) {
            return new String(buf, String.LATIN1);
        } else if (indexCoder == UTF16) {
            return new String(buf, String.UTF16);
        }
    }
}


public class String {
    String(byte[] value, byte coder) {
        // 无拷贝构造
        this.value = value;
        this.coder = coder;
    }
}

可以看出,生成的方法是通过如下步骤来实现:

  1. StringConcatHelper的mix方法计算长度和字符编码 (将长度和coder组合放到一个long中);
  2. 根据长度和编码构造一个byte[];
  3. 然后把相关的值写入到byte[]中;
  4. 使用byte[]无拷贝的方式构造String对象。

上面的火焰图可以看到实现的细节。这样的实现,和使用StringBuilder相比,减少了StringBuilder以及StringBuilder内部byte[]对象的分配,可以减轻GC的负担。也能避免可能产生的StringBuilder在latin1编码到UTF16时的数组拷贝

  • StringBuilder缺省编码是LATIN1(ISO_8859_1),如果append过程中遇到UTF16编码,会有一个将LATIN1转换为UTF16的动作,这个动作实现的方法是inflate。如果拼接的参数如果是带中文的字符串,使用StringBuilder还会多一次数组拷贝。
class AbstractStringBuilder
    private void inflate() {
        if (!isLatin1()) {
            return;
        }
        byte[] buf = StringUTF16.newBytesFor(value.length);
        StringLatin1.inflate(value, 0, buf, 0, count);
        this.value = buf;
        this.coder = UTF16;
    }
}

@Deprecated注解的变化

该注解用于标识废弃的内容,在jdk9中新增了2个内容:

  • String since() default “”:标识是从哪个版本开始废弃

  • boolean forRemoval() default false:如果为true,标识该废弃的内容会在未来的某个版本中移除

模块化

Java8中有个非常重要的包rt.jar,里面涵盖了Java提供的类文件,在程序员运行Java程序时jvm会加载rt.jar。这里有个问题是rt.jar中的某些文件我们是不会使用的,比如使用Java开发服务器端程序的时候通常用不到图形化界面的库Java.awt ,这就造成了内存的浪费。

Java9中将rt.jar分成了不同的模块,一个模块下可以包含多个包,模块之间存在着依赖关系,其中Java.base模块是基础模块,不依赖其他模块。上面提到的Java.awt被放到了其他模块下,这样在不使用这个模块的时候就无需让jvm加载,减少内存浪费。让jvm加载程序的必要模块,并非全部模块,达到了瘦身效果。

stickPicture.png
stickPicture.png

jar包中含有.class文件,配置文件。jmod除了包含这两个文件之外,还有native library,legal licenses等,两者主要的区别是jmod主要用在编译期和链接期,并非运行期,因此对于很多开发者来说,在运行期仍然需要使用jar包。

模块化的优点:

  • 精简jvm加载的class类,提升加载速度

  • 对包更精细的控制,提高安全

模块与包类似,只不过一个模块下可以包含多个包。下面举例来看下

  • 项目----公司

  • 模块----部门:开发部,测试部

  • 包名----小组:开发1组,开发2组

  • 类 ----员工:张三,李四

接下来演示在测试部模块中调用开发部模块里面的类。

  1. 创建项目,项目下分别创建开发部(命名develop),测试部(命名test)2个模块
  2. 在开发部中创建下面2个包和类
//dev1包
package com.seven97.dev1;

public class Cat {
    public void eat() {
        System.out.println("吃鱼");
    }
}

//dev2包
package com.seven97.dev2;

public class Apple {
    private String name;
}
  1. 在src目录下创建module-info.Java文件,通过module-info.Java的文件来描述模块,开发部模块要将com.seven97.dev1包导出,在src目录下创建module-info.Java
module develop {//develop名字跟模块名一致
    exports com.seven97.dev1;//导出的包,该包下的类可以被其他模块访问
    opens com.seven97.dev2;//导出包,该包下的类可以被其他模块通过反射访问
}
  1. 在测试部模块的src目录下创建module-info.Java
module test {// test名字跟模块名字一致
    requires develop;// 导入develop模块
}
  1. 在测试部模块中创建
package com.seven97.test1;

import com.seven97.dev1.Cat;


public class CatTest {
    public static void main(String[] args) throws Exception {
        Cat cat = new Cat();
        cat.eat();
        
        //反射
        Class<?> clazz = Class.forName("com.seven97.dev2.Apple");
        Object obj = clazz.getDeclaredConstructor().newInstance();
    }
}

上面例子演示了不同模块下类的使用,主要是依赖module-info.Java文件描述了模块的关系。

jshell

在一些编程语言中,例如:python,Ruby等,都提供了REPL(Read Eval Print Loop 简单的交互式编程环境)。jshell就是Java语言平台中的REPL。

有的时候只是想写一段简单的代码,例如HelloWorld,按照以前的方式,还需要自己创建Java文件,创建class,编写main方法,但实际上里面的代码其实就是一个打印语句,此时还是比较麻烦的。在jdk9中新增了jshell工具,可以快速的运行一些简单的代码。

从命令提示符里面输入jshell,进入到jshell之后输入:

image.png
image.png

如果要退出jshell的话,输入/exit即可。

弃用class.newInstance()

image.png
image.png

官方说明:class.newInstance()方法传播由空构造函数引发的任何异常,包括已检查的异常。这种方法的使用有效地绕过了编译时异常检查,否则该检查将由编译器执行。

clazz.newInstance()方法由clazz.getDeclaredConstructor().newInstance()方法代替,该方法通过将构造函数抛出的任何异常包装在(InvocationTargetException)中来避免此问题。

也就是说使用class.newInstance()方法时由默认构造函数中抛出的异常,此方法检查不到,如下例子:

public class ClassInstanceTest {

    public ClassInstanceTest() throws IOException {
        System.out.println("无参构造");
        throw new IOException();
    }

    public static void main(String[] args) {
        try {
            ClassInstanceTest classInstanceTest = ClassInstanceTest.class.newInstance();
        } catch (InstantiationException e) {
            System.out.println("InstantiationException");
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            System.out.println("IllegalAccessException");
            e.printStackTrace();
        }
    }
}

输出:

无参构造
Exception in thread "main" java.io.IOException
    at ClassInstanceTest.<init>(ClassInstanceTest.java:9)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
    at java.base/java.lang.reflect.ReflectAccess.newInstance(ReflectAccess.java:166)
    at java.base/jdk.internal.reflect.ReflectionFactory.newInstance(ReflectionFactory.java:404)
    at java.base/java.lang.Class.newInstance(Class.java:590)
    at ClassInstanceTest.main(ClassInstanceTest.java:14)

可以看到,没有被异常检查捕获到

接下来使用clazz.getDeclaredConstructor().newInstance()方法:

public class ClassInstanceTest {

    public ClassInstanceTest() throws IOException {
        System.out.println("无参构造");
        throw new IOException();
    }

    public static void main(String[] args) {
        try {
            Class clazz = ClassInstanceTest.class;
            clazz.getDeclaredConstructor().newInstance();
        } catch (InstantiationException e) {
            System.out.println("InstantiationException");
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            System.out.println("IllegalAccessException");
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            System.out.println("InvocationTargetException");
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            System.out.println("NoSuchMethodException");
            e.printStackTrace();
        }
    }
}

 输出:
 
 无参构造
InvocationTargetException
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
    at ClassInstanceTest.main(ClassInstanceTest.java:15)
Caused by: java.io.IOException
    at ClassInstanceTest.<init>(ClassInstanceTest.java:9)
    ... 6 more

可以看到异常被捕获到了

弃用finalized()方法

在 Java 中,finalize() 方法曾是对象生命周期的一部分,允许在垃圾收集器准备回收对象之前执行一些清理操作。然而,随着时间的推移,finalize() 方法逐渐暴露出一些问题,包括性能开销、不确定的行为、死锁风险以及增加了垃圾回收的复杂性。

弃用原因

  • 性能问题:该方法的执行会增加垃圾回收的停顿时间,因为 JVM 必须等待对象的 finalize() 方法执行完毕才能回收对象。这对于需要低延迟和高吞吐量的应用来说可能会产生性能问题

  • 不确定性:执行时间是不确定的,因为它依赖于垃圾回收器的运行时机,而垃圾回收器的运行时机是不可预测的。这导致依赖 finalize() 进行资源清理的代码很难编写和调试

  • 死锁风险:如果 finalize() 方法在执行过程中访问了其他对象,而这些对象又恰好正在被垃圾回收,那么就可能发生死锁

  • 安全问题:可以被恶意代码利用来破坏系统的安全性。例如,恶意代码可以覆盖对象的 finalize() 方法,在对象被垃圾回收时执行恶意操作

因此,基于以上原因,它在 Java 9 中被标记为弃用(deprecated)

替代方案

try-catch-resources

try-catch-resources 自动关闭实现了 AutoCloseable 接口的资源。这是一个优雅且安全的方式来管理资源,如文件、网络连接。

InputStream 实现了 AutoCloseable 接口,因此它可以在 try 结束时自动关闭,无需显式调用 close() 方法,源码如下:

public abstract class InputStream implements Closeable {
    // 只展示结构,具体内容省略
}

public interface Closeable extends AutoCloseable {

    /**
     * Closes this stream and releases any system resources associated
     * with it. If the stream is already closed then invoking this
     * method has no effect.
     *
     * <p> As noted in {@link AutoCloseable#close()}, cases where the
     * close may fail require careful attention. It is strongly advised
     * to relinquish the underlying resources and to internally
     * <em>mark</em> the {@code Closeable} as closed, prior to throwing
     * the {@code IOException}.
     *
     * @throws IOException if an I/O error occurs
     */
    public void close() throws IOException;
}

Cleaner

Java 9 引入了 Cleaner 类作为替代方案。允许注册一个回调,当对象变得幻象可达(phantom reachable)时,这个回调会被执行。与 finalize() 不同,Cleaner 不会延迟对象的回收,并且它的执行是异步的。

demo如下:

import java.lang.ref.Cleaner;
import java.lang.ref.Cleaner.Cleanable;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

public class CleanerDemo {

    // 模拟需要清理资源的类
    static class ManagedResource {
        private final AtomicBoolean isClosed = new AtomicBoolean(false);

        public void close() {
            if (isClosed.compareAndSet(false, true)) {
                System.out.println("资源已被清理。");
            }
        }

        public boolean isClosed() {
            return isClosed.get();
        }
    }

    // 模拟持有需要清理资源的类,并使用 Cleaner 进行清理
    static class ResourceHolder implements AutoCloseable {
        private final ManagedResource resource = new ManagedResource();
        private final Cleanable cleanable;

        public ResourceHolder() {
            this.cleanable = Cleaner.create().register(Executors.defaultThreadFactory(), () -> {
                System.out.println("Cleaner 正在执行清理...");
                resource.close();
            });
        }

        @Override
        public void close() {
            // 手动触发清理
            cleanable.clean(); 
        }

        public boolean isResourceClosed() {
            return resource.isClosed();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ResourceHolder holder = new ResourceHolder();

        // 这里是使用资源逻辑

        // 模拟应用程序逻辑结束,释放资源
        holder.close();

        // 检查资源是否已被清理
        System.out.println("资源是否已关闭: " + holder.isResourceClosed());

        // 强制进行垃圾回收以演示 Cleaner 的工作
        System.gc();
        // 等待一段时间以便 Cleaner 有机会执行(这只是示例,实际执行时间不确定)
        Thread.sleep(1000); 
    }
}

//输出
Cleaner 正在执行清理...
资源已被清理。
资源是否已关闭: true

在上述例子中,手动调用了 holder.close(),因此 Cleaner 的回调实际上不会被触发,因为资源已经在 Cleaner 有机会介入之前被清理了。如果注释掉 holder.close() 的调用,那么当 holder 对象变得幻象可达时,Cleaner 的回调可能会被触发(但这仍是不确定的)

因此使用Cleaner 进行资源管理一般也不是最佳实践。Cleaner 主要用于在对象被垃圾回收时执行一些额外的、不是很关键的清理任务

seven97官方微信公众号
seven97官方微信公众号