JVM知识-String s = new String("111")会创建几个对象?

String s = new String("111")会创建几个对象?

一、引入

String字符串的不可变性:常量池中一定不存在两个相同的字符串。

public class App {
    public static void main(String[] args) {
        String a = "111";
        a = "222";
        System.out.println(a);
    }
}

输出结果为:?

分析:String在JVM中的存储

字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池

当创建一个字符串常量时:

  • JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。
  • 如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。

所以在上面的代码中:

String a = "111";==>先在常量池中找,找不到,创建字符串对象,并将该对象的引用地址赋给a

a = "222";==>找不到"222",将"222"的引用地址赋给a

所以最终的输出结果为"222";

引用地址

引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。

比如:User user = new User()

user是一个User 类型的引用变量,它表示的是User这个对象在内存中的地址。

所以,这里的变量a是"111"这个对象的引用地址,变量可变,不可变的是"111";

二、String为什么不可变

1. 查看String类的源码

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    public String() {
        this.value = "".value;
    }
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
} 

可以看到三点:

  • String类为final修饰
  • String存储内容使用的是char数组
  • char数组是final修饰

所以:

//String a = "111";相当于
char data [] ={'1','1','1'};
Stirng a = new String(data);
//a = "222";
char data [] ={'2','2','2'};
a = new String(data);

final修饰符的作用

  • final修饰 一个类:表明这个类不能被继承。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法
  • 当final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写,但可以重载多个final修饰的方法。重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
  • 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化。
  • 如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
  • 另外final修饰一个成员变量(属性),必须要显示初始化。在申明的时候给其赋值,否则必须在其类的所有构造方法中都要为其赋值

2. String中几个常用方法源码

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            //啥都没有,就直接把当前字符串给你
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        //看到了吗?返回的居然是新的String对象
        return new String(buf, true);
    }
    void getChars(char dst[], int dstBegin) {
        System.arraycopy(value, 0, dst, dstBegin, value.length);
    }    
    public String replace(char oldChar, char newChar) {
        //如果两个是一样的,那就没必要替换了,所以返回this
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            //把当前的char数组复制给val,然后下面基于val来操作
            char[] val = value; 

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                //创建一个新的char数组
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                //创建一个新的String对象
                return new String(buf, true);
            }
        }
        return this;
    }

    public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //正常返回的都是新new出来的String对象
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

    public String trim() {
        int len = value.length;
        int st = 0;
        char[] val = value;    /* avoid getfield opcode */

        while ((st < len) && (val[st] <= ' ')) {
            st++;
        }
        while ((st < len) && (val[len - 1] <= ' ')) {
            len--;
        }
        //如果是该字符串中包含了空格,调用substring方法,否则就是啥都没干原本返回
        //就是如果字符串里有空格,那么还是新生一个String对象返回
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
    }

总结:

  • String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何变化性的操作都会生成新的对象。
  • String对象每次有变化性操作的时候,都会重新new一个String对象(这里指的是有变化的情况)。

3. 简单的测试

public class App {
    public static void main(String[] args) {
        String a = "111";
        String a1 = "111";

        String b = new String("111");  
        String c = new String("111");
        //对象地址是同一个
        System.out.println(a==a1);
        //对象内容是一样的
        System.out.println(a.equals(a1));
        //对象地址不一样
        //我觉得是  : 因为对象类型不一样
        System.out.println(a==b);
        //false:显然是两个不同的对象
        System.out.println(b==c);
        //对象内容是一样的
        System.out.println(a.equals(b));
    }
}

总结:

  • String a = "111"; 在JVM申请内存存放"111"对应的对象,并将对象保存起来。当String a1="1111";的时候,会先去JVM的那块地里寻找是否存在"111",刚好前面保存过,所以找到,然后直接把对象的引用地址给了a1。所以此时的a和a1都保存着同一个引用地址。

  • String b = new String("111");就是创建一个对象然后把对象引用地址赋给变量b。但是这里有个特殊点,那就是(“111”),这里会先去JVM里的那块地里找找,找到了直接存放引用地址。找不到创建一个对象然后把引用地址给String的有参构造方法里。

  • 因为a和b所保存的对象引用是不一样的。(一个引用的是字符串常量,一个引用的是字符串变量)

  • a和a1放在栈上,存放着对象的引用地址。

    new的对象是在堆中。

    常量其实是要看jdk版本的。

4. 最终的答案

String s = new String("111")会创建几个对象?

  • 如果常量池中存在,只需创建一个对象
  • 不存在,创建两个对象
posted @ 2021-04-17 09:05  Rookie--;  阅读(617)  评论(0编辑  收藏  举报