人不丑话也不多,直接开始啦~
一、结构预览
由于String包含的方法实在太多,因此这里就不列出它的结构树了。给大家看一下本篇博客的主要内容的目录吧。
1. String类属性
2. String成员变量
3. String构造器
4. String是如何重写equals()的
5. String常用的工具方法
二、正文
1. String类属性
首先看一下String类的定义:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
可以看到String继承了序列化接口、Comparable接口以及CharSequence 接口
Serializable:序列化接口,至于为什么要实现这个接口,个人觉得可能与数据的存储与传输有关。由于String的应用范围实在是太广了,例如我们常用的数据库连接信息、http、socket端口和IP等都是字符串,又比如redis、memcache等缓存服务也使用了String作为缓存类型,而这些应用场景,都与序列化密不可分。以上属于个人理解,不当之处,可以指出。
Comparable:用于比较字符串的大小,具体的实现方法在下文。
CharSequence:有序的字符集,CharSequence就是字符序列,String, StringBuilder和StringBuffer本质上都是通过字符数组实现的,CharSequence 是 char 值的一个可读序列。此接口对许多不同种类的 char 序列提供统一的只读访问。
2. String成员变量
private final char value[];
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
所以,一个字符串是由一个序列化标识、散列值、储存字符串的char数组 组成的。
注意:一个空字符串的散列值为0,原因在下文介绍
3. String构造器
可以看到,构造一个String的方法是在是太多了,这里就不做介绍了,主要说一下下面几点:
3.1 String str = "" 与 new String()
这里要提到java中的一个概念:常量池。那么什么是(String)常量池呢?
由于Java中String的应用是在是太广泛了,可以说是随处可见。为了避免资源的浪费,重复的去创建相同的字符串,所以Java运行时会维护一个String Pool(String池), 也叫“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。
使用String str = ""去构造一个字符串时,会去常量池中去查找池中是否存在这样一个字符串,如果存在,则直接引用此字符串,如果不存在,再去创建一个新的字符串并维护到池中。
new String()则不进行判断,直接创建一个新的字符串。示例如下:
String str1 = "helloworld";
String str2 = new String("helloworld");
String str3 = "hello"+"world";
System.out.println(str1 == str2); //运行结果为:false
System.out.println(str2 == str3); //运行结果为:false
System.out.println(str1 == str3); //运行结果为:true
3.2 intern
public native String intern();
这里顺带提一下intern这个方法,它的作用是返回当前String的内存地址或字符串。当调用intern方法时,如果池中已经包含一个与该String
确定的字符串相同equals(Object)
的字符串,则返回该字符串。否则,将此String
对象添加到池中,并返回此对象的引用。
String str1 = "helloworld";
String str2 = str1.intern();
System.out.println(str2); //打印helloworld
4 equals与hashCode
在上篇博客中,我们知道如果重写equals方法,那么必须重写hashCode方法。String对Object的equals进行了重写,使其判断的是字符串内容是否相等,而不是引用地址。所以String的hashCode也是对字符串的内容进行hash。
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;
}
重写后的equals,首先对其引用地址进行判断,如果不同,再判断其类型。在满足类型相同的条件下,获取其存储字符串的变量value,首先对长度进行判断,这样意味着“helloworld”与“hello world”是不相等的。如果长度相同,再通过循环去比较value数组的每一个元素是否相同,只要存在一个元素不同,返回false。
上文说过,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;
}
注意这里的判断是如果hash为0或者字符串长度>0,才会去计算hash值,否则返回默认的hash值0(int的默认值为0)。
关于这里为何使用31,而不是定义其他的常量,网上大致的解释有两点,这里仅做参考:
a. 31本身就是几个优质的质因数之一,使用31能够减少hash碰撞
b. 31=2<<5-1,效率故而较高
5. String常用的方法
5.1 isEmpty
public boolean isEmpty() {
return value.length == 0;
}
用于判断字符串是否为空,不包括null的情形,因此使用这个方法前需要去考虑它是否为null。
5.2 charAt
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
用于获取指定位置的字符,使用该方法需要对字符串长度进行判断,否则有可能出现下标越界的异常。
5.3 compareTo
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;
}
比较两个字符串的大小,本质上是比较阿斯克码的大小,通常用于比较排序。需要注意的是,通过循环的内容,我们可以发现排序的优先级是从前往后的,当靠前的字符不相等时,就会直接返回,不再继续比较。
5.4 indexOf
public int indexOf(int ch) {
return indexOf(ch, 0);
}
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
private int indexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
final char hi = Character.highSurrogate(ch);
final char lo = Character.lowSurrogate(ch);
final int max = value.length - 1;
for (int i = fromIndex; i < max; i++) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}
indexOf通常用于判断某个字符是否存在于当前字符串并获取其下标,indexOf支持从0开始查找,也支持从指定的下标位置开始查找,如果不存在,返回-1。
5.5 substring
substring有多种参数的实现方法,这里只介绍substring(int beginIndex),毕竟它们实现的方法基本相同。其源码如下:
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);
}
相对于indexOf,substring没有处理beginIndex<0的情况,也没有处理beginIndex超出字符串长度的情况,都是直接抛出了下标越界的异常,因此我们在使用时,一定要注意避免这种情况。
substring的实现方法为:String(char value[], int offset, int count),该方法是源码如下:
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);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
在这个方法里,主要关注的是Arrays提供的方法:public static char[] copyOfRange(char[] original, int from, int to),通过这里我们可以知道,substring的实现是通过数组的拷贝实现的,而数组的拷贝底层是通过System.arraycopy这个non-java方法实现的。
5.6 split
我们常用的split(String regex)方法其底层是通过split(String regex, int limit)实现的。源码如下:
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
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<>();
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};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
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);
}
return Pattern.compile(regex).split(this, limit);
}
5.7 trim
去除字符串头尾的空格,中间部分的空格是不做处理的。源码如下:
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
其原理就是通过阿斯克码值得判断去截取字符串。
5.8 valueOf
获取当前对象的String,大致包括以下方法:
valueOf(Object obj)
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
添加了null判断,最终调用Object的toString方法。
valueOf(boolean b)
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
很简单,就是一个三元表达式,返回true或false的字符串。
除此之外,还包括一些基本数据类型的包装类的valueOf方法,其底层都是调用对应的toString方法实现的,由于涉及到其他类的源码知识,这里不做介绍。