谈谈Java字符串创建几个对象的问题

620 阅读8分钟

老掉牙的问题,网上搜了一圈,还是没看到合理清晰的回答,决定自己记一下这个问题的理解。

1. 首先看一下常见的代码

        String s1 = "hello";
        String s2 = new String("hello");
        String s3 = new String("hello");
        String s4 = new String("world");

上述代码分析:

第一行在常量池创建了一个对象"hello",然后返回常量池中"hello"的地址给s1变量;

第二行在堆中创建一块内存,将该内存地址返回给s2变量。紧接着,会去常量池中找是否有“hello”字符串,如果没有就在常量池分配一块空间存放"hello",然后在堆中创建一个常量池中此"hello"对象的拷贝对象;如果有就直接在堆中创建一个new出来的"hello"对象。在本例中,由于常量池已经存在了"hello",所以这行代码只创建一个对象,即堆上new出来的对象;

第三行在堆中创建一块内存,将该内存地址返回给s3变量。该内存存放一个new出来的"hello"对象;

第四行在堆中创建一块内存,将该内存地址返回给s4变量。紧接着,发现常量池中没有"world"字符串,于是在常量池分配一块空间存放"world",然后在堆中创建一个常量池中此"world"对象的拷贝对象。因此,这行代码创建了2个对象。

上述解释可以参考下图:

20210902210646704.png

为了验证上述事实,我们打印如下代码的值:

/**
 * @author huang
 * @date 2021年9月2日 
 */
@SuppressWarnings("restriction")		//取消显示的警告集
public class ObjectsAddressDemo {
 
    static final Unsafe unsafe = getUnsafe();
    static final boolean is64bit = true; // auto detect if possible.
 
    public static void main(String... args) {
        String s1 = "hello";
        String s2 = new String("hello");
        String s3 = new String("hello");
        String s4 = new String("world");
        System.out.println("----s1的信息如下----");
        print(s1);
        System.out.println("----s2的信息如下----");
        print(s2);
        System.out.println("----s3的信息如下----");
        print(s3);
        System.out.println("----s4的信息如下----");
        print(s4);
 
    }
 
    private static void print(String a) {
        // hashcode
        System.out.println("Hashcode :       " + a.hashCode());
        System.out.println("identityHashcode :       " + System.identityHashCode(a));
        //System.out.println("Hashcode (HEX) : " + Integer.toHexString(a.hashCode()));// Integer.toHexString(int)是将一个整型转成一个十六进制数
 
        // toString
        System.out.println("toString :       " + String.valueOf(a));
 
        //通过sun.misc.Unsafe;
        printAddresses("Address", a);
    }
 
    @SuppressWarnings("deprecation")
    public static void printAddresses(String label, Object... objects) {
        System.out.print(label + ":         0x");
        long last = 0;
        int offset = unsafe.arrayBaseOffset(objects.getClass());
        int scale = unsafe.arrayIndexScale(objects.getClass());
        switch (scale) {
            case 4:
                long factor = is64bit ? 8 : 1;
                final long i1 = (unsafe.getInt(objects, offset) & 0xFFFFFFFFL) * factor;
                System.out.print(Long.toHexString(i1));
                last = i1;
                for (int i = 1; i < objects.length; i++) {
                    final long i2 = (unsafe.getInt(objects, offset + i * 4) & 0xFFFFFFFFL) * factor;
                    if (i2 > last)
                        System.out.print(", +" + Long.toHexString(i2 - last));
                    else
                        System.out.print(", -" + Long.toHexString(last - i2));
                    last = i2;
                }
                break;
            case 8:
                throw new AssertionError("Not supported");
        }
        System.out.println();
    }
 
    private static Unsafe getUnsafe() {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}

可以发现,只要字符串new了一个对象,哪怕值一样,其地址也不一样。同时也看出了hashcode跟真实的地址值也不一样。

2. 字符串的拼接

        String str1 = "ab";
        String str2 = "cd";
        String str3 = "abcd";
        String str4 = "ab" + "cd";
        String str5 =  str1 + "cd";
        String str6 = "ab" + str2;
        String str7 = str1 + str2;

思考一下,上述7个字符串的地址值有哪些是相同的呢?同样地,打印结果如下:

-----str1的信息如下----
Hashcode :       3105
identityHashcode :       1163157884
toString :       ab
Address:         0x71695ff20
-----str2的信息如下----
Hashcode :       3169
identityHashcode :       1956725890
toString :       cd
Address:         0x71695ff50
-----str3的信息如下----
Hashcode :       2987074
identityHashcode :       356573597
toString :       abcd
Address:         0x71695ff80
-----str4的信息如下----
Hashcode :       2987074
identityHashcode :       356573597
toString :       abcd
Address:         0x71695ff80
-----str5的信息如下----
Hashcode :       2987074
identityHashcode :       1735600054
toString :       abcd
Address:         0x7169601b8
-----str6的信息如下----
Hashcode :       2987074
identityHashcode :       21685669
toString :       abcd
Address:         0x716960230
-----str7的信息如下----
Hashcode :       2987074
identityHashcode :       2133927002
toString :       abcd
Address:         0x7169602a8

发现只有str3和str4的地址值是相同的,原因如下:

