最后更新:2020年5月18日

世界就是这么痛苦,有人还单身,程序有几个对象的问题都出来了

从最简的开始

String a="123";

问:共有几个对象?创建了几个对象?他们在哪?
答:共有1个对象,创建了1个对象(也可以说是0个),在常量池。

显然,这一个对象就是在常量池里的"123"。那创建了几个对象为什么可以是0也可以是1呢?
因为这个“123”比较特殊,虽然是对象,但是字面量是class加载的时候就放进常量池的。
所以这段代码并没有创建对象,是在类加载时出现的对象。但是也可以说,这句话创建了一个对象。
因为如果没有这句话,“123”就不存在,不就没“创建”这一个对象了吗...换言之,类加载时出现的对象,也可以算作是这句话创建的对象。
字节码如下:

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 11 L0
    LDC "123"
    ASTORE 1
   L1
    LINENUMBER 28 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE aString Ljava/lang/String; L1 L2 1
    MAXSTACK = 1
    MAXLOCALS = 2

那么,

String a="123";
String b="123";

这个呢?
还是创建一个对象,因为还是这一个“123”。字节码如下:

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 11 L0
    LDC "123"
    ASTORE 1
   L1
    LINENUMBER 12 L1
    LDC "123"
    ASTORE 2
   L2
    LINENUMBER 28 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE aString Ljava/lang/String; L1 L3 1
    LOCALVARIABLE aStrig Ljava/lang/String; L2 L3 2
    MAXSTACK = 1
    MAXLOCALS = 3

来来回回,都是LDC “123”,也就是说,这两句话其实就是创建了这1个“123”而已,当然,还是那句话,如果认为是在类加载的时候放进常量池的,你说这两句话没创建对象,也没错。

new String()呢?

String a=new String("123");

问:共有几个对象?创建了几个对象?他们在哪?
答:共有2个对象,创建了2个对象(也可以说是1个),在常量池。

同理,一个是常量池的"123",一个就是这个new创建的对象。这个new创建出来的对象在内存中,的的确确是这句话创建的,所以这句话至少创建了一个对象。至于这个“123”,你懂的。
字节码如下,第5行的New,就是“至少一个对象”的那“一个对象”:

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 12 L0
    NEW java/lang/String
    DUP
    LDC "123"
    INVOKESPECIAL java/lang/String.<init>(Ljava/lang/String;)V
    ASTORE 1
   L1
    LINENUMBER 28 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE aStrig Ljava/lang/String; L1 L2 1
    MAXSTACK = 3
    MAXLOCALS = 2

再难点!

String a = "12"+"3";

问:共有几个对象?创建了几个对象?他们在哪?
答:共有1个对象,创建了1个对象(也可以说是0个),在常量池。

有人可能觉得神奇,我一开始也不信,看到了这个问题也答错了,后来看到字节码,释然了。

    // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 13 L0
    LDC "123"
    ASTORE 1
   L1
    LINENUMBER 31 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE s Ljava/lang/String; L1 L2 1
    MAXSTACK = 1
    MAXLOCALS = 2

你可能一下没看出点端倪,那我告诉你,这段字节码和上面“从最简单开始”里提到的“String a="123";”这句话所生成的字节码的关键代码部分一模一样!!说白了,编译器优化了,看见你“12”+“3”就LDC "123",而不是LDC "12"再搞些什么操作再LDC "3"!意思估计就是:“愚蠢的凡人,“12”+“3”一定是“123”,为啥你要拆开来写成“12”+“3”呢?你傻我不和你一起傻,我直接当你输入是“123”就完事了!”嗯,所以答案和第一个案例一样,解释也一样。
额外再附赠个证明:

String s = "12"+"3";
String t3=new String("123").intern();
System.out.println(s==t3);

结果为true,说明第一句话确实在常量池里放的是“123”,如果不是,那么第三句话会打印false。

那,重点来了,延伸探讨!!

以上都是老面试题,新面试题玩骚操作了,看如下代码

String s="12"+new String("3");

问:共有几个对象?创建了几个对象?他们在哪?
答:共有6个对象,创建了6个对象(也可以说是4个),其中常量池2个,内存中4个。

