首先看一下这道常见的面试题,上面代码中,会创立几个字符串对象?
String s="a"+"b"+"c";
如果你比拟一下Java源代码和反编译后的字节码文件,就能够直观的看到答案,只创立了一个String对象。

预计大家会有疑难了,为什么源代码中字符串拼接的操作,在编译实现后会隐没,间接出现为一个拼接后的残缺字符串呢?

这是因为在编译期间,利用了编译器优化中一种被称为常量折叠(Constant Folding)的技术,会将编译期常量的加减乘除的运算过程在编译过程中折叠。编译器通过语法分析,会将常量表达式计算求值,并用求出的值来替换表达式,而不用等到运行期间再进行运算解决,从而在运行期间节俭处理器资源。

而上边提到的编译期常量的特点就是它的值在编译期就能够确定,并且须要残缺满足上面的要求,才可能是一个编译期常量:

  • 被申明为final
  • 根本类型或者字符串类型
  • 申明时就曾经初始化
  • 应用常量表达式进行初始化
    下面的前两条比拟容易了解,须要留神的是第三和第四条,通过上面的例子进行阐明:
final String s1="hello "+"Hydra";final String s2=UUID.randomUUID().toString()+"Hydra";

编译器可能在编译期就失去s1的值是hello Hydra,不须要等到程序的运行期间,因而s1属于编译期常量。而对s2来说,尽管也被申明为final类型,并且在申明时就曾经初始化,但应用的不是常量表达式,因而不属于编译期常量,这一类型的常量被称为运行时常量。再看一下编译后的字节码文件中的常量池区域:

能够看到常量池中只有一个String类型的常量hello Hydra,而s2对应的字符串常量则不在此区域。对编译器来说,运行时常量在编译期间无奈进行折叠,编译器只会对尝试批改它的操作进行报错解决。

另外值得一提的是,编译期常量与运行时常量的另一个不同就是是否须要对类进行初始化,上面通过两个例子进行比照:

public class IntTest1 {    public static void main(String[] args) {        System.out.println(a1.a);    }}class a1{    static {        System.out.println("init class");    }    public static int a=1;}

运行下面的代码,输入:

init class1

如果对下面进行批改,对变量a增加final进行润饰:
public static final int a=1;
再次执行下面的代码,会输入:
1
能够看到在增加了final润饰后,两次运行的后果是不同的,这是因为在增加final后,变量a成为了编译期常量,不会导致类的初始化。另外,在申明编译器常量时,final关键字是必要的,而static关键字是非必要的,下面加static润饰只是为了验证类是否被初始化过。

咱们再看几个例子来加深对final关键字的了解,运行上面的代码:

public static void main(String[] args) {    final String h1 = "hello";    String h2 = "hello";    String s1 = h1 + "Hydra";    String s2 = h2 + "Hydra";    System.out.println((s1 == "helloHydra"));    System.out.println((s2 == "helloHydra"));}

执行后果:
true
false
代码中字符串h1和h2都应用常量赋值,区别在于是否应用了final进行润饰,比照编译后的代码,s1进行了折叠而s2没有,能够印证下面的实践,final润饰的字符串变量属于编译期常量。

再看一段代码,执行上面的程序,后果会返回什么呢?

public static void main(String[] args) {    String h ="hello";    final String h2 = h;    String s = h2 + "Hydra";    System.out.println(s=="helloHydra");}

答案是false,因为尽管这里字符串h2被final润饰,然而初始化时没有应用编译期常量,因而它也不是编译期常量。

在下面的一些例子中,在执行常量折叠的过程中都遵循了应用常量表达式进行初始化这一准则,这里可能有的同学还会有疑难,到底什么样能力算得上是常量表达式呢?在Oracle官网的文档中,列举了很多种状况,上面对常见的状况进行列举(除了上面这些之外官网文档上还列举了不少状况,如果有趣味的话,能够本人查看):

根本类型和String类型的字面量
根本类型和String类型的强制类型转换
应用+或-或!等一元运算符(不包含++和--)进行计算
应用加减运算符+、-,乘除运算符*、 / 、% 进行计算
应用移位运算符 >>、 <<、 >>>进行位移操作
……
字面量(literals)是用于表白源代码中一个固定值的表示法,在Java中创立一个对象时须要应用new关键字,然而给一个根本类型变量赋值时不须要应用new关键字,这种形式就能够被称为字面量。Java中字面量次要包含了以下类型的字面量:

//整数型字面量:long l=1L;int i=1;//浮点类型字面量:float f=11.1f;double d=11.1;//字符和字符串类型字面量:char c='h';String s="Hydra";//布尔类型字面量:boolean b=true;

当咱们在代码中定义并初始化一个字符串对象后,程序会在常量池(constant pool)中缓存该字符串的字面量,如果前面的代码再次用到这个字符串的字面量,会间接应用常量池中的字符串字面量。

除此之外,还有一类比拟非凡的null类型字面量,这个类型的字面量只有一个就是null,这个字面量能够赋值给任意援用类型的变量,示意这个援用类型变量中保留的地址为空,也就是还没有指向任何无效的对象。

那么,如果不是应用的常量表达式进行初始化,在变量的初始化过程中引入了其余变量(且没有被final润饰)的话,编译器会怎么进行解决呢?咱们上面再看一个例子:
public static void main(String[] args) {

String s1="a";String s2=s1+"b";String s3="a"+"b";System.out.println(s2=="ab");System.out.println(s3=="ab");

}
后果打印:
false
true
为什么会呈现不同的后果?在Java中,String类型在应用==进行比拟时,是判断的援用是否指向堆内存中的同一块地址,呈现下面的后果那么阐明指向的不是内存中的同一块地址。

通过之前的剖析,咱们晓得s3会进行常量折叠,援用的是常量池中的ab,所以相等。而字符串s2在进行拼接时,表达式中援用了其余对象,不属于编译期常量,因而不能进行折叠。

那么,在没有常量折叠的状况下,为什么最初返回的是false呢?咱们看一下这种状况下,编译器是如何实现,先执行上面的代码:

public static void main(String[] args) {

String s1="my ";String s2="name ";String s3="is ";String s4="Hydra";String s=s1+s2+s3+s4;

}

而后应用javap对字节码文件进行反编译,能够看到在这一过程中,编译器同样会进行优化:

能够看到,尽管咱们在代码中没有显示的调用StringBuilder,然而在字符串拼接的场景下,Java编译器会主动进行优化,新建一个StringBuilder对象,而后调用append办法进行字符串的拼接。而在最初,调用了StringBuilder的toString办法,生成了一个新的字符串对象,而不是援用的常量池中的常量。这样,也就能解释为什么在下面的例子中,s2=="ab"会返回false了。

本文代码基于Java 1.8.0_261-b12 版本测试