字符串见闻

184 阅读4分钟

前天晚上,我在一个秋招的交流群里看到了一个群友发的一个问题,顿时就引起来我的兴趣,是这段代码

public class StaticTest1 {
    public static final String A;
    public static final String B;

    static {
        A = "ab";
        B = "cd";
    }

    public static void main(String[] args) {
        String s = A + B;
        String t = "abcd";

        if (s == t) {
            System.out.println("s等于t,它们是同一个对象");
        } else {
            System.out.println("s不等于t,它们不是同一个对象");
        }
    }
}

看到后,我立即把代码敲到了IDE里面,运行之前,我想了一下,被static和final同时修饰的字符串需要在编译时候就要被确定下来,正好在static代码块中有两句赋值语句,所以字符串s和字符串t应该指向同一个实例对象。分析完毕,信心十足地运行了代码,结果却是

看到之后,竟然和分析的不一样,突然间没了辙。
这时候看到一个群友说代码修改一下,于是改成了下面这个样子

public class StaticTest2 {
    public static final String A = "ab";
    public static final String B = "cd";
    static {}

    public static void main(String[] args) {
        String s = A + B;
        String t = "abcd";

        if (s == t) {
            System.out.println("s等于t,它们是同一个对象");
        } else {
            System.out.println("s不等于t,它们不是同一个对象");
        }
    }
}

区别很明显,字符串A和B在声明的时候就已经被赋值了,按道理来说,这个应该和之前的分析一致,这个结果应该是s和t是同一个对象。运行之后,果真,控制台窗口出现了“s等于t,它们是同一个对象”字样。

这个就很奇怪,第一次的运行结果没有按照分析来走。想一想,肯定是分析有差错,但是这些字面上的代码怎么能分析出来呢,现在要的方法只能是将这些文件编译后得到的class文件反编译一下,来看看JVM是怎么处理这些代码的。

经过努力,在我浩如烟海的软件文件夹中找到了jad,这是个命令行工具,将jad.exe文件配置到环境变量里面后就可以直接使用了。

首先,我们要得到这两个文件对应的class文件,要运用到javac命令。这里注意一下,如果代码中有中文字符,命令行里编译可能会报错,javac命令要带上-encoding utf-8参数。

经过处理,我们得到了反编译后的两个文件StaticTest1.jad和StaticTest2.jad。我们可以直接用文本编辑器打开这个两个文件,来进行比较。

下面展示的主要的区别地方

从上图反编译结果来看,对于StaticTest1.java来说,如果仅仅是声明出变量,没有直接赋值,在static代码块中赋值,这样的方式在底层是创建一个StringBuilder对象,然后两个在static代码块中的变量调用append方法添加到StringBuilder对象中,最后调用toSting()方法。重点就在于这个toString()方法,进入到toString()方法的源码中,我们可以看到底层是创建了一个新的对象,于是,这就顺理成章地变成了另外一个对象。

对于StaticTest2.java来说,这个正如我最开始说的那样,JVM会在编译时期就将A+B的值优化为“abcd",这是因为A和B在程序运行就存到了class文件的常量池中,编译器就可以直接从常量池中取出变量来做优化。放在静态代码块里面就已经不是常量,它在编译的时候不会出现在class文件的常量池里面。

class文件头部有一个constant pool,里面存着编译器生成的各种字面量常量,可以通过javap命令查看class文件的字面量。

两个class字节码是不一样的。StaticTest1.class比StaticTest2.class多了许多字面量,这也验证了前面的底层代码的实现更加复杂了一些。

这次对代码的剖析自己感觉还是没有完全说明白为什么JVM会这样子处理,对JVM也了解的不够深入,不过这次群友的热烈讨论还是让自己学到了很多知识,在这里谢谢大家。