1 String 底层实现
String 被声明为 final,因此它不可被继承。
底层是 char 或 byte 类型的 value 数组,value 数组也被声明为 final,这意味着数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
编辑器可让字符串共享在常量池。
1.1 Java 8 - char 数组
在 Java 8 中,String 内部使用 char 数组存储数据。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
1.2 Java 9 - byte 数组
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder
来标识使用了哪种编码。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}
1.3 不可变的好处
- 可以缓存 hash 值
- 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
- String Pool 的需要
- 如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
- 安全性
- String 经常作为参数,String 不可变性可以保证参数不可变。
- 例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。
- 线程安全
- String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
2 StringBuilder 和 StringBuffer
2.1 可变性与线程安全
- String 不可变,因此是线程安全的
- StringBuilder 可变,不是线程安全的,效率比 StringBuffer 高
- StringBuffer 可变,是线程安全的,内部使用 synchronized 进行同步
2.2 底层实现
StringBuilder 和 StringBuffer 继承了 AbstractStringBuilder,AbstractStringBuilder 的 char 数组没有 final 关键字修饰,字符数组长度可变,所有 StringBuilder 和 StringBuffer 也是可变的
AbstractStringBuilder
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
StringBuilder
public StringBuilder() {
// StringBuilder 类继承 AbstractStringBuilder 抽象类
// 创建长度 16 的字符数组
super(16);
}
// 字符串拼接
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
3 String Pool 与引用
字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。
在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
- new String():会在堆新建对象
- intern() 方法: String 对象的 intern 方法会得到字符串对象在常量池中对应的版本的引用,如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
- ""字面量:使用字面量的形式创建字符串,会自动地将字符串放入 String Pool 中
String s1 = "Programming";//先去常量池取,没有就新建对象放在常量池
String s2 = new String("Programming");//两个字符串对象,一个是常量池的"Programming",一个是用 new 创建在堆上的对象
String s3 = "Program";//常量池
String s4 = "ming";//常量池
String s5 = "Program" + "ming";//常量池
String s6 = s3 + s4;//堆
System.out.println(s1 == s2);//false
System.out.println(s1 == s5);//true
System.out.println(s1 == s6);//false
System.out.println(s1 == s6.intern());//true
System.out.println(s2 == s2.intern());//false
4 String 常用方法
4.1 初始化
1)使用字符串常量直接初始化
String s = "hello!";
2)使用构造方法创建并初始化
String s = new String(Object);
初始化源码
private final char value[];
// 本质是字符数组常量,所以不可变
public String() {
this.value = "".value;
}
4.2 操作
1)截取字符串
- 单点截取:subString(开始下标)
- 双点截取:subString(开始下标,结束下标)
2)拼接字符串
- + 号
- join:用一个定界符分割,String.join(“定界符”,“待合并字符串”…)
字符串的 + 操作其本质是创建了 StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用 toString 方法处理成 String 对象
一般情况进行字符串拼接用 + 就可以,但是如果是循环拼接,则需要用 StringBuilder 的 append 来实现。
若不使用 StringBuilder 的 append 方法而使用 + 来进行连接。那么每次在循环体内都将会在 Heap 中创造一个新的 String 对象,造成资源浪费。
3)获取信息
- 下标:indexOf(子字符),lastIndexOf(子字符)
- 字符:charAt(下标)
- 字节数组:getBytes()
- 字符数组:toCharArray()
- 长度:length()
4)替换字符串
- 去掉前后空格:trim()
- 子字符串:split(字符串),StringTokenizer()
5)判断字符串
- 是否相等:.equals()
- 不区分大小写:.equalsIgnoreCase()
- 是否为空串:.length()==0 或 .equals(“”)
- 是否为null:== null
- 前缀:startsWith(前缀)
- 后缀:endsWith(后缀)
- 大小:compareTo()
6)字符串转换
- 大/小写:
- 大写:toLowerCase()
- 小写:toUpperCase()
- 字符串转换为基本类型
- Long.parseLong(“1231”);
- Double.parseDouble(“0.213”);
- 基本类型转换为字符串
- 基本数据类型变量 + “”
- String.valueOf(其他类型的参数);