String是我们在java中经常使用到的一个引用数据类型,下面我们就来仔细了解下它的底层原理。
首先我们来看看String的源码
String存储的值被final修饰
/** The value is used for character storage. */
private final char value[];
这里我们可以看到,String内部是通过一个final所修饰的字符数组来存储字符串,既然被final所修饰,那么它的值只能被初始化而不能被修改,所以String的值在建立对象的时候就被固定了。
字符串拼接需要注意的地方
String a1 = "a";
a1 = a1 + "b";
如上图所示,既然a1所指向对象的值已在初始化的时候被固定为“a”,那么当执行完 a1 = a1 + “b”,为何 a1所指向的对象的值变为了“ab”呢,原因是 当a1+“b”时,虚拟机会在堆内存再创建一个String对象,同时将“ab”赋给这个对象,然后再让a1这个引用指向这个新创建的String对象,这样,我们就能达到字符串拼接的效果。
在这个地方,我们就明白了当我们需要进行大量的字符串拼接的时候,如果我们直接使用上面的直接相加的方法,那么会导致堆内存中存在大量的不必要的String对象,这显然是不太好的,因此我们引入了StringBuilder这个类,这个类中的字符数组没有被final所修饰,所以他可以不断地修改其中的值,当进行字符串拼接时,我们使用这个类就可以避免创建大量的对象,以减少内存的开销。
String的hash值
/** Cache the hash code for the string */
private int hash; // Default to 0
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的hashcode值是只和String所存入的字符串的值相关的,那么我们可以得出结论,即当引用从String对象中所获取的字符串的值相同时,无论这两个引用所指向的对象是否是同一个String对象,他们的hashcode一定是相同的。
String的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的equals方法,通过源码我们也发现当字符数组中存放的值相同时,equals方法会返回true,那么我们可以利用这个方法或者上面的hashcode方法来判断String对象所存储的值是否相同。
判断是否是同一个对象
当我们需要判断两个引用是否是指向同一个String对象时,用“ == ”即可。
String的几种创建方式及其实现原理
说完上述一些常用方法后,我们来看看下面的这几种String对象创建情况
String a1 = new String("abc");
String a2 = "abc";
String a3 = "abc";
String a4 = new String("abc");
System.out.println(a1==a2);
System.out.println(a2==a3);
System.out.println(a1==a4);
false
true
false
通过上面的实验我们可以看到,a2与a3通过直接用等号赋值的方式传递同样的值,他们所指向的对象都为同一个对象,然而当我们通过使用new String()这种方式赋给同样的值时,我们发现,这些引用所指向的对象不是同一个对象。那么,这种现象是如何产生的呢?下面我们就来说说String对象的多种创建方式的区别。
当我们执行`String a1 = new String("abc");`时,JVM会首先查找方法区的常量池是否存在存放“abc”的地址,如果没有,那么则会创建两个对象,一个是new String()创建出来的存在于堆中的对象,另一个对象会将自己的地址放到常量池中,如果有,则只会创建一个对象,即new String()创建出来的存在于堆中的对象。
当执行`String a2 = "abc";`时,a2同样会去常量池中寻找是否存在存放“abc”的地址,如果有,则直接指向这个地址,如果没有,那么它会创建一个对象并将自己的地址放到常量池中,当下次再执行`String a3 = "abc";`时,a3去常量池寻找则会发现存在存放“abc”的地址,此时a3会与a2相同,都指向同一个“abc”对象。
如此我们就可以很好的解释上面几行代码运行的结果了。
String的一些方法
public boolean equals(Object anObject) 判断字符串是否一样
public boolean equalsIgnoreCase(String anotherString) 判断字符串是否一样,忽略大小写
public boolean contains(CharSequence s) 判断字符串是否包含哪字符串
public boolean startsWith(String prefix) 判断字符串是否以什么开头
public boolean endsWith(String suffix) 判断字符串是否以什么结尾
public boolean isEmpty() 判断字符串是否为空字符串
int length():获取字符串的长度
char charAt(int index):获取指定索引位置的字符
int indexOf(int ch):获取指定字符在此字符串第一次出现处的索引
int indexOf(String str):返回指定字符串在此字符串中第一次出现处的索引
int indexOf(int ch,int fromIndex):返回指定字符在此字符串中指定位置后第一次出现处的索引
int indexOf(String str,int fromIndex):返回指定字符串在此字符串中指定位置后第一次出现处的索引
lastIndexOf() 最后出现的位置
String substring(int start):从指定位置开始截取字符串,默认到未尾
String substring(int start,int end):从指定位置开始到指定位置结束截取字符图
JDK对String常量池进行了修改
JDK1.6中常量池放在方法区中,JDK1.7中JVM把String常量区从方法区中移除了,它将常量池移到了堆中;JDK1.8中JVM把String常量池放入到了元空间,这是一个与堆内存不相连的内存空间。