刺探Java String

来源:互联网 发布:网络pos刷卡平台 编辑:程序博客网 时间:2024/05/29 18:51

一、String对象的创建
字符串操作是计算机程序设计中最常见的行为。Java中String类型数据由两种创建形式:

String dictator = “gf”;    String dictator = new String(“gf”);

第一种先在栈中创建一个对String类对象的引用变量gongfei,然后到字符串常量池中查找是否存在字符串”gf”,if若不存在则栈中创建两个char型的值’g’、’f’,然后在堆中创建一个String对象object,它的值是刚才在栈中创建的两个char型值组成的数组{ g’,’f’},接着这个String对象object被存放进字符串常量池,最后让dictator指向这个对象的地址;else如果”gf”已经被保存在字符串常量池中,则在字符串常量池中找到值为”gf”的对象object,然后让dictator指向这个object的地址。也就是说第一种情况,JVM会自动根据栈中数据的实际情况来决定是否需要创建新对象。
第二种可以分解成两步:1)String object = “gf”;2)String dictator = new String(object);第一步等同于第一种创建方式,而第二步由于“gf”已经被创建并存储于字符串常量池中,所以JVM只会在堆中新建一个String对象,它的值共享栈中已有的两个char型值。第二种创建方式需要在堆中创建新的对象,而不管字符串常量池中是否已存在equals的字符串。
二、重载“+“和StringBuilder
String对象是不可变的,这一点往往被使用人员所忽视。查看JDK文档会发现,String类中每一个看起来会改变String值的方法,实际上都是创建了一个全新的String对象。String变量一旦初始化后就不能更改,禁止改变对象的状态,从而增加共享对象的坚固性、减少对象访问的错误,同时还避免了在多线程共享时进行同步的需要。
String对象是不可变的,你可以给一个String对象加任意多的别名。因为String对象具有只读特性,所有指向它的引用都不可能改变它的值,因此,也就不会对其他的引用有什么影响。
不可变性会带来一定的效率问题。为String对象重载的“+“操作符就是一个例子。
操作符“+“可以用来连接String:

public class StringConcatenation{       public static void main(String args[]){             String gongfei = "gongfei";        String dog = "kill" + gongfei + "dictator";        System.out.println(dog);    }}/*output:killgongfeidictator*/

为了确定上段代码是如何工作的,用JDK自带的工具javap来反编译以上代码。
Javap –c StringConcatenation
-c标志表示将生成JVM字节码。剔除掉不感兴趣的部分,得到以下字节码:

注意,从生成的JVM字节码可以看到编译器自动引入了java.lang.StringBuilder类。虽然我们在源程序中并没有使用StringBuilder类,但编译器却默默的使用了它,因为它更高效。
在这个例子中,编译器创建了一个StringBuilder对象,用以构造最终的String,并为每个字符串调用一次StringBuilder的append()方法,统共3次。最后调用toString()生成结果,并存为dictator。
虽然编译器会默默的为我们优化性能,但这并不意味着可以随意的使用String对象。当我们以循环的形式进行字符串的连接,如果使用下面这种方式

String dictators = “”;for(int gongfei = 0; gongfei < gongfeiCount; gongfei++ ){    dictators += dictator[gongfei];}

查看反编译后的JVM字节码,可以看到StringBuilder是在循环体内构造的,这就意味着每次循环都会构造一个StringBuilder对象。下面我们显示的构造一个StringBuilder对象:

StringBuilder dictators = new StringBuilder();For(int gongfei = 0; gongfei < gongfeiCount; gongfei++){    dictators.append(dictator[gongfei]);}

查看反编译后的JVM字节码可以注意到它只生成了一个StringBuilder对象。
因此,当为一个类编写toString()方法时,如果字符串操作比较简单,那就可以信赖编译器。但是,如果要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象,通过它的append()方法来构造最终的结果。
三、巩固 “==“和equals()
下面代码的输出结果是what?

    public class Test {      public static void main(String[] args) {              String str = "abc";          String str1 = "abc";        String str2 = new String("abc");         System.out.println(str == str1);          System.out.println(str1 == "abc");          System.out.println(str2 == "abc");         System.out.println(str1 == str2);              System.out.println(str1.equals(str2));          System.out.println(str1 == str2.intern());          System.out.println(str2 == str2.intern());          System.out.println(str1.hashCode() == str2.hashCode());      }  }

