Java 7 源码学习系列(一)——String
String表示字符串,Java中所有字符串的字面值都是String类的实例,例如“ABC”。字符串是常量,在定义之后不能被改变,字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享它们。例如:
String str = "abc";
相当于
char data[] = {'a', 'b', 'c'};
String str = new String(data);
这里还有一些其他使用字符串的例子:
System.out.println("abc");
String cde = "cde";
System.out.println("abc" + cde);
String c = "abc".substring(2,3);
String d = cde.substring(1, 2);
String类提供了检查字符序列中单个字符的方法,比如有比较字符串,搜索字符串,提取子字符串,创建一个字符串的副本、字符串的大小写转换等。实例映射是基于Character
类中指定的Unicode标准的。 Java语言提供了对字符串连接运算符的特别支持(+),该符号也可用于将其他类型转换成字符串。字符串的连接实际上是通过StringBuffer
或者StringBuilder
的append()
方法来实现的,字符串的转换通过toString
方法实现,该方法由 Object 类定义,并可被 Java 中的所有类继承。 除非另有说明,传递一个空参数在这类构造函数或方法会导致NullPointerException
异常被抛出。String表示一个字符串通过UTF-16(unicode)格式,补充字符通过代理对(参见Character类的 Unicode Character Representations 获取更多的信息)表示。索引值参考字符编码单元,所以补充字符在String中占两个位置。
一、定义
public final class String implements java.io.Serializable, Comparable<String>, CharSequence{}
从该类的声明中我们可以看出String是final类型的,表示该类不能被继承,同时该类实现了三个接口:java.io.Serializable
、 Comparable<String>
、 CharSequence
二、属性
private final char value[];
这是一个字符数组,并且是final类型,他用于存储字符串内容,从fianl这个关键字中我们可以看出,String的内容一旦被初始化了是不能被更改的。 虽然有这样的例子: String s = “a”; s = “b” 但是,这并不是对s的修改,而是重新指向了新的字符串, 从这里我们也能知道,String其实就是用char[]实现的。
private int hash;
缓存字符串的hash Code,默认值为 0
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
因为
String
实现了Serializable
接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID
来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID
与本地相应实体(类)的serialVersionUID
进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException
)。
三、构造方法
String类作为一个java.lang包中比较常用的类,自然有很多重载的构造方法.在这里介绍几种典型的构造方法:
1.使用字符数组、字符串构造一个String
我们知道,其实String就是使用字符数组(char[])实现的。所以我们可以使用一个字符数组来创建一个String,那么这里值得注意的是,当我们使用字符数组创建String的时候,会用到Arrays.copyOf
方法和Arrays.copyOfRange
方法。这两个方法是将原有的字符数组中的内容逐一的复制到String中的字符数组中。同样,我们也可以用一个String类型的对象来初始化一个String。这里将直接将源String
中的value
和hash
两个属性直接赋值给目标String
。因为String一旦定义之后是不可以改变的,所以也就不用担心改变源String
的值会影响到目标String
的值。
当然,在使用字符数组来创建一个新的String对象的时候,不仅可以使用整个字符数组,也可以使用字符数组的一部分,只要多传入两个参数
int offset
和int count
就可以了。
2.使用字节数组构造一个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。
这里的bytes字节流是使用charset进行编码的,想要将他转换成unicode的char[]数组,而又保证不出现乱码,那就要指定其解码方式
同样使用字节数组来构造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
作为String的两个“兄弟”,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除了提供了很多公有的供程序员使用的构造方法以外,还提供了一个保护类型的构造方法(Java 7),我们看一下他是怎么样的:
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,是因为一旦该方法设置为公有,在外面可以访问的话,那就破坏了字符串的不可变性。例如如下YY情形:
char[] arr = new char[] {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; String s = new String(0, arr.length, arr); // "hello world" arr[0] = 'a'; // replace the first character with 'a' System.out.println(s); // aello world
如果构造方法没有对arr进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对arr的修改就相当于修改了字符串。
所以,从安全性角度考虑,他也是安全的。对于调用他的方法来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。
在Java 7 之有很多String里面的方法都使用这种“性能好的、节约内存的、安全”的构造函数。比如:substring
、replace
、concat
、valueOf
等方法(实际上他们使用的是public String(char[], int, int)方法,原理和本方法相同,已经被本方法取代)。
但是在Java 7中,substring已经不再使用这种“优秀”的方法了,为什么呢? 虽然这种方法有很多优点,但是他有一个致命的缺点,对于sun公司的程序员来说是一个零容忍的bug,那就是他很有可能造成内存泄露。 看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。下面是示例代码。
String aLongString = "...a very long string..."; String aPart = data.substring(20, 40); return aPart;
在这里aLongString只是临时的,真正有用的是aPart,其长度只有20个字符,但是它的内部数组却是从aLongString那里共享的,因此虽然aLongString本身可以被回收,但它的内部数组却不能(如下图)。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。
新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的substring比原来的更健壮。
额、、、扯了好远,虽然substring方法已经为了其鲁棒性放弃使用这种share数组的方法,但是这种share数组的方法还是有一些其他方法在使用的,这是为什么呢?首先呢,这种方式构造对应有很多好处,其次呢,其他的方法不会将数组长度变短,也就不会有前面说的那种内存泄露的情况(内存泄露是指不用的内存没有办法被释放,比如说concat方法和replace方法,他们不会导致元数组中有大量空间不被使用,因为他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短!)。
四、其他方法
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)
String string = "h,o,l,l,i,s,c,h,u,a,n,g"; String[] splitAll = string.split(","); String[] splitFive = string.split(",",5); splitAll = [h, o, l, l, i, s, c, h, u, a, n, g] splitFive = [h, o, l, l, i,s,c,h,u,a,n,g]
getBytes
在创建String的时候,可以使用byte[]数组,将一个字节数组转换成字符串,同样,我们可以将一个字符串转换成字节数组,那么String提供了很多重载的getBytes方法。但是,值得注意的是,在使用这些方法的时候一定要注意编码问题。比如:
String s = "你好,世界!";
byte[] bytes = s.getBytes();
这段代码在不同的平台上运行得到结果是不一样的。由于我们没有指定编码方式,所以在该方法对字符串进行编码的时候就会使用系统的默认编码方式,比如在中文操作系统中可能会使用GBK或者GB2312进行编码,在英文操作系统中有可能使用iso-8859-1进行编码。这样写出来的代码就和机器环境有很强的关联性了,所以,为了避免不必要的麻烦,我们要指定编码方式。如使用以下方式:
String s = "你好,世界!";
byte[] bytes = s.getBytes("utf-8");
比较方法
boolean equals(Object anObject); boolean contentEquals(StringBuffer sb); boolean contentEquals(CharSequence cs); boolean equalsIgnoreCase(String anotherString); int compareTo(String anotherString); int compareToIgnoreCase(String str); boolean regionMatches(int toffset, String other, int ooffset,int len) //局部匹配 boolean regionMatches(boolean ignoreCase, int toffset,String other, int ooffset, int len) //局部匹配
字符串有一系列方法用于比较两个字符串的关系。 前四个返回boolean的方法很容易理解,前三个比较就是比较String和要比较的目标对象的字符数组的内容,一样就返回true,不一样就返回false,核心代码如下:
int n = value.length; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; }
v1 v2分别代表String的字符数组和目标对象的字符数组。 第四个和前三个唯一的区别就是他会将两个字符数组的内容都使用toUpperCase方法转换成大写再进行比较,以此来忽略大小写进行比较。相同则返回true,不想同则返回false
在这里,看到这几个比较的方法代码,有很多编程的技巧我们应该学习。我们看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,一样再逐一比较值。 虽然代码写的内容比较多,但是可以很大程度上提高比较的效率。值得学习~~!!!
contentEquals有两个重载,StringBuffer需要考虑线程安全问题,再加锁之后调用contentEquals((CharSequence) sb)
方法。contentEquals((CharSequence) sb)
则分两种情况,一种是cs instanceof AbstractStringBuilder
,另外一种是参数是String类型。具体比较方式几乎和equals方法类似,先做“宏观”比较,在做“微观”比较。
下面这个是equalsIgnoreCase代码的实现:
public boolean equalsIgnoreCase(String anotherString) { return (this == anotherString) ? true : (anotherString != null) && (anotherString.value.length == value.length) && regionMatches(true, 0, anotherString, 0, value.length); }
看到这段代码,眼前为之一亮。使用一个三目运算符和&&操作代替了多个if语句。
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是一个素数!
所谓素数:
质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。
素数在使用的时候有一个作用就是如果我用一个数字来乘以这个素数,那么最终的出来的结果只能被素数本身和被乘数还有1来整除!如:我们选择素数3来做系数,那么3*n只能被3和n或者1来整除,我们可以很容易的通过3n来计算出这个n来。这应该也是一个原因! (本段表述有问题,感谢 @沉沦 的提醒)
在存储数据计算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值就相同。
substring
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); }
前面我们介绍过,java 7 中的substring方法使用String(value, beginIndex, subLen)方法创建一个新的String并返回,这个方法会将原来的char[]中的值逐一复制到新的String中,两个数组并不是共享的,虽然这样做损失一些性能,但是有效地避免了内存泄露。
replaceFirst、replaceAll、replace区别
String replaceFirst(String regex, String replacement)
String replaceAll(String regex, String replacement)
String replace(CharSequence target, CharSequence replacement)
1)replace的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换 2)replaceAll和replaceFirst的参数是regex,即基于规则表达式的替换,比如,可以通过replaceAll(“\d”, “*”)把一个字符串所有的数字字符都换成星号; 相同点是都是全部替换,即把源字符串中的某一字符或字符串全部换成指定的字符或字符串, 如果只想替换第一次出现的,可以使用 replaceFirst(),这个方法也是基于规则表达式的替换,但与replaceAll()不同的是,只替换第一次出现的字符串; 另外,如果replaceAll()和replaceFirst()所用的参数据不是基于规则表达式的,则与replace()替换字符串的效果是一样的,即这两者也支持字符串的操作;
copyValueOf 和 valueOf
String的底层是由char[]实现的:通过一个char[]类型的value属性!早期的String构造器的实现呢,不会拷贝数组的,直接将参数的char[]数组作为String的value属性。然后test[0] = 'A';
将导致字符串的变化。为了避免这个问题,提供了copyValueOf
方法,每次都拷贝成新的字符数组来构造新的String对象。但是现在的String对象,在构造器中就通过拷贝新数组实现了,所以这两个方面在本质上已经没区别了。
valueOf()有很多种形式的重载,包括:
public static String valueOf(boolean b) { return b ? "true" : "false"; } public static String valueOf(char c) { char data[] = {c}; return new String(data, true); } public static String valueOf(int i) { return Integer.toString(i); } public static String valueOf(long l) { return Long.toString(l); } public static String valueOf(float f) { return Float.toString(f); } public static String valueOf(double d) { return Double.toString(d); }
可以看到这些方法可以将六种基本数据类型的变量转换成String类型。
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两个方法。
String.valueOf和Integer.toString的区别
接下来我们看以下这段代码,我们有三种方式将一个int类型的变量变成呢过String类型,那么他们有什么区别?
1.int i = 5;
2.String i1 = "" + i;
3.String i2 = String.valueOf(i);
4.String i3 = Integer.toString(i);
1、第三行和第四行没有任何区别,因为
String.valueOf(i)
也是调用Integer.toString(i)
来实现的。 2、第二行代码其实是String i1 = (new StringBuilder()).append(i).toString();
,首先创建一个StringBuilder对象,然后再调用append方法,再调用toString方法。