专栏首页海纳周报Java的字符串常量相关的一个问题

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之前再试一下。就可以验证了。

本文分享自微信公众号 - HinusWeekly(gh_4b8b4eda4e40)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-02-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java的分代式GC

    要说理解JVM的垃圾回收,什么引用计数,Copy GC,mark & compaction好像都不是必须要掌握的东西。真要说对普通的Java程序员比较重要的东西...

    海纳
  • 详解Python的is操作符

    is 操作符是Python语言的一个内建的操作符。它的作用在于比较两个变量是否指向了同一个对象。 与 == 的区别 class A(): def __i...

    海纳
  • Python的三个问题

    第一,以下程序的执行结果是什么? def foo(a = []): a.append(1) print a foo()foo() 第二,以下...

    海纳
  • 微信公众号H5支付遇到的那些坑

    简史 官方文档说的很清楚,商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,可以调用微信支付完成下单购买的流程。 当然,最近微信支付平台也加入了...

    小柒2012
  • Apriori 关联算法学习

    一言蔽之,关联规则是形如X→Y的蕴涵式,表示通过X可以推导“得到”Y,其中X和Y分别称为关联规则的先导(antecedent或left-hand-side, L...

    用户3003813
  • 微信公众号H5支付遇到的那些坑

    官方文档说的很清楚,商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,可以调用微信支付完成下单购买的流程。

    小柒2012
  • 【Java】07 常见 API

       Object 类是所有类的父类,若一个类没有指定继承的类则继承Object。任一类都直接或间接继承于Object。

    Demo_Null
  • 为什么AlertDialog要使用Builder来构建呢

    首先说句废话,因为 AlertDialog 太过复杂,内部参数太多,然后不使用构建者模式那么 AlertDialog 的构造方法就可能是:

    开发者
  • JAVA反射简单实例

    import java.lang.reflect.Field; import java.lang.reflect.Method; import java.l...

    用户2192970
  • java中两个map比较

    ydymz

扫码关注云+社区

领取腾讯云代金券