Java的字符串常量相关的一个问题
大家过年好!春节假期休了一个长假,今天刚回来。在知乎上遇到了一个很好的问题,忍不住回答了一下。原文转载过来了。
以下代码的运行结果,如何解释?
String h = new String("hw"); String h2 = h.intern(); String h1 = "hw"; System.out.println(h == h1);//false System.out.println(h2 == h1);//true System.out.println(h2 == h);//false String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4);//true
第一,先搞清楚字符串直接量和加法运算的区别。
我们看这样一段代码:
public void test() {
String s1 = new String("s1");
String s2 = new String("s") + new String("2");
}
把它编译完了以后,再使用javap -c来查看它的字节码是这样的:
public void test();
Code:
0: new #7 // class java/lang/String
3: dup
4: ldc #16 // String s1
6: invokespecial #9 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #5 // class java/lang/StringBuilder
13: dup
14: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
17: new #7 // class java/lang/String
20: dup
21: ldc #17 // String s
23: invokespecial #9 // Method java/lang/String."<init>":(Ljava/lang/String;)V
26: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: new #7 // class java/lang/String
32: dup
33: ldc #18 // String 2
35: invokespecial #9 // Method java/lang/String."<init>":(Ljava/lang/String;)V
38: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: astore_2
45: return
}
看到了没有?s1直接调用了String的构造方法。但是s2不是,它实际上使用了StringBuilder,然后通过append方法把"s"和"2"串接起来,这个简单的加法实际上变成了与以下代码等价了:
StringBuilder sb = new StringBuilder();
sb.append("s");
sb.append("2");
String s2 = sb.toString();
第二,String的intern是什么意思?
intern方法是一个native方法,它的具体实现在hotspot的源代码里。我把它简化一下,贴上来:
oop StringTable::intern(Handle string_or_null, jchar* name,
int len, TRAPS) {
unsigned int hashValue = hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
// 在StringTable里查找是否有相同的字符串。
oop found_string = the_table()->lookup(index, name, len, hashValue);
// Found,如果找到就可以直接返回了。
if (found_string != NULL) {
ensure_string_alive(found_string);
return found_string;
}
debug_only(StableMemoryChecker smc(name, len * sizeof(name[0])));
assert(!Universe::heap()->is_in_reserved(name),
"proposed name of symbol must be stable");
// 如果找不到,就把它加到StringTable里。
Handle string;
// try to reuse the string if possible
if (!string_or_null.is_null()) {
string = string_or_null;
} else {
string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
}
// 此处有省略。以下代码就是把string加到StrintTable这个hash表里。不考虑多线程的情况
// 实际上,added_or_found总是会与string是同一个对象。
// Grab the StringTable_lock before getting the_table() because it could
// change at safepoint.
oop added_or_found;
{
MutexLocker ml(StringTable_lock, THREAD);
// Otherwise, add to symbol to table
added_or_found = the_table()->basic_add(index, string, name, len,
hashValue, CHECK_NULL);
}
ensure_string_alive(added_or_found);
return added_or_found;
}
看到这个代码,我们就知道了。当StringTable里没有某一个字符串的时候,调用intern的时候,就会把这个字符串添加到StringTable里去。
所以,这个代码的结果就容易理解了:
String t1 = new String("hello ") + new String("world");
String t2 = t1.intern();
System.out.println("t1 == t2 is " + (t1 == t2));
这个结果是true,就是因为intern的时候,其实就是把t1放到StringTable,并且直接把t1做为返回值赋给了t2。
第三,但是问题还没结束。字符串常量到底是怎么回事?本来这个问题快要清楚了,一出现字符串常量,一下子又复杂了。
看这样两个例子:
String h = new String("12") + new String("3");
String h1 = new String("1") + new String("23");
String h3 = h.intern();
String h4 = h1.intern();
String h2 = "123";
System.out.println(h == h1); // false
System.out.println(h3 == h4); // true
System.out.println(h == h3); // true
System.out.println(h3 == h2); // true
这个例子,按我们之前说的,h3和h是同一个对象,h3和h4是同一个对象,h和h1不是同一个对象,都可以解释了。h2实际上呢是一个字符串常量,它和h3是同一个对象好像也是对的。但我们调整一下h2的赋值,把h2放到h3之前,结果却变了:
String h = new String("12") + new String("3");
String h1 = new String("1") + new String("23");
String h2 = "123";
String h3 = h.intern();
String h4 = h1.intern();
System.out.println(h == h1); // false
System.out.println(h3 == h4); // true
System.out.println(h == h3); // false
System.out.println(h3 == h2); // true
注意,这一次,h2的赋值在前,h3在后,然后,我们看到h3和h就不再是同一个对象了。这是为啥呢?
这是因为字符串常量,在class文件的常量池中,当执行到ldc指令去访问这个常量的时候,如果该常量是一个字符串类型,hotspot就会在后面默默地创建一个字符串,并且,调用intern方法!
case JVM_CONSTANT_String:
assert(cache_index != _no_index_sentinel, "should have been set");
if (this_oop->is_pseudo_string_at(index)) {
result_oop = this_oop->pseudo_string_at(index, cache_index);
break;
}
result_oop = string_at_impl(this_oop, index, cache_index, CHECK_NULL);
break;
// .......
oop ConstantPool::string_at_impl(constantPoolHandle this_oop, int which, int obj_index, TRAPS) {
// If the string has already been interned, this entry will be non-null
oop str = this_oop->resolved_references()->obj_at(obj_index);
if (str != NULL) return str;
Symbol* sym = this_oop->unresolved_string_at(which);
str = StringTable::intern(sym, CHECK_(NULL));
this_oop->string_at_put(which, obj_index, str);
assert(java_lang_String::is_instance(str), "must be string");
return str;
}
看到那个显眼的StringTable::intern了吗?问题就出在这里。
Java在加载字符串常量的时候会调用一遍intern,那么StringTable里就会留下这个hotspot默认创建的字符串。
好了。回到原问题。
h = new String("hw");
这条语句,"hw"是一个常量字符串,实际上,已经做过一次intern了,StringTable里保留的是hotspot默认创建的字符串。所以h2和h1会是相等的,都是StringTable里的这个默认字符串。
而s3因为是计算得来的,不是字符串常量,所以手动调用s3.intern()时,StringTable里留下的就是s3。再对s4赋值时,由于StringTable里已经有值了,所以不必再创建一次String对象,直接使用StringTable里的那个值就好了,其实就是s3,因此s3与s4是相同的对象。把s4的赋值放到s3之前再试一下。就可以验证了。
- Android 命名规范 (提高代码可以读性)
- Msdn 杂志 asp.net ajax 文章汇集
- 分布式监控系统Zabbix-3.0.3-完整安装记录-新报微信报警(企业微信)
- 测试网站页面网速的一个简单Python脚本
- 框架页面尽可以这么用(后置代码中控制框架)
- 微信小程序「学科排名」发布了
- Nginx 负载均衡的Cache缓存批量清理的操作记录
- DotNet软件开发框架
- Nginx通过https方式反向代理的简单实现
- 再论IBatisNet + Castle进行项目的开发
- 利用xml轻松读取web.config中的用户自定义节
- 分布式监控系统Zabbix-3.0.3-完整安装记录(3)-监控nginx,php,memcache,Low-level discovery磁盘IO
- python报错问题解决:'ascii' codec can't encode character
- 利用message queue实现aspx与winform通信, 并附完整示例
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- Tkinter Menubar
- python pickle模块
- 吴恩达机器学习笔记18-逆矩阵、矩阵转置
- Tkinter messagebox弹窗
- 'utf-8' codec can't decode byte 0xff in position 0
- iOS今日头条第3轮面试回忆
- Python:os.path.join()产生的斜杠在Windows和Linux下的不同表现和解决方法
- python 深复制和浅复制详解
- Octave入门之数据操作—ML Note28
- matlab导出csv文件多种方法实现
- matlab面向对象编程基础
- Octave数据运算基础教程-ML Note29
- Octave中数据的可视化—ML Note 30
- matplotlib animation FuncAnimation画2D线图
- 坚持一下只需要一个理由就够了