String的基本属性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
....
}
- 是不可变类(final),也是线程安全的类,实现了三个接口
- 字符串的hash值默认是0
- 底层是通过char[]数组来存储字符串的
- 因为String实现了Serializable接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
String的构造方法
- 通过字符数组的复制
会用到Arrays.copyOf方法和Arrays.copyOfRange方法。这两个方法是将原有的字符数组中的内容逐一的复制到String中的字符数组中
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
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);
}
- 通过字符串来创建
实际上就是将字符串的数组和hash值复制了一遍
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
- 利用字节数组 和 编码集创建一个String
在Java中String实例中保存有一个char[]字符数组,char[]字符数组是以unicode码来存储的,String 和 char 为内存形式,byte是网络传输或存储的序列化形式。所以在很多传输和存储的过程中需要将byte[]数组和String进行相互转化
所以,String提供了一系列重载的构造方法来将一个字符数组转化成String,提到byte[]和String之间的相互转换就不得不关注编码问题。String(byte[] bytes, Charset charset)是指通过charset来解码指定的byte数组,将其解码成unicode的char[]数组,够造成新的String我们在使用byte[]构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作
所以需要明确的是char[]数组存储的是unicode值
在使用byte[]构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作。
使用使用StringBuffer和StringBuider构造一个String
这两个方法很少使用,当我们有了StringBuffer或者StringBuilfer对象之后可以直接使用他们的toString方法来得到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());
}
- 一个特殊的构造函数
这是默认的包权限,即只有java.lang包下的类才能使用这种构造函数!
/*
* Package private constructor which shares value array for speed.
* this constructor is always expected to be called with share==true.
* a separate constructor is needed because we already have a public
* String(char[]) constructor that makes a copy of the given char[].
*/
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
目前该函数只支持使用share 为true的情形,而且与前面几种构造函数不投的是,新的string共享老的String的char数组,这种做法的两面性是:(1)首先,性能好,这个很简单,一个是直接给数组赋值(相当于直接将String的value的指针指向char[]数组),一个是逐一拷贝。当然是直接赋值快了。其次,共享内部数组节约内存 (2)缺点也很明显的,那就是两个字符串共享一个char[]数组,如果一个string需要被回收,由于char[]被共享,就会导致内存泄漏问题(内存泄露是指不用的内存没有办法被释放)
从JDK7开始,subString不再使用这种share数组的方法,但是concat 和 replace 内部还是使用 这种构造方式,因为他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短!
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);//将value数组拷贝到一个字符数组
str.getChars(buf, len);//将str复制到buf中从len代表的坐标开始
return new String(buf, true);
}
String aLongString = "...a very long string...";
String aPart = data.substring(20, 40);
//真正有用的20-40,alongx中其他字符没有,但也由于被引用,无法被释放掉
return aPart;
- String的几个方法细节
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;
}
hashCode
hashCode的实现其实就是使用数学公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
s[i]是string的第i个字符,n是String的长度。那为什么这里用31,而不是其它数呢? 计算机的乘法涉及到移位计算。当一个数乘以2时,就直接拿该数左移一位即可!选择31原因是因为31是一个素数!
31可以 由i*31== (i<<5)-1来表示,即乘法可以表示为左移5位再减1,比较方便,是虚拟机的一个细节优化
---
通过拷贝value数组来提高安全性能!
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);
}
intern()方法
public native String intern();
该方法返回一个字符串对象的内部化引用。 众所周知:String类维护一个初始为空的字符串的对象池,当intern方法被调用时,如果对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,否则添加字符串到对象池并返回该字符串的引用。
---
String对“+”的重载
我们知道,Java是不支持重载运算符,String的“+”是java中唯一的一个重载运算符,那么java使如何实现这个加号的呢?我们先看一段代码:
public static void main(String[] args) {
String string="hollis";
String string2 = string + "chuang";
}
然后我们将这段代码反编译:
public static void main(String args[]){
String string = "hollis";
String string2 = (new StringBuilder(String.valueOf(string))).append("chuang").toString();
}
看了反编译之后的代码我们发现,其实String对“+”的支持其实就是使用了StringBuilder以及他的append、toString两个方法。
从这里可以看到每一次加法操作都会生成StringBuffer对象,最后再toString生成一个String,所以如果有大量拼接字符串,那么一定会造成大量性能浪费,所以一般都是直接StringBuffer对象执行大量的拼接操作
1、如果两个对象相等,那么他们一定有相同的哈希值(hash code)。
2、如果两个对象的哈希值相等,那么这两个对象有可能相等也有可能不相等。(需要再通过equals来判断)