纳尼?没错,所有公众号和网上复制黏贴的文章,我看到的都不是6个对象,没有人敢答6个对象。
6个对象是谁,它们分别是“12”、“3”、new出来那个String、StringBuilder、StringBuilder.toString()、new char[]。
看字节码。

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 19 L0
    NEW java/lang/StringBuilder
    DUP
    LDC "12"
    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    NEW java/lang/String
    DUP
    LDC "3"
    INVOKESPECIAL java/lang/String.<init>(Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
    ASTORE 1
   L1
    LINENUMBER 30 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE s Ljava/lang/String; L1 L2 1
    MAXSTACK = 4
    MAXLOCALS = 2

这是什么?!?!这是StringBuilder?这玩意怎么出现的?
其实,当等号右边有非常量池对象的时候,这玩意就会出来。java字符串的+号,如果大家都是常量池内的值,就直接把右边相加的结果认为是一个常量池的值,在类加载时扔入常量池。而如果有对象,那么,StringBuilder就出场了。
根据字节码可知,代码变为了

String s=new StringBuilder("12").append(new String("3")).toString();

这一行代码的字节码如下:

    // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 20 L0
    NEW java/lang/StringBuilder
    DUP
    LDC "12"
    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    NEW java/lang/String
    DUP
    LDC "3"
    INVOKESPECIAL java/lang/String.<init>(Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
    ASTORE 1
   L1
    LINENUMBER 31 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE s Ljava/lang/String; L1 L2 1
    MAXSTACK = 4
    MAXLOCALS = 2

注意,这里是代码变为了上面这行,而不是简单的等效,是完全变为了上面这行,因为这是字节码反推的结果。不难看出,关键代码也是完全、100%、绝对、absolutely一样的。
问题转化为,这句用StringBuilder的代码,创建了几个对象?
首先,有个“12”和“3”,又是那句话,放常量池。这是毫无争议的。
然后,new StringBuilder,这肯定是一个对象,这也是毫无争议的。
于是,有人就认为,答案是3。这个人写了个博客,于是,半个C * * N,给的答案都是3,呵。
我们来看StringBuilder源码

@Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

源码写得清清楚楚,清清楚楚,清清楚楚,new,看到了么?
这个value是StringBuilder父类的AbstractStringBuilder的一个字段,它是这样定义的:

/**
 * The value is used for character storage.
 */
 char[] value;

如果,这个临时出现的StringBuilder,它被认为是这句话创建出来的对象,那么,toString()方法 return 的那个new String(),为啥不能算是这句话创建出来的对象呢?仅仅是因为别人没有把这个new写给你看而写在了源码里?偏要你自己拿到这个char数组和StringBuilder的count变量然后new一个String才算创建对象吗?别人帮你创建的就不是对象了?答案显然意见。
至此,已经出现了5个对象了,它们是“12”、“3”、题目的new String()、new出来的StringBuilder和StringBuilder.toString()方法new出来的String,还有一个呢?
没错,value数组!字符数组!
StringBuilder在new的时候,会super调用父类的构造方法,从而初始化它自己的char[] value;

/**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16);
    }
/**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

且,char[] value的初始大小为16(好像某hashmap初始大小也是16)。
看到这个new了么,还是那句话,不能人家因为在源码里new,人家创建的对象就不是对象,人家的代码该运行运行,该做事做事。所以,这当然也算一个对象。至此,6个对象齐全了。
所以,答案是,共有6个对象,创建了6个对象(也可以说是4个),其中常量池2个,内存中4个。
当然,有些人虽然答的是4个,但他们认为这四个是“12”、“3”、new String和new StringBuilder。显然,这些人的回答是错误的。

结束前,再来点难的呗

以下代码,共有几个对象?创建了几个对象?他们在哪?

String s="123456789"+new String("ABCDEFGH");

字节码如下:

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 20 L0
    NEW java/lang/StringBuilder
    DUP
    LDC "123456789"
    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    NEW java/lang/String
    DUP
    LDC "ABCDEFGH"
    INVOKESPECIAL java/lang/String.<init>(Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
    ASTORE 1
   L1
    LINENUMBER 34 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE s Ljava/lang/String; L1 L2 1
    MAXSTACK = 4
    MAXLOCALS = 2

确实可以转换为

String s=new StringBuilder("123456789").append(new String("ABCDEFGH")).toString();

但是,刚看过源码的逆,我相信你还记得,value数组的初始化长度是16,再看看这里多长呢?

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

stringBuilder会调用父类的ensureCapacityInternal()方法

private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

该方***判断是否爆长度,如果爆了,就对value数组进行扩容(最后一句),第一句告诉我们,扩容长度为当前长度的2倍+2,初始长度为16,那么现在应该会扩容到34。

/**
     * This implements the expansion semantics of ensureCapacity with no
     * size check or synchronization.
     */
    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

别急,你们的new来了,Arrays.copyOf()方法是如何执行的?看到这个new了吗?

public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

先new一个数组,然后把旧数组的值复制给新数组,然后返回新数组。
所以,这个问题的答案是多少呢?