Java_String(5)

String

字符串是 String 类的对象,String 类被 final 修饰,赋值后就不能再更改,String 类重写了 toString方法,返回的是该对象本身的值,所以可以直接 print

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Test {
  public static void main(String[] args) {
    String s1 = "aaa";
    s1 = "bbb"; // 这时对象"aaa"变成了垃圾,等待被回收
    System.out.println(s1); //bbb

    String s2 = "abc";
    String s3 = "abc";
    //s2,s3两个引用同时指向在常量池的对象"abc"
    System.out.println(s2 == s3); // true
    //String类重写了equals方法,相同字符序列才为true
    System.out.println(s2.equals(s3)); // true

    //将常量池里的"bcd"对象拷贝一份赋值给new出来的对象
    String s4 = "bcd";
    String s5 = new String("bcd");
    System.out.println(s4 == s5); // false
    System.out.println(s4.equals(s5)); // true

    //Java有常量优化机制
    String s6 = "c" + "c" + "c"; // 在编译时就变成了"ccc"
    String s7 = "ccc";
    System.out.println(s6 == s7); // true
    System.out.println(s6.equals(s7)); // true

    String s8 = "dd";
    String s9 = "ddd";
    String s10 = s8 + "d";
    // s9指向常量池里的"ddd"对象,s10指向堆区的"ddd"对象,+会调用StringBuilder的方法
    System.out.println(s9 == s10); // false
    System.out.println(s9.equals(s10)); //true
  }
}

构造方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Test {
  public static void main(String[] args) {
    byte[] arr1 = {97, 98, 99};
    String s1 = new String(arr1); //使用默认编码方式解码byte数组
    System.out.println(s1); //abc

    byte[] arr2 = {97, 98, 99, 100, 101, 102};
    String s2 = new String(arr2, 2, 3); //从索引2开始解码3个元素
    System.out.println(s2); //cde

    char[] arr3 = {'a', 'b', 'c', 'd', 'e'};
    String s3 = new String(arr3, 1, 4); //从索引1开始转换4个
    System.out.println(s3); // bcde
  }
}

类方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
boolean equals()           //比较字符串内容是否相同,区分大小写 
boolean equalsIgnoreCase() //比较字符串内容是否相同,忽略大小写
boolean contains()         //判断字符串中是否包含某个字符串
boolean startsWith()       //判断字符串是否以某个字符串开头
boolean endsWith()         //判断字符串是否以某个字符串结尾
boolean isEmpty()          //判断字符串是否为空

int length()               //数组length属性,字符串length方法,返回元素个数
char charAt(int index)     //获取索引位处的字符
int indexOf(int c)         //找索引,如果不存在则返回-1
int indexOf(String str) 
int indexOf(int c, int fromIndex) //从fromIndex开始往后找
int indexOf(String str, fromIndex)
//从start位置开始截取字符串直到末尾  
String substring(int start)       
//截取start到end区间的字符串,包括start不包括end,会新建一个字符串保存结果
String substring(int strat, int end)

byte[] getBytes()                 //把字符串转换成字符数组
char[] toCharArray()              //把字符串转换为字符数组
String valueOf(char[] cs)         //把字符数组转换成字符串
String valueOf(int i)             //把int类型的数据转换成字符串
//通过String valueOf(基本数据类型)方法把基本数据类型的值转换为字符串
   
String toLowerCase()       //把字符串转换成小写
String toUpperCase()       //把字符串转换成大写
String concat(String str)  //只用于拼接字符串,+可以将字符串与任意类型相加

hashCode 和 equals

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Test {
  public static void main(String[] args) {
    String s1 = "张三";
    //常量池中已经有了张三,但new还是会在堆中创建一个String对象
    String s2 = new String("张三");
    System.out.println(s1 == s2);      //fasle  两个引用指向的地址不同,两个不同对象
    System.out.println(s1.equals(s2)); //true
    System.out.println(s1.hashCode()); //774889 字符串值相同则hashCode就相同
    System.out.println(s2.hashCode()); //774889
  }
}

查看 String 类重写的 hashCode 方法源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public int hashCode() {
  int h = hash;
  if (h == 0 && value.length > 0) {
    char val[] = value;
    for (int i = 0; i < value.length; i++) {
      h = 31 * h + val[i];
    }
    hash = h;
  }
  return h;
}

31 是一个不大不小的质数,是哈希算法的优选乘子(方法上面的注释有给出对应的计算公式),并且 31 可以优化为 (1 « 5) - 1,coolblog的文章 有进行详细地介绍

String 类也重写了 equals 方法,进行的是两个字符串的逐字比较

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

字符串常量和 intern 方法

