当前位置: 首页 > 后端技术 > Java

Strings=-a-+-b-+-c-,创建了多少个对象?

时间:2023-04-01 22:57:30 Java

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