前言
Java使用String类来代表字符串,实际上String对象的值是一个常量,一旦创建后不能改变,所以它是线程安全地,可以多个线程共享。
JDK8 源码
类定义
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
String类被final修饰,说明它不能被继承;同时实现三个接口:Serializable、Comparable和CharSequence。其中,Serializable接口表明其可以序列化;Comparable接口说明可以用compareTo方法;CharSequence接口表明其具有length()、charAt()等方法。
属性
private final char value[]; //这是final修饰的字符数组,内容一旦被初始化则不能改变,String就是用char[]实现的
private int hash; //字符串的哈希值,默认为0
/**
* String序列化时通过序列号验证版本是否一致;反序列化时,JVM会把传来的字节流中的序列号与本地相应实体的序列号
* 进行比较,如果相同则认为一致,可进行反序列化,否则会出现版本不一致的异常:InvalidCastException
*/
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
内部类
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
疑惑:String已经有了compareTo方法,那为什么还需要这样一个内部静态类呢?
解答:此类的比较和compareTo还是有差别的,它是忽略大小写的,而且这是一个单例,可以简单的用来比较String,因为
String提供了一个变量:CASE_INSENSITIVE_ORDER来持有这个内部类,这样当要比较两个String时可以通过这
个变量来调用;其次可以看到String提供的compareToIgnoreCase方法其实就是调用这个内部类里的方法实现的。
这就是代码复用的一个例子。
构造方法
String类作为一个lang包中比较常用的类,有很多重载的构造方法,这只介绍几种典型的构造方法。
1.使用字符数组、字符串构造一个String
String就是用字符数组实现的,所以可用字符数组来创建一个String,那么需要注意的是,当使用字符数组创建String时,会用到Arrays.copyOf和Arrays.copyOfRange方法,这两个方法是将原有的字符数组中的内容逐一复制到String的数组中。同样,我们也可以用一个String类型的对象来初始化一个String,这里直接将源String中的value和hash两个属性直接赋值给目标String。
2.使用字节数组构造一个String
String实例会保存一个char[],char[]字符数组是以unicode码来存储的,String和char为内存形式,byte是网络传输或存储的序列化形式。所以,在很多传输和存储过程中需要将byte[]和String进行相互转化。String提供了一系列重载的构造方法来将一个byte[]数组转化为String,同时要注意编码问题。String(byte[] bytes, Charset charset)是指通过charset来解码指定的byte[],将其解码成unicode的char[],构建新的String。
同样使用字节数组来构造String也有很多种形式,按照是否指定解码方式分的话可以分为两种:
String(byte bytes[]) String(byte bytes[], int offset, int length)
String(byte bytes[], Charset charset)
String(byte bytes[], String charsetName)
String(byte bytes[], int offset, int length, Charset charset)
String(byte bytes[], int offset, int length, String charsetName)
如果我们在使用byte[]构造String的时候,使用的是下面这四种构造方法(带有charsetName或者charset参数)的一种的话,那么就会使用StringCoding.decode方法进行解码,使用的解码的字符集就是我们指定的charsetName或者charset。我们在使用byte[]构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作。主要体现代码如下:
static char[] decode(byte[] ba, int off, int len) {
String csn = Charset.defaultCharset().name();
try {
// use charset name decode() variant which provides caching.
return decode(csn, ba, off, len);
} catch (UnsupportedEncodingException x) {
warnUnsupportedCharset(csn);
}
try {
return decode("ISO-8859-1", ba, off, len);
} catch (UnsupportedEncodingException x) {
// If this code is hit during VM initialization, MessageUtils is
// the only way we will be able to get any kind of error message.
MessageUtils.err("ISO-8859-1 charset not available: "
+ x.toString());
// If we can not find ISO-8859-1 (a required encoding) then things
// are seriously wrong with the installation.
System.exit(1);
return null;
}
}
3.使用StringBuffer和StringBuider构造一个String
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
当然,这两个构造方法是很少用到的,至少我从来没有使用过,因为当我们有了StringBuffer或者StringBuilfer对象之后可以直接使用他们的toString方法来得到String。关于效率问题,Java的官方文档有提到说使用StringBuilder的toString方法会更快一些,原因是StringBuffer的toString方法是synchronized的,在牺牲了效率的情况下保证了线程安全。
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
4.一个特殊的保护类型的构造方法
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
从代码中我们可以看出,该方法和 String(char[] value)有两点区别,第一个,该方法多了一个参数: boolean share,其实这个参数在方法体中根本没被使用,也给了注释,目前不支持使用false,只使用true。那么可以断定,加入这个share的只是为了区分于String(char[] value)方法,不加这个参数就没办法定义这个函数,只有参数不能才能进行重载。那么,第二个区别就是具体的方法实现不同。我们前面提到过,String(char[] value)方法在创建String的时候会用到 会用到Arrays的copyOf方法将value中的内容逐一复制到String当中,而这个String(char[] value, boolean share)方法则是直接将value的引用赋值给String的value。那么也就是说,这个方法构造出来的String和参数传过来的char[] value共享同一个数组。 那么,为什么Java会提供这样一个方法呢?
原因:首先,性能好,这个很简单,一个是直接给数组赋值(相当于直接将String的value的指针指向char[]数组),一个是逐一拷贝,当然是直接赋值快了;其次,共享内部数组节约内存。
但是,该方法之所以设置为protected,是因为一旦该方法设置为公有,在外面可以访问的话,那就破坏了字符串的不可变性。同时,有一个致命的bug–内存泄漏:
String aLongString = "...a very long string...";
String aPart = data.substring(20, 40);
return aPart;
在这里aLongString只是临时的,真正有用的是aPart,其长度只有20个字符,但是它的内部数组却是从aLongString那里共享的,因此虽然aLongString本身可以被回收,但它的内部数组却不能(都指向同一个内部数组)。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。
其他方法
length() //返回字符串长度
isEmpty() //返回字符串是否为空
charAt(int index) //返回字符串中第(index+1)个字符
char[] toCharArray() //转化成字符数组
trim() //去掉两端空格
toUpperCase() //转化为大写
toLowerCase() //转化为小写
String concat(String str) //拼接字符串
String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar字符
//以上两个方法都使用了String(char[] value, boolean share);
boolean matches(String regex) //判断字符串是否匹配给定的regex正则表达式
boolean contains(CharSequence s) //判断字符串是否包含字符序列s
String[] split(String regex, int limit) //按照字符regex将字符串分成limit份。
String[] split(String regex)
getBytes
在创建String的时候,可以使用byte[]数组,将一个字节数组转换成字符串,同样,我们可以将一个字符串转换成字节数组,那么String提供了很多重载的getBytes方法。但是,值得注意的是,在使用这些方法的时候一定要注意编码问题。比如:
String s = "你好,世界!";
byte[] bytes = s.getBytes();
这段代码在不同的平台上运行得到结果是不一样的。由于我们没有指定编码方式,所以在该方法对字符串进行编码的时候就会使用系统的默认编码方式,比如在中文操作系统中可能会使用GBK或者GB2312进行编码,在英文操作系统中有可能使用iso-8859-1进行编码。这样写出来的代码就和机器环境有很强的关联性了,所以,为了避免不必要的麻烦,我们要指定编码方式。如使用以下方式:
String s = "你好,世界!";
byte[] bytes = s.getBytes("utf-8");
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;
}
分析:该方法首先判断this == anObject ?,也就是说判断要比较的对象和当前对象是不是同一个对象,如果是直接返回true,如不是再继续比较,然后在判断anObject是不是String类型的,如果不是,直接返回false,如果是再继续比较,到了能终于比较字符数组的时候,他还是先比较了两个数组的长度,不一样直接返回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;
}
hashCode的实现其实就是使用数学公式:s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]。s[i]是string的第i个字符,n是String的长度。那为什么这里用31,而不是其它数呢? 计算机的乘法涉及到移位计算。当一个数乘以2时,就直接拿该数左移一位即可!选择31原因是因为31是一个素数!
在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率!所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!
在java乘法中如果数字相乘过大会导致溢出的问题,从而导致数据的丢失.而31则是素数(质数)而且不是很长的数字,最终它被选择为相乘的系数的原因不过如此!
在Java中,整型数是32位的,也就是说最多有2^32= 4294967296个整数,将任意一个字符串,经过hashCode计算之后,得到的整数应该在这4294967296数之中。那么,最多有 4294967297个不同的字符串作hashCode之后,肯定有两个结果是一样的, hashCode可以保证相同的字符串的hash值肯定相同,但是,hash值相同并不一定是value值就相同。
intern()
public native String intern();
该方法返回一个字符串对象的内部化引用。 众所周知:String类维护一个初始为空的字符串的对象池,当intern方法被调用时,如果对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,否则添加字符串到对象池并返回该字符串的引用。
String对“+”的重载
代码
public static void main(String[] args) {
int i = 5;
String string="Hello";
String string2 = string + "World";
}
反编译
public static void main(String[] args) {
int i = 5;
String string = "Hello";
String string2 = new StringBuilder().append((String)string).append((String)"World").toString();
}
看了反编译之后的代码我们发现,其实String对“+”的支持其实就是使用了StringBuilder以及他的append、toString两个方法。