1.字符串概述

什么是字符串

字符串就是Unicode字符串序列,每个用双引号括起来的字符串都是String类的实例。

字符串基本操作

获得子串
substring方法,substring方法的第一个参数是字符串字符的开端,第二个参数是不想复制的第一个位置。
s.substring(0,3); // 得到位置为0,1,2的字符。
调用s.substring(a,b);方法返回字符串的长度为b-a。

字符串拼接
+

不可变长字符串

在java中String类对象是不可变对象,不过可以修改字符串变量,让它的引用指向另一个字符串。

不可变长字符串的一个特点就是编译器可以让字符串共享,这种共享的效率高于提取字串和拼接字串。

检测字符串是否相等

如果虚拟机始终将相同的字符串共享,就可以使用==运算符检测是否相等。但实际上只有字符串字面量是共享的,通过+和substring等操作得到的字符串并不共享(实际上他们都创建了新的对象)。

可以使用equals方法检测两个字符串是否相等。

空串

空串也是一个对象,只不过是串长度为0,内容为空的字符串。

2.String进阶

2.1.String的构造函数

String类的构造函数源代码

  1. // The value is used for character storage.
  2. // value数组被用来存储字符
  3. private final char value[];
  4. // 无参构造函数,返回空字符串的value数组
  5. public String() {
  6. this.value = "".value;
  7. }
  8. // 有参构造
  9. public String(String original) {
  10. this.value = original.value;
  11. this.hash = original.hash;
  12. }

字符串实际上就是对字符数组的封装。

String类的一些方法
像replace, substring,toLowerCase等方法,都有返回值,实际上他们都是在内部创建了一个对象,然后进行操作,返回值。


2.2.+

在java中可以通过+返回字符串的拼接。Java语言为“+”连接符以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。其中字符串连接是通过 StringBuilder(或 StringBuffer)类及其append 方法实现的,对象转换为字符串是通过 toString 方法实现的,该方法由 Object 类定义,并可被 Java 中的所有类继承。

也就是说在两个字符串拼接时,会创建一个StringBuilder对象完成两个字符串的拼接。通过反编译可以得知底层的代码是这样的。

  1. /**
  2. * 测试代码
  3. */
  4. public class Test {
  5. public static void main(String[] args) {
  6. int i = 10;
  7. String s = "abc";
  8. System.out.println(s + i);
  9. }
  10. }
  11. /**
  12. * 反编译后
  13. */
  14. public class Test {
  15. public static void main(String args[]) { //删除了默认构造函数和字节码
  16. byte byte0 = 10;
  17. String s = "abc";
  18. System.out.println((new StringBuilder()).append(s).append(byte0).toString());
  19. }
  20. }
  21. // append的其中一个重载方法
  22. // public StringBuilder append(Object obj) {
  23. return append(String.valueOf(obj));
  24. }

使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意堆内存创建的对象太多,会影响程序的效率,所以在由大量字符串拼接的程序中应该先创建StringBuilder对象,再做字符串的拼接。

还有一种情况,如果再程序的编译阶段+两边都是字符串时,编译器会进行相应的优化,直接将两个字符串常量拼接好。

  1. String s = "a"+"b";
  2. // 反编译之后就是
  3. // String s = "ab";

看一个编译器能够判断的例子

  1. /**
  2. *
  3. * 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
  4. * 所以此时的"a" + s1和"a" + "b"效果是一样的。故结果为true。
  5. */
  6. String s0 = "ab";
  7. final String s1 = "b";
  8. String s2 = "a" + s1;
  9. System.out.println((s0 == s2)); //result = true

看一个编译器不能判断的例子

  1. /**
  2. * 编译期无法确定
  3. * 这里面虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定
  4. * 因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
  5. */
  6. String s0 = "ab";
  7. final String s1 = getS1();
  8. String s2 = "a" + s1;
  9. System.out.println((s0 == s2)); //result = false
  10. public String getS1() {
  11. return "b";
  12. }

也就是说,如果再编译阶段编译器能够确定+两边是两个字符串,就会做出相应的优化,从而提高程序的效率。如果在运行期间才能确定,编译器不做优化。

2.3.字符串常量池

在Java的内存分配中,总共3种常量池,分别是Class常量池、运行时常量池、字符串常量池。

字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。

当执行String s1 = “AB”时,JVM首先会去字符串常量池中检查是否存在”AB”对象,如果不存在,则在字符串常量池中创建”AB”对象,并将”AB”对象的地址返回给s1;如果存在,则不创建任何对象,直接将字符串常量池中”AB”对象的地址返回给s1。

如果通过new的方式创建对象,堆中肯定有一个Obj,如果字符串常量池中有对应的字符串,则它的引用值赋值给Obj,Obj的引用值复制给String对象。


2.4.intern方法

直接使用双引号声明出来的String对象会直接存储在字符串常量池中,如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。

  1. String s1 = "AB";
  2. String s2 = new String("AB");
  3. System.out.println(s1 == s2.intern());
  4. // 结果是true

String.intern() 方法时,如果存在堆中的对象,会直接保存对象的引用并返回,而不会重新创建对象。

  1. String s1 = "A";
  2. System.out.println(s1 == s1.intern());
  3. // 结果不相等

所以我们可以调用intern方法手动将String对象加入到字符串常量池中。


2.5.String,StringBuilder和StringBuffer

继承结构:

1.png

三者的区别:
String是不可变字符序列,StringBuilder和StringBuffer是可变字符序列。
执行速度StringBuilder > StringBuffer > String。
StringBuilder是非线程安全的,StringBuffer是线程安全的


2.6.字符串的不可变性

如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态指的是不能改变对象内的成员变量
数据类型不可变。
引用类型的变量不能指向其他的变量。
引用类型指向的对象的状态不能改变。
任何使成员变量获得新值的函数都应该将新的值保存在新的对象中,而保持原来的对象不被修改。