前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java的字符串常量相关的一个问题

Java的字符串常量相关的一个问题

作者头像
海纳
发布2018-03-02 12:03:05
8560
发布2018-03-02 12:03:05
举报
文章被收录于专栏:海纳周报海纳周报

大家过年好!春节假期休了一个长假,今天刚回来。在知乎上遇到了一个很好的问题,忍不住回答了一下。原文转载过来了。

以下代码的运行结果,如何解释?

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

第一,先搞清楚字符串直接量和加法运算的区别。

我们看这样一段代码:

代码语言:js
复制
   public void test() {
        String s1 = new String("s1");
        String s2 = new String("s") + new String("2");
    }

把它编译完了以后,再使用javap -c来查看它的字节码是这样的:

代码语言:javascript
复制
 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"串接起来,这个简单的加法实际上变成了与以下代码等价了:

代码语言:javascript
复制
        StringBuilder sb = new StringBuilder();
        sb.append("s");
        sb.append("2");
        String s2 = sb.toString();

第二,String的intern是什么意思?

intern方法是一个native方法,它的具体实现在hotspot的源代码里。我把它简化一下,贴上来:

代码语言:javascript
复制
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里去。

所以,这个代码的结果就容易理解了:

代码语言:javascript
复制
        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。

第三,但是问题还没结束。字符串常量到底是怎么回事?本来这个问题快要清楚了,一出现字符串常量,一下子又复杂了。

看这样两个例子:

代码语言:javascript
复制
        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之前,结果却变了:

代码语言:javascript
复制
       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方法!

代码语言:javascript
复制
 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之前再试一下。就可以验证了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-02-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 HinusWeekly 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档