Tip:笔者立刻毕业了,准备开始 Java 的进阶学习计划。因而打算先从 String 类的源码分析入手,做为后面学习的案例。这篇文章寄托着从此进阶系列产出的愿望,但愿能坚持下去,不忘初心,让本身保持那份对技术的热爱。java
由于学习分析源码,因此借鉴了 HollisChuang 成神之路的大部份内容,并在此基础上对源码进行了学习,在此感谢。程序员
关于 String 字符串,对于Java开发者而言,这无疑是一个很是熟悉的类。也正是由于常用,其内部代码的设计才值得被深究。所谓知其然,更得知其因此然。正则表达式
举个例子,假如想要写个类去继承 String,这时 IDE 提示 String 为final类型不容许被继承。数据库
此时最早想到的确定是 java 中类被 final 修饰的效果,其实由这一点也能够引出更多思考: 好比说 String 类被设计成 final 类型是出于哪些考虑?数组
在Java中,被 final 类型修饰的类不容许被其余类继承,被 final 修饰的变量赋值后不容许被修改缓存
查看 String 类在 JDK 7 源码中的定义:安全
public final class String implements java.io.Serializable, Comparable<String>, CharSequence{
...
}
复制代码
能够看出 String 是 final 类型的,表示该类不能被其余类继承,同时该类实现了三个接口:java.io.Serializable
Comparable<String>
CharSequence
bash
对于 Sting 类,官方有以下注释说明:网络
/*
Strings are constant;
their values can not be changed after they are created.
Stringbuffers support mutable strings.
Because String objects are immutable they can be shared. Forexample:
*/
复制代码
String 字符串是常量,其值在实例建立后就不能被修改,但字符串缓冲区支持可变的字符串,由于缓冲区里面的不可变字符串对象们能够被共享。(其实就是使对象的引用发生了改变)app
/**
The value isused for character storage.
*/
private final char value[];
复制代码
这是一个字符数组,而且是 final 类型,用于存储字符串内容。从 fianl 关键字能够看出,String 的内容一旦被初始化后,其不能被修改的。
看到这里也许会有人疑惑,String 初始化之后好像能够被修改啊。好比找一个常见的例子:
String str = “hello”; str = “hi”
其实这里的赋值并非对 str 内容的修改,而是将str指向了新的字符串。另外能够明确的一点:String 实际上是基于字符数组 char[] 实现的。
下面再来看 String 其余属性: 好比缓存字符串的 hash Code,其默认值为 0:
/**
Cache the hashcode for the string
*/
private int hash; //Default to 0
复制代码
关于序列化 serialVersionUID:
/**
use serialVersionUID from JDK 1.0.2 for interoperability
*/
private static final long serialVersionUID = -6849794470754667710L;
/**
Class String is special cased with in the Serialization Stream Protocol.
*/
privates tatic final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]
复制代码
由于 String 实现了 Serializable 接口,因此支持序列化和反序列化支持。Java 的序列化机制是经过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体(类)的 serialVersionUID 进行比较,若是相同就认为是一致的,能够进行反序列化,不然就会出现序列化版本不一致的异常 (InvalidCastException)。
空的构造器
public String(){
this.value = "".value;
}
复制代码
该构造方法会建立空的字符序列,注意这个构造方法的使用,由于创造没必要要的字符串对象是不可变的。所以不建议采起下面的建立 String 对象:
String str = new String()
str = "sample";
复制代码
这样的结果显而易见,会产生了没必要要的对象。
使用字符串类型的对象来初始化
public String(String original){
this.value = original.value;
this.hash = original.hash;
}
复制代码
这里将直接将源 String 中的 value 和 hash 两个属性直接赋值给目标 String。由于 String 一旦定义以后是不能够改变的,因此也就不用担忧改变源 String 的值会影响到目标 String 的值。
使用字符数组来构造
public String(char value[]){
this.value = Arrays.copyOf(value, value.length);
}
复制代码
public String(char value[], int offset, int count){
if(offset<0){
throw new StringIndexOutOfBoundsException(offset);
}
if(count<=0){
if(count<0){
throw new String IndexOutOfBoundsException(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);
}
复制代码
这里值得注意的是:当咱们使用字符数组建立 String 的时候,会用到 Arrays.copyOf 方法或 Arrays.copyOfRange 方法。这两个方法是将原有的字符数组中的内容逐一的复制到 String 中的字符数组中。会建立一个新的字符串对象,随后修改的字符数组不影响新建立的字符串。
使用字节数组来构建 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 也有不少种形式,按照是否指定解码方式分的话能够分为两种:
public String(byte bytes[]){
this(bytes, 0, bytes.length);
}
复制代码
public String(byte bytes[], int offset, int length){
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
复制代码
若是咱们在使用 byte[] 构造 String 的时候,使用的是下面这四种构造方法(带有 charsetName 或者 charset 参数)的一种的话,那么就会使用 StringCoding.decode 方法进行解码,使用的解码的字符集就是咱们指定的 charsetName 或者 charset。
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 的时候,若是没有指明解码使用的字符集的话,那么 StringCoding 的 decode 方法首先调用系统的默认编码格式,若是没有指定编码格式则默认使用 ISO-8859-1 编码格式进行编码操做。主要体现代码以下:
static char[] decode(byte[] ba, int off, int len){
String csn = Charset.defaultCharset().name();
try{ //use char set name decode() variant which provide scaching.
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 initiali zation, MessageUtils is the only way we will be able to get any kind of error message.
MessageUtils.err("ISO-8859-1 char set not available: " + x.toString());
// If we can not find ISO-8859-1 (are quired encoding) then things are seriously wrong with the installation.
System.exit(1);
return null;
}
}
复制代码
使用 StringBuffer 和 StringBuilder 构造一个 String 做为 String 的两个“兄弟”,StringBuffer 和 StringBuilder 也能够被当作构造 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 的,在牺牲了效率的状况下保证了线程安全。
StringBuilder 的 toString() 方法:
@Override
public String toString(){
//Create a copy, don't share the array return new String(value,0,count); } 复制代码
StringBuffer 的 toString() 方法:
@Override
public synchronized String toString(){
if (toStringCache == null){
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
复制代码
一个特殊的保护类型的构造方法 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,是由于一旦该方法设置为公有,在外面能够访问的话,若是构造方法没有对 arr 进行拷贝,那么其余人就能够在字符串外部修改该数组,因为它们引用的是同一个数组,所以对 arr 的修改就至关于修改了字符串,那就破坏了字符串的不可变性。
**安全的:**对于调用他的方法来讲,因为不管是原字符串仍是新字符串,其 value 数组自己都是 String 对象的私有属性,从外部是没法访问的,所以对两个字符串来讲都很安全。
在 Java 7 以前有不少 String 里面的方法都使用上面说的那种“性能好的、节约内存的、安全”的构造函数。 好比:substring
replace
concat
valueOf
等方法
实际上它们使用的是 public String(char[], ture) 方法来实现。
可是在 Java 7 中,substring 已经再也不使用这种“优秀”的方法了
public String substring(int beginIndex, int endIndex){
if(beginIndex < 0){
throw new StringIndexOutOfBoundsException(beginIndex);
}
if(endIndex > value.length){
throw new StringIndexOutOfBoundsException(endIndex);
}
intsubLen = endIndex-beginIndex;
if(subLen < 0){
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this : newString(value, beginIndex, subLen);
}
复制代码
为何呢? 虽然这种方法有不少优势,可是他有一个致命的缺点,对于 sun 公司的程序员来讲是一个零容忍的 bug,那就是他颇有可能形成内存泄露。
看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,而后对其进行解析并提取其中的一小段内容,这种状况常常发生在网页抓取或进行日志分析的时候。
下面是示例代码:
String aLongString = "...averylongstring...";
String aPart = data.substring(20, 40);
return aPart;
复制代码
在这里 aLongString 只是临时的,真正有用的是 aPart,其长度只有 20 个字符,可是它的内部数组倒是从 aLongString 那里共享的,所以虽然 aLongString 自己能够被回收,但它的内部数组却不能释放。这就致使了内存泄漏。若是一个程序中这种状况常常发生有可能会致使严重的后果,如内存溢出,或性能降低。
新的实现虽然损失了性能,并且浪费了一些存储空间,但却保证了字符串的内部数组能够和字符串对象一块儿被回收,从而防止发生内存泄漏,所以新的 substring 比原来的更健壮。
length() 返回字符串长度
public int length(){
return value.length;
}
复制代码
isEmpty() 返回字符串是否为空
public boolean isEmpty(){
return value.length == 0;
}
复制代码
charAt(int index) 返回字符串中第(index+1)个字符(数组索引)
public char charAt(int index){
if((index < 0) || (index >= value.length)){
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
复制代码
char[] toCharArray()
转化成字符数组 trim()
去掉两端空格 toUpperCase()
转化为大写 toLowerCase()
转化为小写
须要注意 String concat(String str)
拼接字符串 String replace(char oldChar, char newChar)
将字符串中的 oldChar 字符换成 newChar 字符
以上两个方法都使用了
String(char[] value, boolean share)
concat 方法和 replace 方法,他们不会致使元数组中有大量空间不被使用,由于他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短,因此使用了共享的 char[] 字符数组来优化。
boolean matches(String regex)
判断字符串是否匹配给定的regex正则表达式 boolean contains(CharSequence s)
判断字符串是否包含字符序列 s String[] split(String regex, int limit)
按照字符 regex将字符串分红 limit 份 String[] split(String regex)
按照字符 regex 将字符串分段
getBytes
在建立 String 的时候,可使用 byte[] 数组,将一个字节数组转换成字符串,一样,咱们能够将一个字符串转换成字节数组,那么 String 提供了不少重载的 getBytes 方法。
public byte[] getBytes(){
return StringCoding.encode(value, 0, value.length);
}
复制代码
可是,值得注意的是,在使用这些方法的时候必定要注意编码问题。好比: String s = "你好,世界!"; byte[] bytes = s.getBytes();
这段代码在不一样的平台上运行获得结果是不同的。因为没有指定编码方式,因此在该方法对字符串进行编码的时候就会使用系统的默认编码方式。
在中文操做系统中可能会使用 GBK 或者 GB2312 进行编码,在英文操做系统中有可能使用 iso-8859-1 进行编码。这样写出来的代码就和机器环境有很强的关联性了,为了不没必要要的麻烦,要指定编码方式。
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException{
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}
复制代码
boolean equals(Object anObject);
比较对象 boolean contentEquals(String Buffersb);
与字符串比较内容 boolean contentEquals(Char Sequencecs);
与字符比较内容 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,同样再逐一比较值。 虽然代码写的内容比较多,可是能够很大程度上提升比较的效率。值得学习!!!
StringBuffer 须要考虑线程安全问题,加锁以后再调用
contentEquals 有两个重载:
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 语句。
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]
所谓“冲突”,就是在存储数据计算 hash 地址的时候,咱们但愿尽可能减小有一样的 hash 地址。若是使用相同 hash 地址的数据过多,那么这些数据所组成的 hash 链就更长,从而下降了查询效率。
因此在选择系数的时候要选择尽可能长的系数而且让乘法尽可能不要溢出的系数,由于若是计算出来的 hash 地址越大,所谓的“冲突”就越少,查找起来效率也会提升。
如今不少虚拟机里面都有作相关优化,使用 31 的缘由多是为了更好的分配 hash 地址,而且 31 只占用 5 bits。
在 Java 中,整型数是 32 位的,也就是说最多有 2^32 = 4294967296 个整数,将任意一个字符串,通过 hashCode 计算以后,获得的整数应该在这 4294967296 数之中。那么,最多有 4294967297 个不一样的字符串做 hashCode 以后,确定有两个结果是同样的。
hashCode 能够保证相同的字符串的 hash 值确定相同,可是 hash 值相同并不必定是 value 值就相同。
substring 前面咱们介绍过,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(Char Sequencetarget, Char Sequencereplacement)
public String replace(char oldChar, char newChar){
if(oldChar != newChar){
int len = value.length;
int i = -1;
char[] val = value; /*avoid get field opcode*/
while (++i < len){
if (val[i] == oldChar){
break;
}
}
if( i < len ){
char buf[] = new char[len];
for (intj=0; j<i; j++){
buf[j] = val[j];
}
while (i < len){
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf,true);
}
}
return this;
}
复制代码
好比能够经过 replaceAll (“\d”, “*”)把一个字符串全部的数字字符都换成星号;
相同点是都是所有替换,即把源字符串中的某一字符或字符串所有换成指定的字符或字符串,若是只想替换第一次出现的,可使用 replaceFirst(),这个方法也是基于规则表达式的替换。另外,若是replaceAll() 和r eplaceFirst() 所用的参数据不是基于规则表达式的,则与replace()替换字符串的效果是同样的,即这二者也支持字符串的操做。
copyValueOf 和 valueOf String 的底层是由 char[] 实现的,早期的 String 构造器的实现呢,不会拷贝数组的,直接将参数的 char[] 数组做为 String 的 value 属性。字符数组将致使字符串的变化。
为了不这个问题,提供了 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 方法被调用时,若是对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,不然添加字符串到对象池并返回该字符串的引用。
咱们知道,Java 是不支持重载运算符,String 的 “+” 是 java 中惟一的一个重载运算符,那么 java 使如何实现这个加号的呢?咱们先看一段代码:
public static void main(String[] args) {
String string = "hello";
String string2 = string + "world";
}
复制代码
而后咱们将这段代码的实际执行状况贴出来看看:
public static void main(String args[]){
String string = "hollo";
String string2 = (new StringBuilder(String.valueOf(string))).append("world").toString();
}
复制代码
看了反编译以后的代码咱们发现,其实 String 对 “+” 的支持其实就是使用了 StringBuilder 以及他的 append、toString 两个方法。
String.valueOf和Integer.toString的区别 接下来咱们看如下这段代码,咱们有三种方式将一个 int 类型的变量变成呢过String类型,那么他们有什么区别?
int i = 5;
String i1 = "" + i;
String i2 = String.valueOf(i);
String i3 = Integer.toString(i);
复制代码
第三行和第四行没有任何区别,由于 String.valueOf(i) 也是调用 Integer.toString(i) 来实现的。 第二行代码实际上是 String i1 = (new StringBuilder()).append(i).toString();
首先建立了一个 StringBuilder 对象,而后再调用 append 方法,再调用 toString 方法。
仍是先上代码:
public class switchDemoString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default: break;
}
}
}
复制代码
对编译后的代码进行反编译:
public static void main(String args[]) {
String str = "world";
String s;
switch((s = str).hashCode()) {
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
default: break;
}
}
复制代码
看到这个代码,你知道原来字符串的 switch 是经过 equals() 和 hashCode() 方法来实现的。记住,switch 中只能使用整型,好比 byte,short,char(ackii码是整型) 以及 int。还好 hashCode() 方法返回的是 int 而不是 long。
经过这个很容易记住 hashCode 返回的是 int 这个事实。仔细看下能够发现,进行 switch 的实际是哈希值,而后经过使用 equals 方法比较进行安全检查,这个检查是必要的,由于哈希可能会发生碰撞。
所以性能是不如使用枚举进行 switch 或者使用纯整数常量,但这也不是不好。由于 Java 编译器只增长了一个 equals 方法,若是你比较的是字符串字面量的话会很是快,好比 ”abc” ==”abc” 。若是你把 hashCode() 方法的调用也考虑进来了,那么还会再多一次的调用开销,由于字符串一旦建立了,它就会把哈希值缓存起来。 所以若是这个 siwtch 语句是用在一个循环里的,好比逐项处理某个值,或者游戏引擎循环地渲染屏幕,这里 hashCode() 方法的调用开销其实不会很大。
其实 swich 只支持一种数据类型,那就是整型,其余数据类型都是转换成整型以后在使用 switch 的。
特别要注意的是,String 类的全部方法都没有改变字符串自己的值,都是返回了一个新的对象。
不然会有大量时间浪费在垃圾回收上,由于每次试图修改都有新的String 对象被建立出来。