String源码分析

参照:http://www.hollischuang.com/archives/99的文章

String类实现了Serializable, Comparable, CharSequence接口.

public final class String implements java.io.Serializable, Comparable<String>, CharSequence{}

其中,String被final修饰表示String是一个不可变的类。
所谓不可变类,就是类一旦被实例化,该实例的属性是不可改变的,如java中的包装类和java.lang.String都是属于不可变类。
String不能被继承,是线程安全的,这样设计的原因如下:
http://blog.csdn.net/u013905744/article/details/52414111

成员变量:

String类中包含一个不可变的char数组用来存放字符串,一个int型的变量hash用来存放计算后的哈希值。

//用于存储字符串,从 fianl 关键字可以看出,String 的内容一旦被初始化后,其不能被修改的。
private final char value[];

//缓存String的hash值
private int hash; // Default to 0

/** 
serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。具体解释如下:
http://swiftlet.net/archives/1268 
*/
private static final long serialVersionUID = -6849794470754667710L;

 /**
 也是序列化和反序列化有关
 */
private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];

构造函数:

1、空构造方法
该构造方法会创建空的字符序列,注意这个构造方法的使用,因为创造不必要的字符串对象是不可变的。因此不建议采取下面的创建 String 对象:

public String() {
        this.value = new char[0];
    }

2、使用字符串类型的对象来初始化
这里将直接将源 String 中的 value 和 hash 两个属性直接赋值给目标 String。因为 String 一旦定义之后是不可以改变的,所以也就不用担心改变源
String 的值会影响到目标 String 的值。

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
}

3、使用字符数组来初始化

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中的字符数组中。会创建一个新的字符串对象,随后修改的字符数组不影响新创建的字符串。

4、使用字节数组来初始化
在Java中,String实例中保存有一个char[]字符数组,char[]字符数组是以unicode码来存储的,String 和 char 为内存形式,byte是网络传输或存储的序列化形式。所以在很多传输和存储的过程中需要将byte[]数组和String进行相互转化。所以,String提供了一系列重载的构造方法来将一个字符数组转化成String,提到byte[]和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());
}

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共享同一个数组。
好处是:
首先,性能好,直接给数组赋值(相当于直接将String的value的指针指向char[]数组)。其次,共享内部数组节约内存
从安全性角度考虑,他也是安全的。对于调用他的方法来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,其次,它是默认类型的封装,外部访问不了,因此对两个字符串来说都很安全。
坏处:有可能会造成内存泄漏。
看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析,如下:

String aLongString = "...a very long string..."; 
String aPart = data.substring(20, 40);
return aPart;

真正有用的是aPart,其长度只有20个字符,但是它的内部数组却是从aLongString那里共享的,因此虽然aLongString本身可以被回收,但它的内部数组却不能。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。

其他方法

length() 返回字符串长度
isEmpty() 返回字符串是否为空
charAt(int index) 返回字符串中第(index+1)个字符
getChars(char dst[], int dstBegin)  将字符从此字符串复制到目标字符数组
void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) 将字符从此字符串的srcBegin开始到srcEnd结束的子串,复制到从dstBegin开始的目标字符数组
char[] toCharArray() 转化成字符数组
trim() 去掉两端空格
toUpperCase() 转化为大写
toLowerCase() 转化为小写
//以下两个方法都使用了String(char[] value, boolean share);
String concat(String str) //拼接字符串
String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar字符
//将一个字符串转换成字节数组,使用默认字符集
getBytes()
//同上,使用指定字符集
getBytes(Charset charset)
//同上,使用指定字符集
getBytes(String charsetName)

在使用这些方法的时候一定要注意编码问题。比如:

String s = "你好,世界!"; 
byte[] bytes = s.getBytes();

这段代码在不同的平台上运行得到结果是不一样的。由于我们没有指定编码方式,所以在该方法对字符串进行编码的时候就会使用系统的默认编码方式,比如在中文操作系统中可能会使用GBK或者GB2312进行编码,在英文操作系统中有可能使用iso-8859-1进行编码。这样写出来的代码就和机器环境有很强的关联性了,所以,为了避免不必要的麻烦,我们要指定编码方式。

比较方法

boolean equals(Object anObject);

