一、概述
String类在java中是一个比较特殊的存在,它虽然不是基本类型,但是使用频率却非常高。这主要是因为String对象的值是一个常量,为什么是常量在下文源码中我们可以看到,正是因为它这一特性,所以它是线程安全的。接下来就让我们深入分析源码,揭开String类的神秘面纱!
二、源码
(1) 类的定义,源码如下
//String是final类型的,属于不可覆盖类型
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
我们可以看到,String类被关键字final修饰,上一篇将Object讲到, final修饰的类,为最终类,该类不能被继承 。同时它还实现了serializable接口、comparable接口和charSequence接口。那么实现这三个接口有什么作用呢?
* 实现serializable接口:serializable接口也和 Cloneable接口一样,是JVM的标记接口, 如果要序列化某些类的对象,这些类就必须实现Serializable接口。 没有implements Serializable,你就不能通过rmi(包括ejb)提供远程调用。ava的RMI(remote method invocation).RMI允许象在本机上一样操作远程机器上的对象。当发送消息给远程对象时,就需要用到serializaiton机制来发送参数和接收返回直。 Java的JavaBeans. Bean的状态信息通常是在设计时配置的。Bean的状态信息必须被存起来,以便当程序运行时能恢复这些状态信息。这也需要serializaiton机制。serialization 允许你将实现了Serializable接口的对象转换为字节序列,这些字节序列可以被完全存储以备以后重新生成原来的对象。 serialization不但可以在本机做,而且可以经由网络操作(就是猫小说的RMI)。这个好处是很大的。
* 实现comparable接口: 此接口强行对实现它的每个类的对象进行整体排序,这种排序被称为类的自然排序。String实现这个接口的目的是为了重写compareTo方法,源码在下文解析。
* 实现charSequence接口:实现charSequence提供的length()、charAt()、chars()方法获取IntStream流等。
(2) 成员变量如下:
//构造器接收的字符串全部都存在了char类型的数组中(1.9后用byte数组,在不同的字节编码下节省空间)了,有因为value是final类型的,所以不可变
private final char value[];
// 用来存放String类对象经过hashcode()得到的hash值
private int hash; // Default to 0
// 实现序列化的标识
private static final long serialVersionUID = -6849794470754667710L;
(3) 构造函数,主要看一下以下五个构造函数源码:
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.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);
}
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
注意:
* 当我们用String str=new String()时,产生的是一个“”空字符串,而不是null。
* 因为jdk1.8及以前,String类里都是采用char数组来存储,所以可以看到调用构造函数传char数组时,构造函数内采用Array.copyof方法来直接复制内容。
* 我们也常用new String("字符串".getBytes("iso-8859-1"),"utf-8")方法来解码字符串。
(4) 重要方法解析,首先就是我们的equals(Object anObject)方法 ,源码如下:
//判断的是内容是否相等,先判断,对象是否相等,因为字符串对象无法修改,字符数组间的比较
public boolean equals(Object anObject) {
//判断是否是同一个引用对象
if (this == anObject) {
return true;
}
//判断是否是String类型
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++;
}
//不相等直接返回true
return true;
}
}
return false;
}
我们发现String类重写了equals方法,那么下面必定是重写了hashcode方法的。Hashcode是用于散列数据的快速存取,如利用HashSet/HashMap/HashTable类来存储数据时,都是根据存储对象的hashcode值来判断是否相同。如果我们对一个对象重写了 equals方法,意思是只要对象的成员变量的值相等那么equals就返回true,但不重写hashcode方法,那么我们再new一个新的对象的时 候,当原对象.equals(新对象)等于true的时候,两者的hashcode值是不相等的。由此产生了理解上的不一致,比如在存储散列集合(如 Set类)的时候,将会存储了两个一样的对象,导致混淆,因此,也就必须重写hashcode方法。
通俗的讲是为了hashmap或者hashset避免逻辑上冲突吧,比如你创建一个Map然后new一个student对象,赋唯一属性,如姓名身高体重,然后给定学号map.put(stu,23),当你get(stu)就能得到该学生学号,这时候你又new了一个一模一样的人也是就student,如果不重写hashcode,那么会默认用hash地址算法,得到的不一样的hashcode值,这时候你get这个学生的学号就无法get到了,因为hashcode不同。
(5) hashCode()方法源码如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
//hash值的计算过程,不同于Object类是直接使用本地方法根据存储地址产生的
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
这里采用31作为计算常数, 是因为31是一个奇质数,所以31*i=32*i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。
(6) charAt(int index)方法源码如下:
//根据索引值,获得值,就是字符数组的获取
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
(7) compareTo(String anotherString) 和 compareToIgnoreCase(String str) 方法
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
我们可以清晰看到判断逻辑,挨着比较char字符的值大小,不同就返回写在前面的字符串在具体坐标位置上和目标字符串相同位置上的char字符差值,不会再比较后面的char字符。如果比较的长达达到了两个字符串的最小长度都没出结果,就直接返回前面字符串的长度减去后面支付串长度的差值。
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
此方法与compareTo()方法与compareTo()方法一致,只是忽略了大小写。CASE_INSENSITIVE_ORDER是源码里定义的比较器,源码如下:
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
(8) concat(String str)方法
public String concat(String str) {
//新串的长度
int otherLen = str.length();
//如果新字符串长度为0,则直接返回原字符串
if (otherLen == 0) {
return this;
}
//原字符串的长度
int len = value.length;
//存放最终字符串的字符数组(长度为len+otherlen),通过Arrays类的copyOf方法复制源数组
char buf[] = Arrays.copyOf(value, len + otherLen);
//通过getChars方法将拼接字符串拼接到源字符串中,然后将新串返回。
str.getChars(buf, len);
return new String(buf, true);
}
API对此方法也做出了解释:
* 如果参数字符串的长度为 0,则返回此 String 对象。
* 否则,创建一个新的String对象,用来表示由此 String 对象表示的字符序列和参数字符串表示的字符序列连接而成的字符序列。
(9) indexOf(String str) 和 indexOf(String str, int fromIndex)方法
public int indexOf(String str) {
return indexOf(str, 0);
}
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
//核心实现代码 用static修饰是因为AbstractStringBuilder类的indexOf(String str, int fromIndex)调用这个方法,但是他是其他类的方法只能通过静态的方式调用,它是为了别的类调用方便
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
//找到需要遍历的最大位置,因为我们可能不需要一直遍历到最后
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
//这里我觉得写的比较好,找到第一个相等字符的位置,不相等就一直加,注意边界
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
//再次判断下边界,如果大于边界就可以直接返回-1了
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
//这个循环找到和目标字符串完全相等的长度
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
//如果完全相等的长度和目标字符串长度相等,那么就认为找到了
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
这个方法首先会查找子字符串的头字符在此字符串中第一次出现的位置,再以此位置的下一个位置开始,然后将子字符串的字符依次和此字符串中字符进行比较,如果全部相等,则返回这个头字符在此字符串中的位置;如果有不相等的,则继续在剩下的字符串中查找,继续进行上面的过程,直到查找到子字符串或没有找到返回-1为止。
(10) split(String regex) 和 split(String regex, int limit)方法,正则匹配
public String[] split(String regex) {
return split(regex, 0);
}
public String[] split(String regex, int limit) {
char ch = 0;
//1. 如果regex只有一位,且不为列出的特殊字符;
//2.如regex有两位,第一位为转义字符且第二位不是数字或字母,“|”表示或,即只要ch小于0或者大于9任一成立,小于a或者大于z任一成立,小于A或大于Z任一成立
//3.第三个是和编码有关,就是不属于utf-16之间的字符
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
//1.limit 是要输入一个数值,这个数值n如果 >0 则会执行切割 n-1次,也就是说执行的次数不会超过输入的数值次.数组长度不会大于切割次数.输入limit为数字1,切割执行1-1次 ,也就是0次,所以切割后的数组长度仍然是1,也就是原来的字符串
//2.如果输入的limit数值是非正数,则执行切割到无限次,数组长度也可以是任何数值
//3.如果输入limit数值等于0,则会执行切割无限次并且去掉该数组最后的所有空字符串
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// 添加最后一个遗留的参数
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// 返回结果,下面在继续判断limit的值为0或者不为0
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
//split()方法在非特殊情况情况下是调用Pattern进行分割处理的。通过这个类实现正则匹配
return Pattern.compile(regex).split(this, limit);
}
Pattern类是理解为模式类,创建一个匹配模式,构造方法私有,不可以直接创建,但可以通过Pattern.complie(String regex) 简单工厂方法创建一个正则表达式。
(11) replace(char oldChar, char newChar) 方法
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++;
}
////使用String的构造方法进行重新创建String
return new String(buf, true);
}
}
return this;
}
(12) substring(int beginIndex) 和 substring(int beginIndex, int endIndex)方法
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
//endIndex不可以大于数组的字符串的长度;
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
//endIndex >= beginIndex && endIndex <= str.length()否则,角标越界异常:StringIndexOutOfBoundsException
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//返回字符串的时候,包括beginIndex位置的元素,但不包括endIndex位置的元素
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
我们可以看出,下面的方法是包含了上面一个方法的判断条件的。并且如果改变了,都会在最后返回一个new的String对象。返回字符串的时候,包括beginIndex位置的元素,但不包括endIndex位置的元素
(13) intern()方法
public native String intern();
此本地方法执行思路:判断这个常量是否存在于常量池。如果存在 , 判断存在内容是引用还是常量, 如果是引用, 返回引用地址指向堆空间对象, 如果是常量, 直接返回常量池常量 ; 如果不存在, 将当前对象引用复制到常量池,并且返回的是当前对象的引用 。
三、总结
敬请期待《我的jdk源码(三): AbstractStringBuilder类》。
来源:oschina
链接:https://my.oschina.net/qq785482254/blog/4277676