其实 String 方面的面试题往深了延申的话, 还是会延伸到 JVM, 所以还是希望读者对 JVM 有一定的了解, 这样更便于理解 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.
来源: https://juejin.im/post/5c0fb9bbe51d451dd867a96a