通过字面量赋值创建的字符串,例如 String s = "abc"; 会先在常量池中查找是否存在相同的字符串,若存在则将引用直接指向该字符串,若不存在则在常量池中生成一个,再将引用指向该字符串

Java 不提供运算符的重载,对于两个字符串的 + 操作其实只是一个语法糖,在编译阶段进行字符串的拼接,例如 String s= "Ja" + "va";,在编译时直接被拼接成 String s= "Java"; 然后再去常量池中查找是否存在从而进行创建或引用

需要注意的是通过 + 行拼接,实际调用的是 StringBuilder 的 append 方法,会在堆里创建对象,将在下面讨论 StringBuilder 时再详细讨论这个问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {
  public static void main(String[] args) {
    String s1 = "abc";
    String s2 = "abc";
    System.out.println(s1 == s2); //true,均指向常量池中对象

    String s3 = new String("abc");
    String s4 = new String("abc");
    System.out.println(s3 == s4); //false,两个引用指向堆中的不同对象
    
    String s5 = "abc";
    String s6 = "a";
    String s7 = "bc";
    String s8 = s6 + s7;
    //s6 + s7是通过StringBuilder.append()实现的,会生成不同的对象
    System.out.println(s5 == s8); //false

    String s9 = "abc";
    final String s10 = "a";
    final String s11 = "bc";
    String s12 = s10 + s11;
    //s10和s11被final修饰,在编译时会被替换为各自的常量值,而常量的+拼接在编译时就完成了 
    System.out.println(s9 == s12); //true
  }
}

new String() 会在堆上创建字符串对象,intern 方法的作用就是改变引用指向的位置到常量池,多余的对象就可以被回收了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Test {
  public static void main(String[] args) {
    String s13 = "abc";
    String s14 = new String("abc").intern();
    //调用了intern方法,发现常量池中已经有"abc"了,则将s14引用指向常量池中的"abc"
    //至于这个新new的String对象就变成了垃圾,等待被回收
    System.out.println(s13 == s14); //true

    String s15 = new String("abc");
    String s16 = new String("abc");
    //没有调用intern方法,则多出了两个字符值都为"abc"的String对象
    //因为有引用指向着,对象不会被回收
    System.out.println(s15 == s16); //false
  }
}

JDK1.6、JDK1.7、JDK1.8 常量池一直在变化,具体内容在 JVM 的部分进行讨论

不可变设计

1
2
3
public final class String {
  private final char value[];
}

String 类被 final 修饰,即 String 不能继承,value char[] 数组也被 final 修饰,即引用 value 的地址值不可变,但引用指向的 char 数组是可变的

value 被 private 修饰且没有提供访问方法,String 类中的方法也没有改动 value,String 本身也被 final 修饰,防止继承后 value 被修改

设计为不可变的目的是保证多线程安全、类加载安全、常量池管理,缺点就是会产生垃圾

但其实可以通过反射来修改 value

StringBuffer

构造方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Test {
  public static void main(String[] args) {
    //StringBuffer对象底层是一个长度为16的字符数组toStringCache
    //空参构造默认初始分配16个字符大小的空间
    StringBuffer sb1 = new StringBuffer();
    StringBuffer sb2 = new StringBuffer(10);
    System.out.println(sb1.length()); //返回容器中字符个数,0
    System.out.println(sb1.capacity()); //返回容量,16
    System.out.println(sb2.capacity()); //10

    StringBuffer sb3 = new StringBuffer("hello, world");
    System.out.println(sb3.length()); //12
    //16 + 12 = 28,字符串参数长度 + 默认初始容量
    System.out.println(sb3.capacity()); //28
  }
}

类方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test {
  public static void main(String[] args) {
    //stringBuffer也重写了toString()方法
    StringBuffer sb = new StringBuffer();
    //append()方法可以将任意数据类型添加到字符串缓冲区,返回字符串缓冲区本身
    StringBuffer sb1 = sb.append(true);
    System.out.println(sb1); //true
    StringBuffer sb2 = sb.append("hello, world");
    System.out.println(sb2); //truehello, world
    StringBuffer sb3 = sb.append(100);
    System.out.println(sb3); //truehello, world100
    //sb, sb1, sb2, sb3 指向的是同一个对象
    System.out.println(sb); //truehello, world100

    StringBuffer sb4 = new StringBuffer("1234");
    //在3索引处插入
    sb4.insert(3, "hello");
    System.out.println(sb4); //123hello4
    //根据索引删除字符,包含头不包含尾
    sb4.delete(0,3);
    System.out.println(sb4); //hello4
    sb4.delete(0, sb4.length()); //清空缓冲区

    StringBuffer sb5 = new StringBuffer("1234");
    //根据索引替换字符串,含头不含尾
    sb5.replace(0, 4, "abcd");
    System.out.println(sb5); //abcd
    //翻转字符串
    sb5.reverse();
    System.out.println(sb5); //dcda
    //截取字符串,返回String类型,不再是StringBuffer类型
    //不限制结束索引默认到末尾,不改变原序列
    String str = sb5.substring(1, 3);
    System.out.println(str); //cb
    System.out.println(sb5); //dcda
  }
}

