java常量池以及“==”字符串比较

时间:2021-02-10 20:01:33

  对于字符串来说,对象的引用存储于栈中,编译期已确定的(直接用双引号定义的)存储在常量池中,任何用new创建的字符串对象(运行期产生)存储于堆中。

  String s1="Hello"; //对象的引用s1存储在栈中,"Hello"存储在常量池中;

  String s2=new String("world");//对象的引用s2存储在栈中,"world"首先会在常量池中创建,当new执行时,在堆中创建新的对象,将常量池中的"world"复制到堆中。

  new我们比较好理解,每当用new创建对象时,不管这些对象的内容是不是一样的,都会在堆中就会开辟一个新的内存空间。也就是说这些对象的空间是不共享的(关于共享的概念在下面常量池中会解释的更加清楚)。下面来讲解常量池的存储原理。

  常量池的来源
  JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存就是常量池。

  java中的常量池技术
  为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个,有就直接取出来,不会重复创建),则在需要重复创建相等变量时节省了很多时间。
可以想象将各种字符串存放在公共的存储池中,字符串变量指向存储池中相应的位置,如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。

  工作原理

  当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象,则返回这个对象的引用,否则新的字符串对象被创建,然后返回这个新对象的引用。

 首先说明一点,在java中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。


看下面的例子

public class StringDemo1 {
    public static void main(String[] args){
        String s1="Hello";
        String s2="Hello";    //"Hello"字符串对象已存在,不会重新创建新的字符串对象
        System.out.println(s1==s2);            //true

        String s3=new String("Hello");
        String s4=new String("Hello")
        System.out.println(s3==s4);            //false

        System.out.println(s1==s3);            //false
    }
}

  很明显,当我们使用了new来构造字符串对象的时候,不管字符串常量池中有没有相同内容的对象的引用,新的字符串对象都会创建。

  关于java的连接表达式"+",看这个例子:
public class StringDemo2 {
    public static void main(String[] args){
        String s1="Hello";
        String s2="World";

        String s3="HelloWorld";
        String s4=s1+s2;
        System.out.println(s3==s4);          //false

        String s5="Hello"+"World";
        System.out.println(s3==s5);          //true
    }
}
  s5虽然是动态拼接出来的字符串,但是所有参与拼接的部分s1,s2都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接拼好.因此String s5="Hello"+"World";在class文件中被优化成String s5 = "HelloWorld"; (只有字面量的拼接才会加入到常量池中,我是这么理解的)。因此s3==s5的结果并不出乎意料。
  那如何理解s3==s4返回false呢?
  String s4=s1+s2实质相当于String s4=(new StringBuilder()).append(s1).append(s2).toString()
对于所有包含new方式新建对象(包括null)的"+"连接表达式,它所产生的新对象都不会被加入字符串池中。当然这只是比较浅显的理解, 虽然s1、s2在赋值的时候使用的字符串字面量,但是拼接成s4的时候,s1、s2作为两个变量,都是不可预料的,所以不做优化。等到运行时,s1和s2拼接的字符串存放在堆中地址不确定,不可能与常量池中的"HelloWorld"地址相同。

  因此下面的程序应该会更加好理解:
public class StringDemo3 {
    public static final String s1="Hello";
    public static final String s2="World";
    public static void main(String[] args){
        String s3="HelloWorld";
        String s4=s1+s2;
        System.out.println(s3==s4);      //true
    }
}
  s1,s2定义为常量已赋值在编译时就存放进常量池中了,因此s4也固定了。如果 关于java程序编译和运行的过程不太明白,参考文章:java程序编译和运行过程
  这么看来,好像返回的结果取决于s1与s2是常量还是变量,以及它们何时被赋值。
  之前还看到一个更有意思的程序:
public class StringDemo4 {
    public static final String s1;
    public static final String s2;

    static{
        s1="Hello";
        s2="World";
    }

    public static void main(String[] args){
        String s3="HelloWorld";
        String s4=s1+s2;
        System.out.println(s3==s4);
    }
}
  关于执行结果有兴趣的同胞可以自己尝试一下。
  实际上java的6种基本类型的包装类都实现了常量池技术(Byte,Short,Integer,Long,Character,Boolean),其他两种浮点数类型的包装类并没有实现常量池技术(Float,Double)。这个可以通过程序验证下。