其实 String 方面的面试题往深了延申的话,还是会延伸到 JVM,所以还是希望读者对 JVM 有一定的了解,这样更便于理解 String 的设计。
String 源码分析
String 结构
/*
Strings are constant; their values can not be changed after they are created.
Stringbuffers support mutable strings.Because String objects are immutable they can be shared. Forexample:
*/
public final class String implements java.io.Serializable,
Comparable<String>, CharSequence
源码里可以看到 String 被 final 修饰并继承了三个接口
源码注释也说到字符串是不变的; 它们的值在创建后无法更改.Stringbuffers 支持可变字符串。
因为 String 对象是不可变的,所以它们可以共享
final
修饰类:类不可被继承,也就是说,String 类不可被继承了
修饰方法:把方法锁定,以访任何继承类修改它的涵义
修饰遍历:初始化后不可更改
Comparable 和 Serializable
Serializable 接口里为空
Comparable 接口里只有一个 public int compareTo(T o); 方法
这两个接口不做叙述.
CharSequence
接口中的方法
length(): int
charAt(): char
subSequence(int,int):CharSwquence
toString(): String
chars(): IntStream
codePoints(): IntStream
我们发现这个接口中的方法很少, 没有我们常用的 String 方法,
那么它应该是一个通用接口, 会有很多实现类, 包括 StringBuilder 和 StringBuffer
String 构造方法
空参构造
public String() {
this.value = “”.value;
}
解析
String str=new String(“abc”);
1. 先创建一个空的 String 对象
2. 常量池中创建一个 abc, 并赋值给第二个 String
3. 将第二个 String 的引用传递给第一个 String
注: 如果常量池中有 abc, 则不用创建, 直接把引用传递给第一个 String
String 类型初始化
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
案例: String str=new String(“str”);
字符数组初始化
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
注: 将传过来的 char 数组 copy 到 value 数组里
字节数组初始化
byte 类型的方法有 8 个, 两个过时的 剩下六个又分为指定编码和不指定编码
不指定编码
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException(“charset”);
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
指定编码
String(byte bytes[], Charset charset)
String(byte bytes[], String charsetName)
String(byte bytes[], int offset, int length, Charset charset)
String(byte bytes[], int offset, int length, String charsetName)
解析
byte 是网络传输或存储的序列化形式,
所以在很多传输和存储的过程中需要将 byte[] 数组和 String 进行相互转化,
byte 是字节,char 是字符, 字节流和字符流需要指定编码, 不然可能会乱码,
bytes 字节流是使用 charset 进行编码的, 想要将他转换成 unicode 的 char[] 数组,
而又保证不出现乱码, 那就要指定其解码方法
StringBUilder 构造
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
注: 很多时候我们不会这么去构造, 因为 StringBuilder 跟 StringBuffer 有 toString 方法
如果不考虑线程安全, 优先选择 StringBuilder
equals 方法
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;
}
String 重写了父类 Object 的 equals 方法
先判断地址是否相等 (地址相等的情况下, 肯定是一个值, 直接返回 true)
在判断是否是 String 类型, 不是则返回 false
如果都是 String, 先判断长度,
再比较值, 把值赋给 char 数组, 遍历两个 char 数组比较
hashcode 方法
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;
}
如果 String 的 length== 0 或者 hash 值为 0, 则直接返回 0
如果上述条件不满足, 则通过算法计算 hash 值
intern 方法
public native String intern();
注: 方法注释会有写到, 意思就是调用方法时,
如果常量池有当前 String 的值, 就返回这个值, 没有就加进去, 返回这个值的引用
String str1=”a”;
String str2=”b”;
String str3=”ab”;
String str4 = str1+str2;
String str5=new String(“ab”);
System.out.println(str5==str3);// 堆内存比较字符串池
//intern 如果常量池有当前 String 的值, 就返回这个值, 没有就加进去, 返回这个值的引用
System.out.println(str5.intern()==str3);// 引用的是同一个字符串池里的
System.out.println(str5.intern()==str4);// 变量相加给一个新值,所以 str4 引用的是个新的
System.out.println(str4==str3);// 变量相加给一个新值,所以 str4 引用的是个新的
false
true
false
false
重点: – 两个字符串常量或者字面量相加,不会 new 新的字符串, 其他相加则是新值,(如 String str5=str1+”b”;)
因为在 jvm 翻译为二进制代码时,会自动优化,把两个值后边的结果先合并,再保存为一个常量。
String 里还有很多方法,substring,replace 等等, 我们就不一一举例了
StringBuilder
StringBuilder 和 Stringbuffer 这两个类的方法都很想一样, 因此我们就那 StringBuilder 的源码作分析
等下再去看三者之间的关系和不同
结构和构造
public final class StringBuilder extends AbstractStringBuilder
implements java.io.Serializable, CharSequence{
// 空构造,初始大小 16
public StringBuilder() {
super(16);
}
// 给予一个初始化容量
public StringBuilder(int capacity) {
super(capacity);
}
// 使用 String 进行创建
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
//String 创建和 CharSequence 类型创建, 额外多分配 16 个字符的空间,
// 然后调用 append 将参数字符添加进来,(字符串缓冲区)
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
}
解析
我们可以看到方法内部都是在调用父类的方法,
通过继承关系, 我们是知道它的父类是 AbstractStringBuilder,
父类里实现类 Appendable 跟 CharSequence 接口,所以它能够跟 String 相互转换
父类 AbstractStringBuilder
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
解析
父类里是只有两个构造方法, 一个为空实现, 一个为指定字符数组的容量,
如果事先知道 String 的长度小于 16, 就可以节省内存空间,
他的数组和 String 的不一样, 因为成员变量 value 数组没有被 final 修饰,
所以可以修改他的引用变量的值, 即可以引用到新的数组对象,
所以 StringBuilder 对象是可变的
append
append 有很多重载方法, 原理都差不多
我们以 String 举例
// 传入要追加的字符串
public AbstractStringBuilder append(String str) {
// 判断字符串是否为 null
if (str == null)
return appendNull();
// 不为 null, 获得它的长度
int len = str.length();
// 调用方法,把原先长度 + 追加字符串长度的和传入方法
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
// 判断是否满足扩展要求
private void ensureCapacityInternal(int minimumCapacity) {
// 和 - 原先字符串长度是否 >0 肯定是大于 0 的
if (minimumCapacity – value.length > 0)
// 调用复制空间的方法,和当参数
expandCapacity(minimumCapacity);
}
// 开始扩充
void expandCapacity(int minimumCapacity) {
// 先把原先长度复制 2 倍多 2
int newCapacity = value.length * 2 + 2;
// 判断 newCapacity- 和是否 <0
if (newCapacity – minimumCapacity < 0)
// 小于 0 的情况就是你复制的长度不够,那就把和的长度给复制的长度
newCapacity = minimumCapacity;
// 正常逻辑怎么着都走不到这一步,新长度肯定是大于 0
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
// 将数组扩容拷贝
value = Arrays.copyOf(value, newCapacity);
}
insert
insert 同样有很多重载方法,下面以 char 和 String 为例
insert 的 ensureCapacityInternal(count + 1); 和上面一样,不做讲解了
public AbstractStringBuilder insert(int offset, char c) {
// 检查是否满足扩充条件
ensureCapacityInternal(count + 1);
// 拷贝数组
System.arraycopy(value, offset, value, offset + 1, count – offset);
// 进行复制
value[offset] = c;
count += 1;
return this;
}
public AbstractStringBuilder insert(int offset, String str) {
// 判断要插入的坐标是否在字符串内,不再则报数组下标越界
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
// 判断要插入的是否为 null
if (str == null)
str = “null”;
// 获得要插入的字符串长度
int len = str.length();
// 检查是否满足扩充条件
ensureCapacityInternal(count + len);
// 拷贝数组
System.arraycopy(value, offset, value, offset + len, count – offset);
str.getChars(value, offset);
count += len;
return this;
}
StringBuffer
public final class StringBuffer extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
}
跟 StringBuilder 差不多,只不过在所有的方法上面加了一个同步锁
equals 与 ==
equals
String 类重写了父类 equals 的方法 我们先看下父类的
// 直接判断地址
public boolean equals(Object obj) {
return (this == obj);
}
再看下 String 类的 equals
public boolean equals(Object anObject) {
// 地址相等肯定为 true, 就不用继续往下走了
if (this == anObject) {
return true;
}
// 地址不相等的情况下,比较两个字符的内容是否一样
// 把字符串方法 char[] 数组里,遍历比较
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;
}
==
== 比较的是内存地址
基本数据类型比较值
引用数据类型比较地址值
(对象的引用,在堆空间,String 在字符串池,newString 在堆空间)
根据下面案例分析一下源码创建方式 | 对象个数 | 引用指向
String a=”abc”; |1 | 常量池 String b=new String(“abc”);; |1 | 堆内存 (abc 则是复制的常量池里的 abc)String c=new String() |1 | 堆内存 String d=”a”+”bc”; |3 | 常量池 (a 一次,bc 一次,和一次,d 指向和)String e=a+b; |3 | 堆内存
重点 – 两个字符串常量或者字面量相加,不会 new 新的字符串, 变量相加则是会 new 新的字符串,new 出来的都在堆
总结
String 被 final 修饰,一旦创建无法更改,每次更改则是在新创建对象
StringBuilder 和 StringBuffer 则是可修改的字符串
StringBuilder 和 StringBuffer 的区别
StringBuffer 被 synchronized 修饰,同步,线程安全
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
如果程序不是多线程的,那么使用 StringBuilder 效率高于 StringBuffer。