String 和 StringBuffer 转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Test {
  public static void main(String[] args) {
    //通过构造方法,将String转换为StringBuffer
    StringBuffer sb1 = new StringBuffer("hello, world");

    //通过append()将String转换为StringBuffer
    StringBuffer sb2 = new StringBuffer();
    sb2.append("hello, world");

    //将StringBuffer转换为String
    StringBuffer sb = new StringBuffer("hello");
    String str1 = new String(sb);
    String str2 = sb.toString();
    String str3 = sb.substring(0, sb.length());
  }
}

数组转换为字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
  public static void main(String[] args) {
    int[] arr = {1, 2, 3};
    String str = array2String(arr);
    System.out.println(str); //[1, 2, 3]
  }
  public static String array2String(int[] arr) {
    //使用StringBuffer将数组转换成字符串,只用创建一个对象,没有垃圾
    StringBuffer sb = new StringBuffer();
    sb.append("[");
    for (int i = 0; i < arr.length; i++) {
      if (i == arr.length - 1) {
        sb.append(arr[i] + "]");
      } else {
        sb.append(arr[i] + ", ");
      }
    }
    return sb.toString();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//使用String每次赋值都会产生垃圾
public static String array2String(int[] arr) {
  String s = "[";
  for (int i = 0; i < arr.length; i++) {
    if (i == arr.length - 1) {
      s = s + arr[i] + "]";
    } else {
      s = s + arr[i] + ",";
    }
  }
  return s;
}

String 和 StringBuffer 传参

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
  public static void main(String[] args) {
    String s = "hello";
    //将hello值传入函数赋值给形参,函数执行完弹栈,对s = "hello"没有影响
    //传入String引用的拷贝,函数通过引用值即对象地址能获取到s对象
    changeString(s);
    System.out.println(s); //hello
    //这里不变的原因是引用s指向的地址没变,且地址对应的对象也没变

    StringBuffer sb = new StringBuffer("hello");
    //传入StringBuffer对象引用的拷贝,函数通过引用值即对象地址能获取到sb对象
    changeStringBuffer(sb);
    System.out.println(sb); //hello world
  }

  public static void changeString(String s) {
    //根据传入的引用获取到s对象即"hello"
    s += " world";
    //引用s被赋值为+拼接后产生的新对象"hello world"的地址,但没有影响到main中的引用s
  }
  public static void changeStringBuffer(StringBuffer sb) {
    sb.append(" world");
  }
}

StringBuilder

String 是不可变字符序列,StringBuffer 和 StringBuilder 都是可变字符序列

Stringbuffer 是 jdk1.0 就有的,在大部分涉及字符串修改的操作上加了 synchronized 关键字来保证线程安全,效率较低

StringBuilder 是 jdk1.5 新增的,线程不安全,效率较高

StringBuilder 的类方法与 StringBuffer 的基本相同

若字符串相加操作较少则使用 + 拼接的方式,若字符串相加操作较多则使用 StringBuilder,StringBuffer 基本不用

1
2
3
4
5
6
7
8
9
public class Test {
  public static void main(String[] args) {
    String s = "";
    for (int i = 0; i < 10; i++) {
      s += "a";
    }
    System.out.println(s);
  }
}

通过 + 拼接字符串时,会先把 String 转换成 StringBuilder,通过调用其 append 方法实现拼接,之后再通过 toString 方法返回 String,如果大量使用 + 拼接,则会大量地生成 StringBuilder 对象,这时就应该用 StringBuilder 代替 String,只用创建一个 StringBuilder 对象

1
2
3
4
5
6
7
8
9
public class Test {
  public static void main(String[] args) {
    StringBuilder s = new StringBuilder();
    for (int i = 0; i < 10; i++) {
      s.append("a");
    }
    System.out.println(s.toString());
  }
}

如果能保证 + 拼接全部在一条语句中,就只会产生一个 StringBuilder 对象,编译器还会对其优化,在编译后直接就拼接成一个完整的字符串

1
String s = "a" + "a" + "a" + "a" + "a" + "a" + "a" + "a" + "a" + "a"; 

以上内容是玉山整理的笔记,如有错误还请指出