最后更新: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一个数组,然后把旧数组的值复制给新数组,然后返回新数组。
所以,这个问题的答案是多少呢?