做个基础的说明,堆(heap)内存和栈(Stack)内存的问题。堆和栈的数据结构这里就不解释了。Java语言使用内存的时候,栈内存主要保存以下内容:基本数据类型和对象的引用,而堆内存存储对象,栈内存的速度要快于堆内存。总结成一句话就是:引用在栈而对象在堆。
Java中的比较有两种,是==和equals()方法,equals()是Object类的方法,定义在Object类中的equals()方法是如下实现的:

        public boolean equals(Object obj){          return (this==obj);  }  
String类重写了equals()方法,改变了这些类型对象相等的原则,即判断对象是否相等依据的原则为判断二者的内容是否相等。 Java运行时维护一个String池,池中的String对象不可重复,没有创建,有则作罢。String池不属于堆和栈,而是属于常量池。下面分析上方代码的真正含义 

Java代码

    String str = "abc";      String str1= "abc"; 
第一句的真正含义是在String池中创建一个对象”abc”,然后引用时str指向池中的对象”abc”。第二句执行时,因为”abc”已经存在于String池了,所以不再创建,则str==str1返回true就明白了。str1==”abc”肯定正确了,在String池中只有一个”abc”,而str和str1都指向池中的”abc”,就是这个道理。 
String str2 = new String("abc");  
这个是Java SE的热点问题,众所周知,单独这句话创建了2个String对象,而基于上面两句,只在栈内存创建str2引用,在堆内存上创建一个String对象,内容是”abc”,而str2指向堆内存对象的首地址。 下面就是str2==”abc”的问题了,显然不对,”abc”是位于String池中的对象,而str2指向的是堆内存的String对象,==判断的是地址,肯定不等了。

str1.equals(str2),这个是对的,前面说过,String类的equals重写了Object类的equals()方法,实际就是判断内容是否相同了。
下面说下intern()方法,在JavaDoc文档中,这样描述了intern()方法:返回字符串对象的规范化表示形式。怎么理解这句话?实际上过程是这样进行的:该方法现在String池中查找是否存在一个对象,存在了就返回String池中对象的引用。
那么本例中String池存在”abc”,则调用intern()方法时返回的是池中”abc”对象引用,那么和str/str1都是等同的,和str2就不同了,因为str2指向的是堆内存。
hashCode()方法是返回字符串内容的哈希码,既然内容相同,哈希码必然相同,那他们就相等了,这个容易理解。
再看下面的例子:

    public class Test {     private static String str = "abc";     public static void main(String[] args) {        String str1 = "a";        String str2 = "bc";          String combo = str1 + str2;          System.out.println(str == combo);          System.out.println(str == combo.intern());      }      }  
这个例子用来说明用+连接字符串时,实际上是在堆内容创建对象,那么combo指向的是堆内存存储”abc”字符串的空间首地址,显然str==combo是错误的,而str==combo.intern()是正确的,在String池中也存在”abc”,那就直接返回了,而str也是指向String池中的”abc”对象的。此例说明任何重新修改String都是重新分配内存空间,这就使得String对象之间互不干扰。也就是String中的内容一旦生成不可改变,直至生成新的对象。 
String s0="helloworld"; String s1="helloworld"; String s2="hello" + "word"; 

System.out.println( s0==s1 ); //true 可以看出s0跟s1是同一个对象
System.out.println( s0==s2 ); //true 可以看出s0跟s2是同一个对象
分析:因为例子中的 s0和s1中的”helloworld”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”hello”和”world”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中”helloworld”的一个引用。所以我们得出s0==s1==s2。

参考:《Tinking in Java (Fourth Edition)》
http://www.cnblogs.com/fguozhu/articles/2661055.html
http://sarin.iteye.com/blog/603684/

0 0
原创粉丝点击