1.String介绍 

String 位于java.lang包下,作为Java的核心类提供了很多字符串处理方法,例如,比较,替换,截取等等。以下是部分源码:

public final class String

    implements java.io.Serializable, Comparable<String>, CharSequence {

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

    private final char value[];

    /** Cache the hash code for the string */

    private int hash; // Default to 0

}

从上面可以看出

  • String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。(主要是为了"安全性"和"效率"的缘故。)
  • String类实现了Serializable、Comparable、CharSequence三个接口。
  • String实例的值是通过字符数组实现字符串存储的。(java9 改成byte[])

Java9之后String的新属性,coder编码格式

java strcmp 在哪个包里 java string在哪个包下_java

在程序中,绝大多数字符串只包含英文字母数字等字符,使用Latin-1编码,一个字符占用一个byte。如果使用char,一个char要占用两个byte,会占用双倍的内存空间。但是,如果字符串中使用了中文等超出Latin-1表示范围的字符,使用Latin-1就没办法表示了。这时JDK会使用UTF-16编码,那么占用的空间和旧版(使用char[])是一样的。

coder:

  • 0 代表Latin-1(单字节编码)
  • 1 代表 UTF-16 编码

2.字符串创建方式

  • 初始化的方式创建:
String str1="hello";

String str2="hello";

System.out.println(str1==str2);

当使用初始化的方式创建对象时,JVM会首先检查字符串池中是否存在值相等的字符串,如果存在,则不进行创建,而是直接返回字符串池中该字符串的引用地址。如果不存在,则创建该对象,并放入字符串池中,返回新创建的字符串的引用地址。

  • 使用new关键字创建:
String str3=new String("hello");

String str4=new String("hello");

System.out.println(str3==str4);

当使用new关键字创建对象时,JVM仍然首先检查字符串池中是否存在要创建的字符串,如果不存在,则在字符串池中创建一个字符串对象,然后在堆内存中继续创建一个字符串对象,返回该对象的引用地址。如果存在,则只在堆内存中创建一个字符串对象,返回该对象的引用地址。

java strcmp 在哪个包里 java string在哪个包下_java_02

3.字符串常量池

Java为了避免产生大量的字符串对象,设计了一个字符串池(String Pool),通过初始化方式创建的字符串对象都会存在于字符串池中,且字符串池中的字符串不会重复,以便可以被共享使用,提高存储效率。

  • 字符串常量池,它是一个Hash表。JDK6中,默认长度是1009,存放在永久代。在JDK7版本中,字符串常量池被移到了堆中,StringTable的长度可以指定,默认长度是60013。
  • 存放内容:在JDK6及之前版本中,字符串常量池里放的都是字符串常量;在JDK7中,由于String.intern()发生了改变,因此字符串常量池中也可以存放放于堆内的字符串对象的引用。

4.intern方法

intern 方法是一个native方法。intern()方法设计的初衷,就是重用String对象,节省内存消耗。

intern()会从字符串常量池中查询当前字符串是否存在:

  • 如果存在:就直接返回当前字符串在常量池的地址;
  • 如果不存在:
  • JDK6:把此对象复制一份,放入字符串常量池中,并返回字符串常量池中的对象地址;
  • JDK7及以后:把对象引用的地址(堆中的)复制一份,放入字符串常量池中,并返回字符串常量池中的对象地址.

举个栗子1:

String str1 = new String("a");

str1.intern();

String str2 = "a";

System.out.println(str1 == str2);
  • JDK6:

java strcmp 在哪个包里 java string在哪个包下_java strcmp 在哪个包里_03

  1. 先在常量池中声明为"a"的字符对象,再通过关键字new在堆中生成"a"的字符对象并被str1引用;
  2. 接着str1.intern();先在常量池中查找是否有相应的字符串"a"存在,有,直接返回引用;
  3. String str2 = "a";是属于符号引用,直接返回常量池中"a"引用地址;
  4. 最后str1和str2对比: false
  • JDK7:
  • 同上

举个栗子2:

String str3 = new String("a") + new String("a");

str3.intern();

String srt4 = "aa";

System.out.println(str3 == str4);
  • JDK6:

java strcmp 在哪个包里 java string在哪个包下_常量池_04

  1. 先在常量池中生成为"a"的字符对象;
  2. 然后在堆中生成str3为"aa"的字符对象,并被str3引用;
  3. 接着str3.intern();执行便去常量池中查询是否存在为"aa"的字符对象,但是发现没有,复制一份为"aa"到常量池中,返回字符串常量池中地址;
  4. String str4 = "aa";这句代码是符号引用,直接去常量池中创建,但是发现池中已经存在为aa的字符对象的引用,因此直接返回该引用;
  5. 而最后判断str3与str4的值,结果还是一个堆中的str3与常量池中的str4比较,那返回的结果必然是false。
  • JDK7:

java strcmp 在哪个包里 java string在哪个包下_java_05

  1. 先在常量池中生成为"a"的字符对象;
  2. 然后在堆中生成str3为"aa"的字符对象,并被str3引用;
  3. 接着str3.intern();执行便去常量池中查询是否存在为"aa"的字符对象,但是发现没有,不会复制一份为"aa"到常量池中,而是直接复制堆中"aa"的字符对象的引用,所以它与str3的引用地址是相同的;
  4. String str4 = "aa";这句代码是符号引用,直接去常量池中创建,但是发现池中已经存在为aa的字符对象的引用,因此直接返回该引用;
  5. 最后判断str3与str4是否相等,因为它们之间的引用的相同的,所以返回的结果为true。

注意事项:

  • 使用 intern 方法一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担
  •  

5.“+”连接符

在Java中 “+” 为字符串连接符

  • “+” 连接字符连接字符时编译时优化问题:
  1. “+”连接字符中只有常量(字符串常量,字符常量,基本数据类型(int ,long等))时,会有编译时优化;
  2. “+” 连接字符的过程有new方式创建 String类型字符串时,没有编译时优化;
  • “+” 连接字符的过程:会创建StringBuilder对象和String对象,使用StringBuilder.append()方法从左到右依次添加字符串到StringBuilder中,最后调用 .toString() 方法返回字符串对象String.

举个栗子:

String a = "aa" + "bb" +"cc" ;

String a2 = "aa" + "bb" + new String("hh") +"cc" ;

java strcmp 在哪个包里 java string在哪个包下_字符串_06

总结:

  • 使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。
  • 综上,“+”连接符对于直接相加的字符串常量效率很高,因为在编译期间便确定了它的值的字符串相加,在编译期间便被优化成了。对于间接相加(即包含字符串引用,且编译期无法确定值的),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

参考

  • 深入理解Java String类
  • 轻松理解String.intern()