Java中常量池、字符串、参数传递涉及到的一些问题

为什么有这篇文章

String orgId = someThreadLocal.getOrgId();
someClass.someMethod(orgId);
someClass.someMethod(someThreadLocal.getOrgId());

起源于上面这段代码对比,我们产生了一些争论:

A: orgId这个中间变量,导致JVM多产生了一个对象。
B & C: 不,没有多产生。

为了了结这段争论,我花时间阅读了一小部分《深入理解Java虚拟机》,并结合网上的资料做了部分总结。

常量池

常量池里有什么?

  1. 字面量(Liternal)
  2. 符号引用(Symbolic References)

常量池从哪里来

javac编译时,将能确定的常量(UTF8编码的字符串,integer, float, long, double, string)保存到了.class文件中的常量池部分中。

紧接着主次版本号之后的是常量池入口,常量池是Class文件结构中与其他项目关联最多的数据类型名也是占用Class文件控件最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据结构。 --《深入理解Java虚拟机》

String s = "haha"对应的虚拟机指令是

0:   ldc     #16; //String haha    
2:   astore_1 
3:   return

以上内容通过javap命令获得。
haha这个字符串被保存在常量池的CONSTANT_String_info表中。

String与常量池

所以以下几种例子,字符串在编译后都保存在常量池中:

String s1 = "hello world";
String s2 = "hello" + " world";
System.out.println(s1 == s2); // Output: true

结果说明:JVM创建了两个引用s1和s2,但在String Pool中只创建了一个对象(hello world),而且两个引用都指向了该对象。

当变量的值编译期无法确定时(加号连接有引用存在),就会在运行时动态拼接,并把新地址赋给新变量:

String s1 = new String("hello world2");
String s2 = new String("hello world2");
System.out.println(s1 == s2); // Output: false

再看下String的构造函数描述:

     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.

意思也很明确,每一个new出来的String对象都是你传入的参数的复制,都是新的对象。而我们又知道Java中new出的对象都是存在Heap中的,所以这里并不会说常量池中有这个字符串,就不产生新对象了。

上面这段分析的结论是:运行时的new String总是会产生新对象的且存在Heap中

那么最开始的问题就变成了:方法返回的字符串和外面接收到的字符串是同一个对象吗?

方法返回的String是否是新的对象?

public static void main(String[] args) {
    String s1 = oneString();
    System.out.println(System.identityHashCode(s1)); // Output: 1044036744
}

private static String oneString() {
    String s1 = new String("hello world3");
    System.out.println(System.identityHashCode(s1)); // Output: 1044036744
    return s1;
}

通过System.identityHashCode获取与内存地址强相关的hashCode,可见并没有产生新的对象

不过此时我又有新的疑问了,方法传入的String是否是新的对象?

方法传入的String是否是新的对象?

public static void main(String[] args) {
	String s1 = "hello world4";
    System.out.println(System.identityHashCode(s1));  // Output: 1044036744
    pass(s1);
}

private static void pass(String s) {
    System.out.println(System.identityHashCode(s)); // Output: 1044036744
}

由输出可见:传入String的也是同一个对象。那为何我改变String的时候,外部的String不变呢?和平时的理解不一样啊!
经过一番查询,找到了一种解释:
Java is always pass-by-value. when we pass the value of an object, we are passing the reference to it.
如下图:
Java中常量池、字符串、参数传递涉及到的一些问题

到此为止最开始的问题已解决了。