  1. 当+号一边存在字符串变量时,则会在堆中创建一个内容为该拼接字符串的对象;
  2. 当+号两边都是字符串常量时,则先会寻找字符串常量池中是否存在已经拼接好的字符串。如果不存在,则会在里面创建一个新的字符串,不会在堆中创建新的对象。最后将这常量池中的字符串地址赋给“=”左边的字符串变量.

这里值得注意的是,只有使用引号包含文本的方式创建的字符串对象之间使用“+”连接产生的新对象才会被加入字符串常量池中。对于所有包含new方式新建对象(包括null)和变量形式 的“+”连接表达式,它所产生的新对象都不会被加入字符串池中

这也很好理解,因为常量池是为了提高效率而设置的,如果每连接两个变量字符串都要在常量池中创建一份的话,那常量池的容量大小岂不是要爆了,毕竟字符串的组合可以千变万化。考虑到这一点,所以常量池只创建文本常量对象。

3. intern()用法

intern()的用法在jdk1.6之前和jdk1.7之后略有不同,这里通俗的讲一讲。先来介绍一下intern()的作用,介绍之前回顾一下上面所讲的字符串定义和拼接在内存中的对象创建问题,引入如下两个问题:

  1. 字符串在用new定义时,如果常量池中有该对象的内容值,那么是不是浪费内存了,因为new定义的时候会在堆上创建一个对象,该对象和常量池中的那个是一样的内容。
  2. 字符串在变量拼接的时候,如果常量池中也有拼接好的内容值,那么是不是也造成了内存浪费,原因是变量拼接时会在堆上创建一个同样内容值的对象。 如果字符串比较多,那上述两个问题引起的内存就不能不轻视了。为此,java提供了一个解决方案,即intern()方法。intern()方法有啥用呢,看如下代码:
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

我们来逐行解释上面的代码:

  1. String s = new String("1"),在常量池中和堆上生成了两个对象,栈上的s变量指向堆上的对象,也即s保存的是堆上对象的地址
  2. s.intern(),s的内容值是"1",调用intern()后会在常量池中寻找是否有"1"这个常量字符串,发现常量池中存在"1"了,所以返回常量池中"1"的引用,也即返回常量池中"1"的地址
  3. String s2 = "1",栈上的s2变量指向常量池中的"1",也即s2保存的是常量池中"1"的地址 结果自然s和s2不相等,因此返回false。再看下面代码:
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
  1. String s3 = new String("1") + new String("1"),在常量池中生成"1",在堆上生成一个内容值为"11"的对象,且栈变量s3指向该对象
  2. s3.intern(),s3的内容值是"11",调用intern()后会在常量池中寻找是否有"11"这个常量字符串,发现常量池中没有"11",此时jdk1.6会在常量池中创建"11",并返回常量池中"11"的地址;而jdk1.7不会创建"11"这个字符串,而是会在常量池中保存一份s3指向的堆内存上对象的地址(比较拗口,其实就是保存s3的值),并返回该地址值
  3. String s4 = "11",会试图直接在常量池中创建,但是发现常量池中已经包含这个对象了,虽然常量池中不是这个对象的内容值,而是这个对象的地址值。于是常量池会将保存的这个地址值返回给s4 结果在jdk1.7下s3==s4,返回true。

综上,在使用new定义字符串或者变量拼接成新字符串的时候,如果顺便使用.intern()方法,会直接在常量池中寻找是否有同样内容的对象,如果有的话就不用在堆上开辟空间了,可以节省堆上的内存。

4. 练习

如下代码输出结果是啥?

String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
 
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);

结合上面的讲解,我们来细细分析:

  1. String s = new String("1"),生成堆上对象和常量池"1"对象,s指向堆上的对象
  2. String s2 = "1",s2指向常量池中的"1"
  3. s.intern(),因为常量池中有"1",所以返回常量池中"1"的地址
  4. String s3 = new String("1") + new String("1"),生成堆上对象,该对象内容值是"11",s3指向该对象
  5. String s4 = "11",在常量池中创建"11",并使s4指向它
  6. s3.intern(),常量池有"11"了,所以返回常量池中"11"的地址

所以,输出结果为false,false