继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Java探秘之神秘的字符串String(二)

小丑鱼yang
关注TA
已关注
手记 4
粉丝 19
获赞 84

不可变性

String可以说是最常用的类型了,即字符串类型,String是常量(final关键词修饰的),他的值不能被创建后更改,因为字符串是不可被改变的,所以可以被用来共享。
Java语言为String提供了同基本数据类型相似的操作符(+,+=),这里请注意,由于String是不可被改变的,所以每次操作都会会重新生成一个String类型。

1 String a = "a";
2 System.out.println(a.hashCode());
3 a += "b";
4 System.out.println(a.hashCode()); 

输出的结果是不一致的,此外String的所有方法都是返回一个新的字符串对象。

共享性
这里就需要提到jvm的内存模型了,jvm中将内存大致分为两大块(细致不止两大块,这里重点不是这个,所以就大致的解释下),引用《深入java虚拟机》作者的原话, 经常有人把Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。
堆内存呢主要存放所有对象的实例(包括数组)、而栈主要存放的是方法、局部变量等信息,而在此之外还有一个常量池用来存放字面常量和符号引用,而每当我们创建一个字符串类型时就会先在常量池中查找是否存在该字符串,如果存在则直接返回实例引用,否则将会实例化一个新的字符串存放到常量池中,从而实现了共享。
以下例子来源《Effective》一书

String a = new String("a");

这个例子中呢,创建了两个对象,第一个对象“a”实例化以后被存储到常量池中(常量池中不存在的情况下),第二个a是堆(因为a的一个对象实例)中的字符串对象,该对象是在运行时创建的。
所以聪明的人才创建了另一种写法:

String a = "a";

这种写法比上一种更为简洁而且是直接创建了对象实例,只创建了一个对象,编译时完成该分配。

String a1 = new String("a");
String a2 = new String("a");
String a3 = "a";
String a4 = "a";
System.out.println(a1 == a2);//false 因为a1跟a2都是运行时创建返回的新实例对象。
System.out.println(a1 == a3);//false
System.out.println(a3 == a4);//true a3、a4都是直接指向常量池中的内存地址。

讲到这里,我们可以拓展开继续讲下去,关于final时字符串的工作表现。

String a = "a";
String bc = "bc";
String abc1 = a + bc;
String abc2 = "abc";
String abc3 = "a" + "bc";//编译器会在编译阶段将其转化成"abc",因为这两个值在编译时就确定了。
String abc4 = a + "bc";//由于a是变量,a的值只能在运行时才能确定
System.out.println( abc1 == abc2);//false
System.out.println( abc2 == abc3);//true
System.out.println( abc2 == abc4);//false

那么我们再来看下面这个情况,加入了final修饰符,变成常量(这里要区分下,String是的值是常量类型,表示的是他的值,而String表示的是最终类)

final String a = "a";
final String bc = "bc";
String abc1 = a + bc;
String abc2 = "abc";
System.out.println( abc1 == abc2);//true 因为final修饰的常量在编译阶段的值就确定了,编译器会将该常量全部"宏替换"成指向该常量池中的实例,final修饰符在编译完成之后是不存在的(这里有点绕,要理解)。

可能刚才有的朋友会很好奇,不是说字符串类型是不可被改变的么,怎么编译器还将其优化合并成一个对象哇,有这种疑问非常好,接下来就是揭秘时刻。
该例子来着《Think in java》一书。
当我们使用“+”操作符进行连接时,编译器会帮我们对代码进行优化将其操作转换成StringBuilder(1.5版本之前是StringBuffer),由于StringBuffer是不安全的,所以1.5之前对其优化处理可能会出现并发问题,这也是为什么编译器会对其进行合并。
而对于“+=”操作符,例如:a += b;会将进行拆分成a = a + b;这是基础不废话了。
这里是java代码

 1 package top.yangchudong.main;
 2 
 3 public class App {
 4 
 5     public static void main(String[] args){
 6         String a = "a";
 7         a += "b";
 8         a += "c";
 9         System.out.println(a);
10     }
11 
12 }

以下是编译结果

E:\JavaProject\GradleTest\src\main\java>javac top\yangchudong\main\App.java

E:\JavaProject\GradleTest\src\main\java>javap top\yangchudong\main\App.class
Compiled from "App.java"
public class top.yangchudong.main.App {
  public top.yangchudong.main.App();
  public static void main(java.lang.String[]);
}

E:\JavaProject\GradleTest\src\main\java>javap -c top\yangchudong\main\App.class
Compiled from "App.java"
public class top.yangchudong.main.App {
  public top.yangchudong.main.App();     //这是默认构造方法    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String a
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder 这里可以清楚的看到定义了一个StringBuilder对象
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<
init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String b
      16: invokevirtual #5                  // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      22: astore_1
      23: new           #3                  // class java/lang/StringBuilder 这里又创建了一个
      26: dup
      27: invokespecial #4                  // Method java/lang/StringBuilder."<
init>":()V
      30: aload_1
      31: invokevirtual #5                  // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      34: ldc           #8                  // String c
      36: invokevirtual #5                  // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      39: invokevirtual #7                  // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      42: astore_1
      43: getstatic     #9                  // Field java/lang/System.out:Ljava/
io/PrintStream;
      46: aload_1
      47: invokevirtual #10                 // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
      50: return
}

这个例子我是故意要这样写的,因为很多人认为既然编译帮我们做了优化,那么他们就可以不注意String的使用,但这其实就是一个编程陷阱。
我们理想上面例子编译器会帮我们编译成下面这个样子:

StringBuilder a = new StringBuilder("a");
a.append("b");
a.append("c");
System.out.println(a.toString());

可现实编译器帮我们做到的只能是这样:

String a = "a";
StringBuilder builder1 = new StringBuilder(a);
builder1.append("b");
a = builder1.toString();
StringBuilder builder2 = new StringBuilder(a);
builder1.append("c");
a = builder1.toString();
System.out.println(a);

所以大家以后在编写的时候要多注意这个问题的存在,当较多的使用操作符操作字符串时还是使用StringBuilder或者StringBuffer。

打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP