以主流的JDK1.8版本来说,String内部实际存储结构为final关键字修饰的char数组,源码如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
//用于存储字符串的值
private final char value[];
/** Cache the hash code for the string */
//缓存该字符串的hashcode
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
//使用JDK 1.0.2中的serialVersionUID进行互操作
private static final long serialVersionUID = -6849794470754667710L;
小结:
(1)String是一个final类,说明Sting不能被继承。设计为final类的好处:第一是安全,第二个好处是高效。只有字符串是不可变的类时,我们才能实现字符串常量池(JDK1.7时从方法区移入到堆中了),字符串常量池可以为我们缓存字符串,提高程序的运行效率。
(2)String中保存数据的是一个final修饰的char数组value,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。
以上两点就是String不可变的原因。
// String 为参数的构造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// char[] 为参数构造方法
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
// StringBuffer 为参数的构造方法
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
// StringBuilder 为参数的构造方法
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
//.....还有很多
public boolean equals(Object anObject) {
//1.先判断两个字符串对象的内存地址是否相同,相同返回true
if (this == anObject) {
return true;
}
//2.判断待比较对象是否为String类型,不是返回false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//3.该对象为String类型的话,就判断他们的长度是否相同,不相同就不需要继续比较了
if (n == anotherString.value.length) {
//4.长度相同,就把两个字符串对象的值转化为char数组,然后进行逐个比较!若有一个不等就返回false
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;
}
体会:从 equals 的源码可以看出,逻辑非常清晰,完全是根据 String 底层的结构来编写出相等的代码。这也提供了一种思路给我们:如果有人问如何判断两者是否相等时,我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char 数组中的字符是否相等即可。
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
//1.获取到两个字符串的最小长度
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
//2.对比每一个字符
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
//3.有字符不相等就返回第一个不相同字符ASCII码的差值
if (c1 != c2) {
return c1 - c2;
}
k++;
}
//4.否则就返回两个字符串长度的差值
return len1 - len2;
}
当compareTo返回0时,则表示两个字符串的值相同。
substring 有两个方法:
public String substring(int beginIndex, int endIndex)
beginIndex:开始位置,endIndex:结束位置;public String substring(int beginIndex)
beginIndex:开始位置,结束位置为文本末尾。substring 方法的底层使用的是字符数组范围截取的方法 :Arrays.copyOfRange(字符数组, 开始位置, 结束位置);
从字符数组中进行一段范围的拷贝。
public String substring(int beginIndex, int endIndex) {
//先检查beginIndex和endIndex范围,超出范围之外的就抛出字符串索引越界异常
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//subtring底层其实是调用了String的一个构造函数
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
//来看看这个构造函数
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//其实没什么特别的,就是调用了Arrays.copyOfRange()方法,从字符数组中进行一段范围的拷贝。
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
替换在工作中也经常使用,有 replace 替换所有字符、replaceAll 批量替换字符串、replaceFirst 替换遇到的第一个字符串三种场景。
其中在使用 replace 时需要注意,replace 有两个方法,一个入参是 char,一个入参是 String,前者表示替换所有字符,如:name.replace('a','b')
,后者表示替换所有字符串,如:name.replace("a","b")
,两者就是单引号和多引号的区别。
需要注意的是, replace 并不只是替换一个,是替换所有匹配到的字符或字符串哦
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
拆分我们使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分,我们演示一个 demo:
String s =“boo:and:foo”;
// 我们对 s 进行了各种拆分,演示的代码和结果是:
s.split(" ;//结果:[“boo”,“and”,“foo”]
s.split(":",2) ;//结果:[“boo”,“and:foo”]
s.split(":",5);// 结果:[“boo”,“and”,“foo”]
s.split(":",-2) ;//结果:[“boo”,“and”,“foo”]
s.split(“o”) ;//结果:[“b”,"",":and:f"]
s.split(“o”,2) ;//结果:[“b”,“o:and:foo”]
4.相关面试题:
(1)==和equals()方法的区别?
对于基本类型,==比较的是值;对于引用类型,==比较的是内存地址。
没有被重写的equals()方法,等价于==,比较的是两个对象的内存地址,源码如下
public boolean equals(Object obj) {
return (this == obj);
}
被重写过的euquls方法,例如String的euqals()方法,比较的是对象的值
(2)为什么要用final修饰Sting?final修饰的好处?
第一:不可变,安全。
第二:高效。只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们提高程序运行的效率。
(3)String 、SringBuffer 、StringBuilder的区别?
①可变性:
String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]
,所以 String 对象是不可变的。而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
但是没有用 final 关键字修饰,所以这两种对象都是可变的。StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
②线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
③性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
(4)String的intern()方法?
判断常量池中是否存在s,若存在则返回常量池中的引用,s的指向不会发生更改;如果常量池中不存在该字符串,那么就新建一个这样的字符串放到常量池中。**
返回值:String
使用常量池的方法一个是通过双引号定义字符串例如:String S = “1”;还有就是上面的intern方法。
(5)String和JVM?
String 常见的创建方式有两种,new String() 的方式和直接赋值的方式,直接赋值的方式会先去字符串常量池中查找是否已经有此值,如果有则把引用地址直接指向此值,否则会先在常量池中创建,然后再把引用指向此值;而 new String() 的方式一定会先在堆上创建一个字符串对象,然后再去常量池中查询此字符串的值是否已经存在,如果不存在会先在常量池中创建此字符串,然后把引用的值指向此字符串
小贴士:JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上。
学习参考:https://www.imooc.com/read/47/article/844