当前位置: 首页 > 科技观察

一篇文章理解Strings=-a-+-b-+-c-,创建了多少个对象?

时间:2023-03-20 13:45:58 科技观察

先看看这个常见的面试题。在下面的代码中,将创建多少个字符串对象?字符串=“a”+“b”+“c”;如果将Java源代码与反编译后的Bytecode文件进行对比,可以直观地看出答案,只创建了一个String对象。估计大家都会有疑问。为什么源码中字符串拼接的操作编译后就消失了,拼接后直接呈现为一个完整的字符串?这是因为在编译期间,应用了编译器优化。一种称为常量折叠(ConstantFolding)的技术在编译过程中折叠编译期常量的加减乘除运算。通过语法分析,编译器会对常量表达式进行计算和求值,并将表达式替换为计算出的值,而不必等到运行时才进行计算处理,从而节省了运行时的处理器资源。上面提到的编译时常量的特点是它的值可以在编译时确定,只有完全满足以下条件才能成为编译时常量:声明为final基本类型或字符串类型。它已经使用常量表达式进行了初始化和初始化。上面的前两项比较容易理解。第三项和第四项需要注意,如下例所示:finalStrings1="hello"+"Hydra";finalStrings2=UUID.randomUUID().toString()+"Hydra";编译器可以在编译时获取s1的值为helloHydra,而不用等到程序运行,所以s1是一个编译时常量。对于s2,虽然也被声明为final类型,并且在声明的时候已经进行了初始化,但是它没有使用常量表达式,所以不属于编译时常量。这种常量称为运行时常量。再看编译后的字节码文件中的常量池区:可以看到常量池中只有一个String常量helloHydra,s2对应的字符串常量不在该区。对于编译器来说,运行时常量在编译过程中是不能折叠的,编译器只会在试图修改它的时候报错。另外值得一提的是,编译时常量和运行时常量的另一个区别是类是否需要初始化。下面两个例子进行了比较:}运行上面的代码,输出:initclass1如果修改上面的,在变量a中加入final进行修改:publicstaticfinalinta=1;再次执行上面的代码,输出结果为:1.可以看到最后加上最后的修改后,两次运行的结果是不一样的。这是因为加了final之后,变量a就变成了编译时常量。将导致类被初始化。另外,在声明编译器常量时,final关键字是必须的,而static关键字不是必须的。上面的静态修改只是验证类是否已经初始化。让我们再看几个例子来加深对final关键字的理解,运行下面的代码:Strings2=h2+"Hydra";System.out.println((s1=="helloHydra"));System.out.println((s2=="helloHydra"));}执行结果:字符串h1和h2都被使用truefalse代码中的常量赋值,区别在于是否使用final修饰。对比编译后的代码,s1被折叠了,s2没有,可以证实上面final修饰的字符串变量是编译时常量的理论。再看一段代码,执行下面的程序,会返回什么结果?publicstaticvoidmain(String[]args){Stringh="hello";finalStringh2=h;Strings=h2+"Hydra";System.out.println(s=="helloHydra");}答案是错误的,因为虽然字符串h2在这里finalized,初始化的时候不是编译时常量,所以不是编译时常量。在上面的一些例子中,在执行常量折叠的过程中遵循了使用常量表达式进行初始化的原则。说到这里,可能有同学会有疑惑。什么可以被视为常量表达式?在Oracle官网的文档中,列举了很多情况。下面列出常见的情况(除了下面的,官方文档还列出了很多情况,有兴趣的可以自己查):基本类型和String类型的字面量值以及类型的强制类型转换字符串类型使用一元运算符(不包括++和--)计算,例如+或-或!使用加减运算符+、-和乘除运算符*、/、%进行计算使用移位运算符>>、<<、>>>进行移位运算...文字是用于表示固定值的符号在源码中,在Java中创建对象时需要用到new关键字,但是给基本类型变量赋值时就不需要用new关键字了,这个方法可以调用字面量。Java中的字面量主要包括以下几种类型的字面量://整型字面量:longl=1L;inti=1;//浮点型字面量:floatf=11.1f;doubled=11.1;//字符和字符字符串类型字面量:charc='h';Strings="Hydra";//布尔型字面量:booleanb=true;当我们在代码中定义和初始化一个字符串对象时,程序会在常量池(constantpool)中缓存字符串的字面值。如果后续代码再次使用字符串的字面值,则会直接使用常量池中的字符串字面值。此外,还有一种特殊的空类型字面量。这种类型的文字中只有一个为空。这个字面量可以赋值给任何引用类型的变量,表示这个引用类型变量存储的地址为空。也就是说,它还没有指向任何有效的对象。那么,如果不使用常量表达式进行初始化,而在变量初始化过程中又引入了其他变量(且未被final修饰),编译器将如何处理呢?让我们看另一个例子:publicstaticvoidmain(String[]args){Strings1="a";Strings2=s1+"b";Strings3="a"+"b";System.out.println(s2=="ab");System.out.println(s3=="ab");}结果打印:falsetrue为什么会出现不同的结果?在Java中,使用==比较String类型时,判断引用是否指向堆内存中的同一个地址。如果出现上面的结果那么说明它指向的不是内存中的同一个块地址。通过前面的分析,我们知道s3会进行常量折叠,并引用常量池中的ab,所以它们是相等的。拼接字符串s2时,表达式中引用了其他对象,不是编译时常量,所以不能折叠。那么,在没有常量折叠的情况下,为什么最后会返回false呢?我们来看看编译器是如何实现这种情况的,首先执行如下代码:publicstaticvoidmain(String[]args){Strings1="my";Strings2="name";Strings3="is";Strings4="Hydra";Strings=s1+s2+s3+s4;}然后使用javap反编译字节码文件,可以看到在这个过程中,编译器也会进行优化:可以看到,虽然我们在代码,在字符串拼接场景下,Java编译器会自动优化,新建一个StringBuilder对象,然后调用append方法进行字符串拼接。最后调用StringBuilder的toString方法生成一个新的字符串对象,而不是引用常量池中的常量。这样也可以解释为什么在上面的例子中,s2=="ab"会返回false。【小编推荐】一篇文章带你了解内存中的Slice。一篇文章带您了解Containerd的使用。在云中执行灾难恢复的五种有效方法。为什么应该选择TypeScript而不是JavaScript。一篇文章带你看懂Python基本条件语句