`

谈谈Java中的String

    博客分类:
  • Java
阅读更多
本帖中代码使用的jdk版本:
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) Client VM (build 25.66-b17, mixed mode)



先思考一个问题:String为什么是不可更改的。
查看String类的签名如下:
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {}

然后再看看String到底是怎么存储字符串的:
/** The value is used for character storage. */
    private final char value[];

String类的签名,和存储String的char数组都被final修饰,它们确保了String对象是永远不会被修改的。

一、内存存储情况

   先来一段代码:
public class Test02{
        String s_01 = "hello my ...my...";

        public static void main(String[] args){
                String s_01 = "hello my name is smallbug";
        }
}

编译之后,用javap -verbose Test02 命令来反编译:
可以看到在字节码的Constant pool区有如下两行:
#18 = Utf8               hello my ...my...
#20 = Utf8               hello my name is smallbug

可见不管是全局变量还是局部变量,只要一开始String被赋值了,那么它的值就会被保存在字节码的常量池中。其实你会发现如果把之前的:
String s_01 = "hello my name is smallbug";

改为:
String s_01 = "hello" + "my" + "name" + "is" + "smallbug";

结果也是一样的,compiler发现这些"+"操作完全可以在编译阶段优化掉,compiler就会进行一定的优化操作。
   那么像这种常量在对象初始化之后会被放在何处呢?在jdk1.6及1.6之前是在方法区中的String Pool中,但是jdk1.7之后JDK将String Pool放到了堆中。这也就引出了那个String的经典问题:
String s_01 = "hello my name is smallbug";
String s_02 = "hello my name is smallbug";
String s_03 = new String("hello my name is smallbug");
System.out.println(s_01==s_02);
System.out.println(s_01.equals(s_02));
System.out.println(s_01==s_03);
System.out.println(s_01.equals(s_03));

相信结果一定不会出乎所有人的意料:
true
true
false
true

那么Why?
在对象初始化时首先将s_01对应的字符串"hello my name is smallbug"放入了运行时常量池。之后发现又来一个jvm当然不会再去创建一个了,直接把之前的字符串拿来用就行,那么就可以断定其实s_01,s_02两个reference是指向同一个内存地址的。所以s_01==s_02会是true。
在继续之前先粘一段JDK中String类equals方法的实现过程:
public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

可见String类将Object类复写了,它其实比较的是char数组中的字符是否全部相等,只要全部相等equals方法就会返回true,不关你是对象中的字符串还是对象中的字符串。所以也就可以解释两个equals方法返回true了。那么s_01==s_03中s_01指向的是运行时常量池,s_03指向的是一个String对象,两者地址肯定不相同,所以肯定返回false。
    在讨论String的具体存储位置时还会涉及到一个本地方法,签名如下:
public native String intern();

先说一下它的具体作用:如果对于一个String对象,在运行时常量池中没有与其相对应的字符串常量,就会将这个String对象中的字符串放到运行时常量池中一份。一定注意并不是复制了一个String对象副本。在之前的代码中再加一句:
System.out.println(s_01==s_03.intern());

会看到他返回的是true。
下边还有个更有趣的问题:
 public static void main(String[] args){
                String s_01 = new String(args[0]);
                String s_02 = "hellosmallbug";
                s_01.intern();
                System.out.println(s_01==s_02);
        }
//input:java Test02 hellosmallbug

输出结果是:false
但是:
 String s_01 = new String(args[0]);
                s_01.intern();
                String s_02 = "hellosmallbug";
                System.out.println(s_01==s_02);
//input:java Test02 hellosmallbug

输出结果却是true。
如果用jdk1.7+运行的结果跟我的一样如果之前的版本会出现两个false。这就更郁闷了,到底是是怎么回事呢。
    在jdk1.6之前Sting Pool是在方法区的,执行String s_01 = new String(args[0]);会在堆中创建一个String对象,当执行到 s_01.intern();时,会在StringPool中创建字符串常量,然后让刚才创建的那个对象指向该字符串。所以不管intern()在什么时候调用s_01指向的是堆中的对象,但是s_02指向的是String Pool中的常量,地址肯定不会相等。
    但是在1.7+时候StringPool被放到了堆中。在执行String s_01 = new String(args[0]);时,会在堆中创建一个String对象,但是在执行s_01.intern();时会在堆中的StringPool创建常量,然后s_01reference指向这个常量的地址。再执行String s_02 = "hellosmallbug";时,会发现StringPool中已经有了相同的常量故它也指向这个常量地址,因为所指向的地址相同,所以也就解释了后一段代码为什么返回true。那么前一段代码为什么又会返回false呢?那是因为:String s_01 = new String(args[0]);在堆中创建了一个String对象,之后 String s_02 = "hellosmallbug";在StringPool中创建了一个常量并将s_02 reference指向了这个常量地址。当执行s_01.intern()时之前创建的对象发现StringPool中已经有这个常量了,所以对象又指向了这个常量的地址。但是s_01还是指向被创建的那个对象,它所指向的地址一直没有变过。所以也就出现了s_01==s_02的结果为false。


要注意的是,String的常量池是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。

在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:-XX:StringTableSize=7758

二、应该知道的小问题
相信所有Java程序员都写过这么一段类似的代码:
String s = "";
for(int i = 0; !"end".equals(args[i]);){
      s+=args[i];
}

同样用javap命令反编译字节码,会产生一个令人非常懊恼恨不得立刻去修改以前代码的冲动:
 Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // String
         2: astore_1
         3: iconst_0
         4: istore_2
         5: ldc           #3                  // String end
         7: aload_0
         8: iload_2
         9: aaload
        10: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
        13: ifne          40
        16: new           #5                  // class java/lang/StringBuilder
        19: dup
        20: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        23: aload_1
        24: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        27: aload_0
        28: iload_2
        29: aaload
        30: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        33: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        36: astore_1
        37: goto          5
        40: return

发现13行是判断37到5行还是个循环。在这个循环里每一次对String执行"+"操作,都会创建一个StringBuilder对象,可见这多么消耗性能。为了避免这种事情发生只要你在执行循环之前创建一个StringBuilder对象然后将之后的"+"操作换成String.append()就可以。
0
1
分享到:
评论

相关推荐

    简单谈谈Java中String类型的参数传递问题

    主要介绍了简单谈谈Java中String类型的参数传递问题的相关资料,需要的朋友可以参考下

    Java面试题.docx

    18、Java中String的了解 19、String为什么要设计成不可变的? 20、Object类的equal和hashCode方法重写,为什么? 21-40题 21、List,Set,Map的区别 26、ArrayMap和HashMap的对比 29、HashMap和HashTable的区别 ...

    JAVA面试题最全集

    谈谈java多线程 23.谈谈文件加密技术 24.软件开发生命周期 25.路由协议种类及特点 26.java的awt和swing组件的GUI设计的关键 27.对于java流的认识 28.简单描述一下awt与swing区别。 29.简述java编程中事件处理...

    java面试宝典

    42、一个“.java”源文件中是否可以包含多个类(不是内部类)?有什么限制? 12 43、说出一些常用的类,包,接口,请各举5 个。 12 44、Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类?是否可以...

    Java面试宝典2020修订版V1.0.1.doc

    21、数组中有没有length()方法,String中有没有length()方法? 18 23、final, finally, finalize的区别。 18 24、‘==’和equals的区别? 18 25、JAVA中Object类中有哪些常用方法? 19 26、heap和stack有什么区别...

    Java 基础面试题

    24. 手写单例模式中的懒汉式和饿汉 25. transient 这个关键字是干啥的 26. 什么是一致性hash算法 27. 构造方法链 28. 谈谈你对线程调度的理解 29. JDK动态代理和CGLIB动态代理 30. 反射机制以及反射的方式 31...

    Java面试宝典-经典

    1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 7 2、Java有没有goto? 7 3、说说&和&&的区别。 8 4、在JAVA中如何跳出当前的多重嵌套循环? 8 5、switch语句能否作用在byte上,能否作用在...

    Java面试宝典2010版

    1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 7 2、Java有没有goto? 7 3、说说&和&&的区别。 8 4、在JAVA中如何跳出当前的多重嵌套循环? 8 5、switch语句能否作用在byte上,能否作用在...

    java面试题大全(2012版)

    1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 7 2、Java有没有goto? 7 3、说说&和&&的区别。 8 4、在JAVA中如何跳出当前的多重嵌套循环? 8 5、switch语句能否作用在byte上,能否作用在...

    java面试要点集合

    3、String和StringBuffer区别 4、jsp有哪些动作标记 5、动态Include和静态Include的区别 6、Exception中Finally的用法 7、jsp的内置对象有哪些 北京环贸通科技有限公司 1、jsp两种跳转方式 2、struts标签库有哪些 ...

    java面试问题总结

    2. StringBuffer与StringBuilder,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,不像String一样创建一些对象进行操作,所以速度就快了。 3. StringBuilder :...

    【大厂面试题总结】JavaSE面试题总结详细教程

    【大厂面试题总结】JavaSE面试题总结详细教程: 目录: 递归算法之输出某个目录下所有文件和子目录列表 泛型中extends和super的区别 内部类的理解 ...java中实现多态的机制 string常量池和intern韩雅茹

    涵盖了90%以上的面试题

    一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 如何在main方法执行前输出”hello world” java程序的初始化顺序 请说出作用域public,private,protected,以及不写时的区别 为什么java中有些...

    【大厂面试题总结】JavaSE面试题合集及其答案,基本包括javaSE所有知识点和详细解释

    【大厂面试题总结】JavaSE面试题合集及其答案,基本包括javaSE所有知识点和详细解释 。 JavaSE面试题总结详细教程: 目录: 递归算法之输出某个目录下所有文件和子目录...java中实现多态的机制 string常量池和intern

    java_ms.rar_Math Class_java collection

     第九,String s = new String("xyz") 创建了几个String Object?   第十,Math.round(11.5)等於多少? Math.round(-11.5)等於多少?   第十一,short s1 = 1 s1 = s1 + 1 有什么错? short s1 = 1 s1 += 1 有什么...

    最新Java面试宝典pdf版

    1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 7 2、Java有没有goto? 7 3、说说&和&&的区别。 8 4、在JAVA中如何跳出当前的多重嵌套循环? 8 5、switch语句能否作用在byte上,能否作用在...

    Java面试笔试资料大全

    1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 7 2、Java有没有goto? 7 3、说说&和&&的区别。 8 4、在JAVA中如何跳出当前的多重嵌套循环? 8 5、switch语句能否作用在byte上,能否作用在...

    java面试宝典2012

    1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 8 2、Java有没有goto? 8 3、说说&和&&的区别。 8 4、在JAVA中如何跳出当前的多重嵌套循环? 8 5、switch语句能否作用在byte上,能否作用在...

    java面试题

    答:JDO是java对象持久化的新的规范,为java data object的简称,也是一个用于存取某种数据仓库中的对象的标准化API。 CORBA? 答:CORBA标准是公共对象请求代理结构,用途为:用不同的程序设计语言书写,在不同的...

Global site tag (gtag.js) - Google Analytics