您所在的位置:主页 > JAVA技术 >

Java中的String学习笔记

时间:2016-01-29 17:04来源:未知 作者:os 点击:

  

  本帖中代码使用的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类的签名如下:

  Java代码

  public final class String

  implements java.io.Serializable, Comparable, CharSequence {}

  然后再看看String到底是怎么存储字符串的:

  Java代码

  /** The value is used for character storage. */

  private final char value[];

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

  一、内存存储情况

  先来一段代码:

  Java代码

  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区有如下两行:

  Java代码

  #18 = Utf8 hello my ...my...

  #20 = Utf8 hello my name is smallbug

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

  Java代码

  String s_01 = "hello my name is smallbug";

  改为:

  Java代码

  String s_01 = "hello" + "my" + "name" + "is" + "smallbug";

  结果也是一样的,compiler发现这些"+"操作完全可以在编译阶段优化掉,compiler就会进行一定的优化操作。

  那么像这种常量在对象初始化之后会被放在何处呢?在jdk1.6及1.6之前是在方法区中的String Pool中,但是jdk1.7之后JDK将String Pool放到了堆中。这也就引出了那个String的经典问题:

  Java代码

  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));

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

  Java代码

  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方法的实现过程:

  Java代码

  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的具体存储位置时还会涉及到一个本地方法,签名如下:

  Java代码

  public native String intern();

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

  Java代码

  System.out.println(s_01==s_03.intern());

  会看到他返回的是true。

  下边还有个更有趣的问题:

  Java代码

  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

  但是:

  Java代码

  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程序员都写过这么一段类似的代码:

  Java代码

  String s = "";

  for(int i = 0; !"end".equals(args[i]);){

  s+=args[i];

  }

  同样用javap命令反编译字节码,会产生一个令人非常懊恼恨不得立刻去修改以前代码的冲动:

  Java代码

  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."":()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()就可以。