boolean contentEquals(StringBuffer sb);
boolean contentEquals(CharSequence cs);


boolean equalsIgnoreCase(String anotherString);

int compareTo(String anotherString);
//使用CASE_INSENSITIVE_ORDER.compare(this, str)
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) //检测两个字符串在一个区域内是否相等(ignoreCase==True忽略大小写)

前四个返回boolean的方法很容易理解,前三个比较就是比较String和要比较的目标对象的字符数组的内容,一样就返回true,不一样就返回false,核心代码如下:

 int n = value.length;
 while (n-- != 0) {
     //v1 v2分别代表String的字符数组和目标对象的字符数组
     if (v1[i] != v2[i])
         return false;
     i++;
 }

第四个和前三个方法唯一的区别就是他会将两个字符数组的内容都使用toUpperCase方法转换成大写再进行比较,以此来忽略大小写进行比较。相同则返回true,不想同则返回false

equals方法首先判断this == anObject ?,也就是说判断要比较的对象和当前对象是不是同一个对象,如果是直接返回true,如不是再继续比较,然后在判断anObject是不是String类型的,如果不是,直接返回false,如果是再继续比较,到了能终于比较字符数组的时候,他还是先比较了两个数组的长度,不一样直接返回false,一样再逐一比较值。 虽然代码写的内容比较多,但是可以很大程度上提高比较的效率。值得学习~~!!!

contentEquals有两个重载,StringBuffer需要考虑线程安全问题,再加锁之后调用contentEquals((CharSequence) sb)方法。contentEquals((CharSequence) sb)则分两种情况,一种是cs instanceof AbstractStringBuilder,另外一种是参数是String类型。具体比较方式几乎和equals方法类似,先做“宏观”比较,在做“微观”比较。

public static final Comparator<String>
    CASE_INSENSITIVE_ORDER = 
        new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
     implements Comparator<String>, java.io.Serializable{}

String类中包含一个内部类CaseInsensitiveComparator,其实现了Comparator接口,重写了compare方法,并进行了实例化CASE_INSENSITIVE_ORDER,作为定义好的排序准则,与compareTo排序规则相同。

//从toffset位置开始,比较prefix是否存在于目的字符串中
boolean startsWith(String prefix, int toffset)
//调用了startsWith(suffix, 0)方法
boolean startsWith(String prefix)
//调用了startsWith(suffix, value.length - suffix.value.length))方法
endsWith(String suffix)

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作为系数是因为它是一个奇素数。
Effective Java》是这样说的:之所以选择31,是因为它是个奇素数,如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的 好处并不是很明显,但是习惯上都使用素数来计算散列结果。31有个很好的特性,就是用移位和减法来代替乘法,得到更好的性能:31*i== (32*i - i)==(i<<5)-i。现在的VM可以自动完成这种优化。

在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率!所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
在Java中,整型数是32位的,也就是说最多有2^32= 4294967296个整数,将任意一个字符串,经过hashCode计算之后,得到的整数应该在这4294967296数之中。那么,最多有 4294967297个不同的字符串作hashCode之后,肯定有两个结果是一样的。
终上所述,因为hash值是int类型,系数要选择0-32以内的数,且要选计算出来的hash地址越大,且又因为31有个很好的特性,就是用移位和减法来代替乘法,得到更好的性能,所以选31。

注意:hashCode 可以保证相同的字符串的 hash 值肯定相同,但是 hash 值相同并不一定是 value 值就相同

substring

java 7 中的 substring 方法使用String(value, beginIndex, subLen) 方法创建一个新的 String 并返回,这个方法会将原来的 char[] 中的值逐一复制到新的 String 中,两个数组并不是共享的,虽然这样做损失一些性能,但是有效地避免了内存泄露。

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);
}

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对象,在构造器中就通过拷贝新数组实现了,所以这两个方面在本质上已经没区别了。

 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方法。

switch对字符串支持的实现

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类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
如果你需要一个可修改的字符串,应该使用StringBuffer 或者 StringBuilder。
否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string对象被创建出来。
如果你只需要创建一个字符串,你可以使用双引号的方式,如果你需要在堆中创建一个新的对象,你可以选择构造函数的方式。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值