Table of Contents
概述
String类成员变量
重要方法解析
equals方法
其他String的用法
intern()方法和字符串比较
hashcode方法
字符串拼接方法
其他方法
length方法
isEmpty方法
charAt方法
startWith方法
String 对象内存分配问题
正则表达式
本文基于jdk1.8
String这个类,在java的世界里无人不知无人不晓,并且其实现原理也一直在更新,知道的人多,真正掌握的不多,这篇博客会尽量深入的解析String类的方方面面。
从java的体系角度来讲,String类并不像Object类,Class类那样在一些关键技术以及特性,如继承,虚拟机类加载机制上发挥着重要作用。String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象。
但是String类却仍然是非常常用的一个类,我们来看一下他是怎么实现的。
在String类里面存储字符的数据结构定义如下:
/** The value is used for character storage. */
private final char value[];
首先这是一个方final关键字修饰的变量,这是String对象不可变的原因所在。
其次可以看到,是一个char类型的数组。什么意思?就是说java里面一个字符串里的字符事实上是以数组的形式在保存,其中的每一个字符都是char类型的。同时使用final修饰,意味着是不可变的。
那么char是个什么东西呢?了解java基础的朋友应该知道,char是java的基本数据类型之一。Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。关于java基本类型的已经有非常多文章了,可以参考这里。这里char就是这一种字符类型。
因此,明白了这个字符串的存储结构之后,它的很多方法就很好理解了。如计算length,无非就是返回数组长度,isEmpty方法无非就是判断一下数组长度是否为0.
除value之外,String类仅有的两个重要成员变量的另一个就是它的hash值,定义如下:
/** Cache the hash code for the string */
private int hash; // Default to 0
一个String对象的字符内容,和这个String的hash值,就是这个字符串的全部精华了。
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;
}
从代码里不难看出,重写的equals方法里,是根据字符串的类容逐个比较,若字符串内容一致则返回true.
那么如下代码的返回结果依次是什么呢?
String s1 = "1";
String s2 = "1";
String s3 = new String("1");
String s4 = new String("1");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
System.out.println(s3.equals(s4));
System.out.println(s3 == s4);
System.out.println(s1 == s3);
答案是:true,true,true,false.false
有一个结果很奇怪,第一个,为什么==的比较返回了true呢?难道是String对象的==比较和其他对象有区别?显然不是,关于两个==的判定是由java语言机制规定的,不可能不一致,否则就出大事了。因此只有一种可能,s1与s2在内存里就是指向的同一个内存上的对象。上面代码的内存图如下:
因为String太过常用,JAVA类库的设计者在实现时做了个小小的变化,即采用了享元模式,每当生成一个新内容的字符串时,他们都被添加到一个共享池(位于常量池)中,当第二次再次生成同样内容的字符串实例时,就共享此对象,而不是创建一个新对象,但是这样的做法仅仅适合于通过=符号进行的初始化.
另外必须引起注意的是:如上图所示,在使用等于符号定义字符串的时候,字符串的引用是在栈内存里,而字符串对象是在常量池中的。
上面的情形在笔者看来是有一定实际价值,值得掌握的,但下面这些内容则只需要了解即可。
考虑下面这张情形:
String s1 = "hello";
String s3 = "he" + "llo";
String s4 = "hel" + new String("lo");
String s5 = new String("hello");
String s7 = "h";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s3);//true
System.out.println(s1 == s4);//false
System.out.println(s1 == s9);//false
System.out.println(s4 == s5);//false
s1和s3打印结果为true,s1和s4为false,s1和s9为false. s4和s5为false。
s3中字面量的拼接其实就是“hello”,jvm在编译期间就已经对它进行优化,所以s1和s3是相等的。
String s4 = "hel" + new String("lo") 实质上是两个对象的相加,编译器不会进行优化,相加的结果存在堆中生成一个新的String对象,而s1存在字符串常量池中,当然不相等。
s9则也会创建出一个新的对象出来。因此s1和s9结果为false。原因在于虚拟机在处理字符串相加的时候,都是静态字符串(如"
a"+"b")的结果会添加到字符串池,如果其中含有变量(如s9中的s7以及s8)则不会进入字符串常量池中。
s4是创建在堆当中的一个新的对象,s5也是创建在堆中的一个新的对象。他们当然不是同一个对象,结果为false。
String类中的intern方法的作用是返回字符串对象的规范化表示形式。
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
其返回结果遵顼:是一个字符串,内容与此字符串相同,但一定取自具有唯一字符串的池。
因此虽然在返回值中调用intern方法并没有什么效果,但是实际上后台这个方法会做一系列的动作和操作。在调用“ab”.intern()方法的时候会返回”ab”,但是这个方法会首先检查字符串常量池中是否有”ab”这个字符串,如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串常量池中,然会返回这个字符串的引用。(重要)
考虑以下代码:
String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str4 = str1 + str2;
String str5 = new String("ab");
System.out.println(str5.equals(str3));
System.out.println(str5 == str3);
System.out.println(str5.intern() == str3);
System.out.println(str5.intern() == str4);
其打印结果为:true ,false, true, false. 原因如下
实现的源代码如下:
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
*
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
*
* using {@code int} arithmetic, where {@code s[i]} is the
* ith character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
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是一个int类型的值,定义如下:
private int hash;
它是一个 private 修饰的变量,作用是来存放 String 对象的 hashCode。
String的hashCode计算方法,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。
31 的二进制为: 11111,占用 5 个二进制位,在进行乘法运算时,可以转换为 (i << 5) - 1
。31*i等价于(i<<5)-i,这两个运算是一样的。
主要是因为31是一个奇质数,如果是偶数的话,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。
31有一个很好的特性,就是用移位和减法来代替乘法
一个字符串的hashcode计算过程如下:
String str = "ray";
value = {'r', 'a', 'y'};
hash = 0;
value.length = 3;
val = value;
val[0] = "r";
val[1] = "a";
val[2] = "y";
h = 31 * 0 + r = r;
h = 31 * (31 * 0 + r) + a = 31 * r + a;
h = 31 * (31 * (31 * 0 + r) + a) + y = 31 * 31 * r + 31 * a + y;
哈希计算公式可以计为s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
另外:对于是否应该使用31作为权重,一直都有不同的观点。但目前的jdk源代码仍然使用的是31.
字符串哈希可以做很多事情,通常是类似于字符串判等,判回文之类的。但是仅仅依赖于哈希值来判断其实是不严谨的,除非能够保证不会有哈希冲突,通常这一点很难做到。
就拿jdk中String类的哈希方法来举例,字符串"gdejicbegh"与字符串"hgebcijedg"具有相同的hashCode()返回值-801038016,并且它们具有reverse的关系。这个例子说明了用jdk中默认的hashCode方法判断字符串相等或者字符串回文,都存在反例。
关于乘法溢出:
java 中int基本数据类型的最大范围(2147483647),于是作了默认的类型提升(type promotion),中间结果做为long类型存放,返回结果时目标数据类型int不能够容纳下结果,于是根据Java的基础类型的变窄转换(Narrowing primitive conversion)规则,把结果宽于int类型宽度的部分全部丢弃,也就是只取结果的低32位 。更多
字符串的拼接也是我们经常使用的方法。这里着重比较使用加号拼接和使用String类的concat方法以及使用StringBuilder拼接的区别。String对象是不可变的,你可以给一个String对象加任意多的别名。
使用加号:
使用加号是我们只能在class文件里通过反编译才能看到编译器是如何处理加号的。在jdk1.5之后,编译器事实上已经是在用StringBuilder实现,编译器自动引入了java.lang.StringBuilder类,虽然在源代码中没有使用它,会自动将+号替换为StringBuilder的append方法,可以通过命令javap -v class文件来查看.
既然已经自动转换成StringBuilder了,那么是否意味着任何情况下都直接使用加号吗?答案不是的,编译器虽然做到了把加号解析为StringBuilder,但是没有那么智能,因此需要我们自己判断什么时候使用,考虑如下代码:
String string = new String();
for (int i = 0; i < 50000; i++) {
string = string + i;
}
注意,在每次把加号解析为StringBuilder的时候编译器都是new了一个StringBuilder出来,这意味着在上面这种情况下,会不停的new新的StringBuilder出来,因此效率就会大大降低。但是,值得一提的是,这和拼接的内容有关系,若直接拼接两个字符,如:
String string = "d" + "r";
则编译器不会每次都去new一个StringBuilder而是直接拼接。
因此在这种情况下,即不断的循环的情况下,我们就应该在外面new个StringBuilder对象,然后在循环里面append最好,若是在一行里,则直接使用+号拼接即可。
StringBuilder的append方法底层的实现调用了System类的一个方法arrayCopy,该方法为一个native方法。
另外,和StringBuilder有关的还有另一个类,叫做StringBuffer,这两个类最大的区别在于StringBuffer的append方法是使用了synchronized修饰的,这保证了它在并发情况下的正确性。
而String类自己提供的concat方法内部的实现也是调用了System类的另一个copyOf的方法,但是这个类最后的返回时new了一个String对象,这意味着在循环很多的情况下,依然会存在new大量对象的情况。
返回一个字符串的长度
public int length() {
return value.length;
}
实现原理非常简单,因为保存字符内容的是一个Char类型的数组,因此直接返回这个数组的length即可。
判断一个字符串是否为空字符串
public boolean isEmpty() {
return value.length == 0;
}
实现原理同上,判断数组的长度是否为0即可。
返回指定下标处的字符
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
先做了必要的范围验证。实现原理也是同上,直接根据数组的下标取对应的字符即可。
判断一个字符串是否以某一串字符prefix打头,核心实现逻辑如下
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
ta[]是当前String对象的一个拷贝,
pa[]是传入的prefix字符串的一个拷贝
pc是prefix字符串的长度,比较的逻辑在while循环里面:
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
根据prefix长度,逐一比较。
值得一提的是,endsWith方法的实现直接使用了startsWith的实现逻辑。只是在调用的时候同时指定了length:
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
String是一个特殊的包装类数据,可以用:
String str = new String("abc");
String str = "abc";
两种形式来创建,第一种是用new来创建对象的,它会在堆中按照new对象的流程创建一个新的对象出来,每调用一次就会创建一个新的对象;而第二种是先在栈中创建一个对String类的对象引用str ,然后查找常量池中有没有存放"abc",如果没有,则将"abc"存放常量池,并令str 指向"abc",如果已经有"abc",则直接令str指向"abc"。
在java当中,字符串的操作主要集中在String,StringBuffer和StringTokenizer类当中。
String当中自带了一个非常有用的正则表达式工具-split方法,其功能是将字符串从正则表达式匹配的地方切开。
String类还自带的一个正则表达式工具是'替换',你可以只替换正则表达式第一个匹配的字符串,或是替换所有匹配的地方。
字符
B | 指定字符B |
\xhh | 十六进制为oxhh的字符 |
\uhhhh | 十六进制表示为oxhhhh的Unicode字符 |
\t | 制表符Tab |
\n | 换行符 |
\r | 回车 |
\f | 换页 |
\e | 转义 |
字符类
任意字符 | |
[abc] | 包含a,b和c的任何字符,和a|b|c作用相同 |
[^abc] | 除了a,b和c之外的任何字符 |
[a-zA-Z] | 从a到z或A到Z的任何字符 |
[abc[hij]] | 任意a,b,c,h,i和j字符 |
\s | 空白符 |
\S | 非空白符 |
\d | 数字[0-9] |
\D | 非数字[^0-9] |
\w | 词字符[a-zA-Z0-9] |
\W | 非词字符[^\w] |