关于Java字符串的一点探究

这几天琢磨Java内存分配,看到有个博客讲的不错,只是讲到String字符串的时候,抛了异常没有解决问题如下:

常量池中的字符串常量与堆中的String对象有什么区别呢?为什么直接定义的字符串同样可以调用String对象的各种方法呢?
       带着诸多疑问,我和大家一起探讨一下堆中String对象和常量池中String常量的关系,请大家记住,仅仅是探讨,因为本人对这块也比较模糊。
       第一种猜想:因为直接定义的字符串也可以调用String对象的各种方法,那么可以认为其实在常量池中创建的也是一个String实例(对象)。String s1 = new String("myString");先在编译期的时候在常量池创建了一个String实例,然后clone了一个String实例存储在堆中,引用s1指向堆中的这个实例。此时,池中的实例没有被引用。当接着执行String s1 = "myString";时,因为池中已经存在“myString”的实例对象,则s1直接指向池中的实例对象;否则,在池中先创建一个实例对象,s1再指向它。

       这种猜想认为:常量池中的字符串常量实质上是一个String实例,与堆中的String实例是克隆关系。
       第二种猜想也是目前网上阐述的最多的,但是思路都不清晰,有些问题解释不通。下面引用《JAVA String对象和字符串常量的关系解析》一段内容。
       在解析阶段,虚拟机发现字符串常量"myString",它会在一个内部字符串常量列表中查找,如果没有找到,那么会在堆里面创建一个包含字符序列[myString]的String对象s1,然后把这个字符序列和对应的String对象作为名值对( [myString], s1 )保存到内部字符串常量列表中。如下图所示:
            如果虚拟机后面又发现了一个相同的字符串常量myString,它会在这个内部字符串常量列表内找到相同的字符序列,然后返回对应的String对象的引用。维护这个内部列表的关键是任何特定的字符序列在这个列表上只出现一次。
           例如,String s2 = "myString",运行时s2会从内部字符串常量列表内得到s1的返回值,所以s2和s1都指向同一个String对象。
           这个猜想有一个比较明显的问题,红色字体标示的地方就是问题的所在。证明方式很简单,下面这段代码的执行结果,javaer都应该知道。
               String s1 = new String("myString");
               String s2 = "myString";
               System.out.println(s1 == s2);  //按照上面的推测逻辑,那么打印的结果为true;而实际上真实的结果是false,因为s1指向的是堆中String对象,而s2指向的是常量池中的String常量。
<img src="https://img-blog.youkuaiyun.com/20141022205250906?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGMzNzI5MjNhMDFtbQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

           虽然这段内容不那么有说服力,但是文章提到了一个东西——字符串常量列表,它可能是解释这个问题的关键。
           文中提到的三个问题,本文仅仅给出了猜想,请知道真正内幕的高手帮忙分析分析,谢谢!
以上为原作者原话,下面咱就讨论一下问题:

当第一次创建一个String对象时,如 

String str1 = new String("abc");

既然是new一个对象,肯定是在堆里分配了空间用于存放"abc",不过,除此之外虚拟机还要去常量池看看有没有"abc"这个字符串,如果没有就在常量池创建一个;如果有,就不创建了.当然str1指向堆里的内存空间。

这时,String str2 = “abc”;

str2指向常量池里的字符串地址;下面列举一段代码:

public class StringTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String str1 = new String("a");
		String str2 = "a";
		String str3 = new String("a");
		System.out.println(str1 == str2);
		System.out.println(str1 == str3);
		System.out.println(str2 == str3);
		/**str1.intern()返回的字符串内存地址在常量池中*/
		System.out.println(str1.intern() == str2);
		System.out.println(str3.intern() == str2);

	}

}
输出:

false
false
false
true
true
String对象的intern()方法返回的字符串内存地址在常量池中

在上面这个例子中 str1.intern()、str3.intern()、str2指向同一块内存,上面的问题就解决了。

关于字符串我又翻了翻《thinking in Java》这本书关于字符串那章,看到有关于“+”重载与StringBuilder的一些知识点:

关于String s = “abc”+"def";

Java底层都做了什么呢?

于是就写了如下代码:

public class Concatenation {
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        String mango = "mango";
        String s = "abc"+mango+"def"+47;
        System.out.println(s);
    }
 
}
使用Jdk自带的javap来反编译上面的代码 就有了以下字节码:

进入dos,找到.class文件目录 输入:javap -c Concatenation 

