除了八大基本数据类型之外,我们还经常使用一个数据结构——String,String可以说是Java编程中最常见的数据结构。String类在java.lang包中,它的作用主要是用于创建一个字符串变量的对象。Java把String类声明为final类,不能有子类,String类对象创建后也不能修改。

如何创建一个String类对象

//字符串声明
    String str;
//字符串创建
    //1.通过new一个对象,将字符串作为参数传进构造函数
    str = new String("abc");

    //2.直接赋值
    str = "abc";

以上两种方式时最常见的创建一个String类对象的方式,除了以上两个之外,我们还可以通过String类其他不同的构造方法进行创建。(下表摘自Java 8 API)

String(byte[] bytes, int offset, int length)

通过使用平台的默认字符集解码指定的字节子阵列来构造新的 String 。

String(byte[] bytes, int offset, int length, Charset

构造一个新的String通过使用指定的指定字节子阵列解码charset 。

String(byte[] ascii, int hibyte, int offset, int count)已弃用

此方法无法将字节正确转换为字符。 从JDK 1.1开始,首选的方式是通过String构造函数获取Charset ,字符集名称,或使用平台的默认字符集。

String(byte[] bytes, int offset, int length, String

构造一个新的 String通过使用指定的字符集解码指定的字节子阵列。

String(byte[] bytes, String

构造一个新的String由指定用指定的字节的数组解码charset 。

String(char[] value)

分配一个新的 String ,以便它表示当前包含在字符数组参数中的字符序列。

String(char[] value, int offset, int count)

分配一个新的 String ,其中包含字符数组参数的子阵列中的字符。

String(int[] codePoints, int offset, int count)

分配一个新的 String ,其中包含 Unicode code point数组参数的子阵列中的 字符 。

String(String

初始化新创建的String对象,使其表示与参数相同的字符序列; 换句话说,新创建的字符串是参数字符串的副本。

String(StringBuffer

分配一个新的字符串,其中包含当前包含在字符串缓冲区参数中的字符序列。

String(StringBuilder

分配一个新的字符串,其中包含当前包含在字符串构建器参数中的字符序列。

String类中常用的方法

1、public int length():该方法返回该字符串的长度

2、public char charAt(int index):该方法返回字符串中指定位置的字符,注意字符串中第一个字符索引是0,最后一个是length()-1。

3、提取子串

(1)public String substring(int beginIndex) :该方法时从beginIndex位置起所有剩余字符作为一个新的字符串返回。

(2)public String substring(int beginIndex , int endIndex):该方法则是从beginIndex位置到endIndex-1位置的字符作为一个新的字符串返回。

4、字符串比较
(1)public int compareTo(String anotherString):该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系。若当前对象比参数大则返回正整数,反之返回负整数,相等返回0。
(2)public int compareToIgnore(String anotherString):与compareTo方法相似,但忽略大小写。
(3)public boolean equals(Object anotherObject):比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false。
(4)public boolean equalsIgnoreCase(String anotherString):与equals方法相似,但忽略大小写。

5、public String concat(String str):该方法将参数中的字符串str连接到当前字符串的后面,效果等价于”+“。

6、字符串中单个字符查找
(1)public int indexOf(int ch/String str):用于查找当前字符串中字符或子串,返回字符或子串在当前字符串中从左边起首次出现的位置,若没有出现则返回-1。
(2)public int indexOf(int ch/String str, int fromIndex):改方法与第一种类似,区别在于该方法从fromIndex位置向后查找。
(3)public int lastIndexOf(int ch/String str):该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找。
(4)public int lastIndexOf(int ch/String str, int fromIndex):该方法与第二种方法类似,区别于该方法从fromIndex位置向前查找。

7、字符串中字符的大小写转换
(1)public String toLowerCase():该方法返回将当前字符串中所有字符转换成小写后的新串
(2)public String toUpperCase():该方法返回将当前字符串中所有字符转换成大写后的新串

8、字符串中字符的替换
(1)public String replace(char oldChar, char newChar):该方法用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串。
(2)public String replaceFirst(String regex, String replacement):该方法用字符replacement的内容替换当前字符串中遇到的第一个和字符串regex相匹配的子串,应将新的字符串返回。
(3)public String replaceAll(String regex, String replacement):该方法用字符replacement的内容替换当前字符串中遇到的所有和字符串regex相匹配的子串,应将新的字符串返回。

9、String trim():该方法截除字符串两端的空格,但是不处理中间的空格,然后返回一个新的字符串。

10、boolean contains (String s):该方法判断参数s是否被包含在字符串中,即字符串s是否为原字符串的子串,然后返回一个boolean类型的值。

11、Stringp[] split(String str):该方法将str作为分隔符进行字符串分解,分解后的字字符串在字符串数组中返回。

还有许多方法,具体可以查看Java API进行了解。

String的底层数据结构

从底层去了解String类能够让我们更加清晰的了解String类是如何进行运作的,话不多说,上手源码:

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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    /** 使用JDK 1.0.2中的serialVersionUID实现互操作性 */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     * 类字符串在序列化流协议中使用特殊大小写。
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

                    …………
}

我们可以很容易看出,String类存储字符串的方式是通过char型数组存储的,并且value被声明为final不可变,这也就是我们为什么说String类一旦创建出来就是不可变的。

String字符串常量和String对象

String字符串常量和String字符串对象底层都是char型数组,但是在内存存储还是有区别的。因为字符串是Java中使用最多的一种数据结构,因此JVM专门开辟了一个常量池空间对String类型的数据进行优化。

JVM是如何通过这个常量池进行优化的呢?当我们声明一个字符串常量时,JVM首先会查看常量池中是否存在这个常量,如果存在就不会创建新的对象,否则在常量池中创建该字符串并创建引用,以后无论以什么方式创建多少个相同的字符串,JVM都会将这个引用赋给这些对象,而不需要重新开辟新的地址空间给相同的数据。

那么String字符串常量和String对象有什么区别呢?这又涉及到了JVM的内存相关。String对象作为一个对象,是存储在JVM中的堆上的,而String字符串常量则是存储在常量池中。当我们创建一个新的String对象,JVM会直接给这个对象开辟一个新的地址空间,无论这个对象所存储的字符串是否与其他对象存储的字符串相同。并且,由于存储的位置不同,即使一个字符串常量存储的字符串与一个String对象存储的字符串相同,此时两个字符串也是不相等的。

String a = "hello"; //字符串常量
String b = new String("hello"); //String对象
System.out.println(a==b);  //false

但是有一些字符串是否相等的问题需要注意:

String a = "hello";
String b = "he" + "llo";
System.out.println(a == b); //true

JVM在编译期间会自动把字符串常量相加操作计算完毕后再执行比较,由于常量池中已经存在“hello”,因此两者的地址是相同的。

String b = "helloWorld";
    String d1 = "hello";
    String d2 = "World";
    String d = d1 + d2;
    String e = d1 + new String("World");
    String f = "hello" + new String("World");

    System.out.println(b == d); //false
    System.out.println(b == e); //false
    System.out.println(b == f); //false
    System.out.println(d == e); //false
    System.out.println(d == f); //false
    System.out.println(e == f); //false

字符串变量的连接动作,在编译阶段会被转化成StringBuilder的append操作,变量d最终指向JVM堆上新建的String对象。前三个比较都是因为一个在堆上而另一个在常量池里,因此为false。后三个比较则是因为d、e、f是三个String对象,因此三个对象的地址肯定不相同。

这里又涉及一个知识点,为什么在编译阶段JVM要将字符串变量转化成StringBuilder来操作而不是String。如上边的代码,d1和d2在编译阶段最终指向的对象是不可知的,所以不能当做常量(String被final所修饰,使用String等于使用一个常量)来看待,除非给d1、d2加上final关键字,才能当做常量来看待。

String中的intern()

intern()方法的作用在于:当调用 intern 方法时,如果常量池中已经有该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

我们通过下面这个场景来了解intern()方法:

String a = new String("helloJava");
    String a1 = a.intern();
    System.out.println(a == a1);//false


    String b = new String("String")+new String("Tests");
    String b1 = b.intern();
    System.out.println(b == b1);//true

    String c = "helloWorld";
    String c1 = c.intern();
    System.out.println(c == c1);//true

是不是感觉很奇怪,为什么第一个是false第二个是true?除了了解intern()的作用外,我们还需要了解一点:JDK7/8的常量池区域都放在了堆区。没错,以上代码的运行环境都是jdk7以上,jdk6则会出现不同的结果,在这里我们就不深谈。

在了解intern()和运行环境的条件下,我们来看看为什么返回的结果会是这样的。

(1)a 与 a1

执行第一句后,a指向的是堆区字符串的引用,同时在常量池也创建了一份字符串,所以a1指向常量池字符串,引用地址自然不同。

(2)b 与 b1

b只在堆区创建了字符串对象,而并没有在常量池创建字符串常量。因此intern()方法返回的引用依旧是堆区的引用,因此两个引用地址是相等的。如果在intern()前创建一个字符串常量,则intern()方法则会返回常量池中的字符串常量,结果也就会变成false。

(3)c 与 c1

这个例子比较简单,c本身就是常量池中的字符串常量,c1自然也是指向同一个字符串常量。