public class com.lichao.string1.Concatenation {
  public com.lichao.string1.Concatenation();
    Code:
       0: aload_0
       1: invokespecial #8 // Method java/lang/Object."<init>":
()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: new #16 // class java/util/GregorianCalendar
       3: dup
       4: sipush 1992
       7: iconst_5
       8: bipush 23
      10: invokespecial #18 // Method java/util/GregorianCalenda
r."<init>":(III)V
      13: astore_1
      14: ldc #21 // String 生日是: %1$tm %1$te %1$tY
      16: iconst_1
      17: anewarray #3 // class java/lang/Object
      20: dup
      21: iconst_0
      22: aload_1
      23: aastore
      24: invokestatic #23 // Method java/lang/String.format:(L
java/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
      27: astore_2
      28: getstatic #29 // Field java/lang/System.out:Ljava/
io/PrintStream;
      31: aload_2
      32: invokevirtual #35 // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
      35: return
}
dup 和invokevirtual语句相当于Java虚拟机上的汇编语句.即使你完全不了解汇编语言也不要紧,重点是编译器自动引入了java.lang.StringBuilder类.虽然我没有使用,但编译器却自作主张使用了它,因为它更高效.
    这个例子中,编译器创建了一个StringBuilder对象,用以构造最终的String,并为每个字符串调用了一次StringBuilder的append()方法,总计4次.最后调用toString()生成结果.并存为s(使用的命令为astore2).
既然编译器能为我们自动优化性能,接下来就看看能为我们优化到什么程度.下面的程序采用两种方式生成一个String:
方法一使用 了多个String对象;方式二在代码中使用了StringBuilder.
public class WhithStringBuider {
    /**方式一*/
    public static String implicit(String[] fields){
        String result = "";
        for(int i = 0; i < fields.length; i++)
            result += fields[i];
        return result;
    }
     
    /**方式二*/
    public static String explicit(String[] fields){
        StringBuilder result = new StringBuilder();
        for(int i = 0; i < fields.length; i++)
            result.append(fields[i]);
        return result.toString();
    }
 
}
javap来反编译上面的代码 就有了以下字节码:
Compiled from "WhithStringBuider.java"
public class com.lichao.string1.WhithStringBuider {
  public com.lichao.string1.WhithStringBuider();
    Code:
       0: aload_0
       1: invokespecial #8 // Method java/lang/Object."<init>":
()V
       4: return
  public static java.lang.String implicit(java.lang.String[]);//implicit对应的字节码
    Code:
       0: ldc #16 // String
       2: astore_1
       3: iconst_0
       4: istore_2
       5: goto 32
       8: new #18 // class java/lang/StringBuilder
      11: dup
      12: aload_1
      13: invokestatic #20 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
      16: invokespecial #26 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
      19: aload_0
      20: iload_2
      21: aaload
      22: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      25: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      28: astore_1
      29: iinc 2, 1
      32: iload_2
      33: aload_0
      34: arraylength
      35: if_icmplt 8
      38: aload_1
      39: areturn
  public static java.lang.String explicit(java.lang.String[]);//explicit()对应的字节码
    Code:
       0: new #18 // class java/lang/StringBuilder
       3: dup
       4: invokespecial #45 // Method java/lang/StringBuilder."<
init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: goto 24
      13: aload_1
      14: aload_0
      15: iload_2
      16: aaload
      17: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      20: pop
      21: iinc 2, 1
      24: iload_2
      25: aload_0
      26: arraylength
      27: if_icmplt 13
      30: aload_1
      31: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      34: areturn
}
在implicit()中在循环内部创建StringBuilder,这意味着每循环一次,就会创建一个新的StringBuilder对象
而在explicit()方法对应的字节码中不仅循环部分更短、更简单,而且它只生成一个StringBuilder对象。显示地创建StringBuilder 还允许你预先为其指定大小.如果你已经知道最终的字符创大概有多长,预先指定StringBuilder的大小可以避免重新分配缓冲.
因此,当你为一个类编写toString()方法时如果字符串操作比较简单,哪就可以信赖编译器,它会为你合理地构造最终的字符串结果,但是,如果你要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象,用它来构造最终的结果,参考如下:
public class UsingStringBuilder {
     /**使用单个 long 创建一个新的随机数生成器*/
    public static Random rand = new Random(50);
     
    public static void main(String[] args){
        UsingStringBuilder us = new UsingStringBuilder();
         
        System.out.println(us);
    }
     
    public String toString(){
        StringBuilder result =  new StringBuilder("[");
        for(int i = 0; i < 25; i++){
            result.append(rand.nextInt(100));
            result.append(",");
        }
              /**删除最后一个","和空格,以便添加右括号*/
        result.delete(result.length() - 2, result.length());
        result.append("]");
         
        return result.toString();
    }
}
输出:
[17, 88, 93, 12, 51, 61, 36, 58, 16, 8, 0, 12, 0, 55, 28, 92, 52, 7, 15, 92, 55, 23, 91, 21, 62]

最终结果是用append()语句一点点拼接起来的,如果你想走捷径,如:append(a+":"+c),那么编译器就会掉入陷阱,从而为你另外创建一个StringBuilder对象处理括号内的字符串操作.
愿读者留下评论,以便大家多多交流。
如有不足之处,望多多指点。
参考文献:《Java编程思想》第四版

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值