java源码学习(一)

来源:互联网 发布:卡是3g的手机是2g网络 编辑:程序博客网 时间:2024/05/29 07:09

本文转自:文章出处

    • 一定义
    • 二属性
    • 三构造方法
      • 使用字符数组字符串构造一个String
      • 使用字节数组创建一个String
      • 使用StringBuffer 和 StringBuilder构造一个String
      • 一个特殊的保护类型的方法
    • 四其他方法
      • 其他方法
      • getByates
      • 比较方法
      • hashCode
      • subString
      • replaceFirstreplaceAllreplace区别
      • copyValueOf 和 valueOf
      • intern方法
      • String对的重载
      • StringvalueOf和IntegertoString的区别

一、定义

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

从该类的声明中我们可以该类是final的,表示该类不能被继承,同时该类实现了三个接口:java.io.SerializableComparableCharSequence

二、属性

private final char value[];

这是一个字符数组,并且是final类型,他用于存储字符串的内容,从final这个关键词中我们可以看出,Sting的内容一旦被初始化了是不能被更改的。虽然有这样的例子: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的时候,String会Arrays.copyOf()方法Arrays.copyOfRange()。这两个方法是将原有的字符数组内容逐一的复制到目标数组中,也就是String源码中的private final char value []属性。同样,我们也可以用一个String类型的对象来初始化一个String。这里直接将源String中的valuehash两个属性值直接赋值给目标String。因为String一旦定义之后是不可以改变的,所以也就不用担心改变源String的值会影响目标String的值。

当然,在使用一个新的字符数组来创建一个新的String对象的时候,也可以使用字符数组的一部分,只需要多传入两个参数int offsetint count就可以了。

2.使用字节数组创建一个String

在java中,String实例中保存有char[]字符数组,char[]字符数组是以unicode码来存储的,String 和 char为内存形式,byte是网络传输 或存储的序列化形式。所以在很多传输和存储的过程中需要将String和byte[]进行相互的转化。所以,String提供了一系列重载的方法来将一个字符数组转换成String,提到byte和String之间的相互转换就不得不关注编码问题。String (byte[] bytes , Charset charset)是指通过charset解码指定的byte数组bytes,将其解码成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的时候,使用的是下面四种构造方法(带有charset或者charsetName参数)的一种,那么就会使用StringCoding.decode方法进行解码,使用的解码字符集就是我们指定的charset或者charsetName.如果我们在使用byte[]数组构造String的时候没有指定解码使用的字符集的话,那么StringCodingdecode方法首先调用系统默认的编码格式,如果没有指定编码格式则默认使用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 (UnsupporttedEncodingException x) {        warnUnsupportedCharset(csn);    }    try {        return decode("ISO-8859-1",ba, off.len);    }catch (){    }}

3.使用StringBuffer 和 StringBuilder构造一个String

作为String的两个兄弟,StringBuffer和StringBuilder也可以被当作String的参数。

public String (StringBuffer buffer ) {    synchronized(buffer) {        this.value = Arrays.copyOf(buffer.getValue(),buffer.length());    }}

当然,这两个构造方法是很少用到的,至少我从来没有使用过,因为当我们有了StringBuffer和StringBuilder这两行个对象之后,可以使用他们的toString方法来获取String对象。关于效率问题,java的官方文档中明确说到使用StringBuilder要比使用StringBuffer的toSting方法更快一些。原因是StringBuffer的toString方法是synchronized的,在牺牲了效率的情况下保证线程安全。

public String toString(){    return new String (value,0,count);}this. value = Arrays.copyOfRange(value,offset,offset+count);

4.一个特殊的保护类型的方法

String除了提供很多共有供程序员使用的构造方法之外,还提供了一个保护类型的构造方法(Java7),我们看一下它是怎么样的:

String (char[] value,boolean share) {    this.value = value;}

从代码中我们可以看出,该方法和String(char[] value)有两点区别。第一点,多了一个参数boolean share,其实这个参数的存在并没有什么实际的用处,也给了注释,目前只支持true,不支持false。那么可以猜测,加入boolean share的目的是为了区分String(char[] value)方法。第二点区别就是彼此的实现方法不同,我们前面提到过,String (char[] value)方法在创建String的时候会使用到Arrays.copyOf()方法将char[] value中的内容逐一复制到String当中,而这个String(char[] value,boolean share)方法则是将value的引用赋值给String的value。那么也就是说,这个方法传过来的char[] value和 构造出来的String共享同一个char[]数组。那么,java为什么会提供这么一个方法呢?首先,我们分析一下使用该构造函数的好处:

首先,性能好,这个很简单,一个是直接给数组赋值(相当于直接String的value的指针指向char[]数组,一个是逐一拷贝,当然是直接赋值快了。
其次共享内存数组节约内存。

但是,该方法之所以设置为protected,是因为该方法一旦设置为公有的,在外面可以访问的话,那就破坏了字符串的不可变性。例如下面的YY情景:

char [] arr = new char[] {'h','e','l','l','o',' ','w','o','r','l','d'};String s = new String(arr,true);//hello worldarr[0] = 'a';//替换char[] value数组第一个元素为'a'System.out.println(s);//aello world

如果构造方法没有对arr进行拷贝,其他人就可以在字符串修改该数组,由于他们引用的是同一个数组,因此对arr的修改就相当于修改了字符串。

所以,从安全角度考虑,他也是安全的。对于调用它的方法来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。

在Java7之前有很多String里面的方法都是用的是这种“性能好的、节约内存的、安全的”构造函数,比如subStringreplaceconcatvalueOf等方法。
但是在Java7中,subString已经不再使用这种优秀的方法了,为什么呢?虽然这种方法有很多的优点,但是它有一个致命的缺点,对于sun公司的程序员来说是一个不能容忍的bug,那就是它很有可能造成内存泄漏。看一个例子,假设一个方法从某个地方(文件、数据库、网络)取得了一个很长的字符串,然后对其解析并提取其中的一小段数据,这种情况经常发生在网络抓取或者日志分析的时候,下面是实例代码:

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

在这里data只是临时的,真正有用的是aPart,其长度只有20个字符,但是aPart的内部数组却是从data那里共享的,因此虽然data可以被回收,但是它内部的数组却不能(如下图)。这就导致了内存泄露。如果一个程序中经常出现这样的情况就有可能造成严重的后果,如内存溢出、性能下降。

Created with Raphaël 2.1.0datadatachar[] valuechar[] valueaPartaPartdata内部的char[] valueaPart共享data中的char[] value

新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的subString比之前的要更健壮。
虽然subString放弃了使用share的这种构造方法,但是还有一些其他的方法在用,这是为什么呢?首先,这种构造方法对应有很多好处,其次,这些方法不会将字符串的长度变短,也就不会造成元数组中有大量的空间不被使用造成浪费而不能回收,也就是前面说的内存泄漏的情况(内存泄漏是指不用的内存没有办法被释放,比如说contact方法和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将String拆分成limit份
String split(String regex)

String s = "h,o,l,l,i,s,c,h,u,a,n,g";String[] splitAll = s.split(",");String[] splitFiveStrings = s.split(",", 5);for (int i = 0; i < splitAll.length; i++) {    System.out.print(i + "=" + splitAll[i]+",");}System.out.println();for(int i = 0; i< splitFiveStrings.length; i++) {    System.out.print(i + "=" + splitFiveStrings[i]+",");} 

输出结果:
0=h,1=o,2=l,3=l,4=i,5=s,6=c,7=h,8=u,9=a,10=n,11=g,
0=h,1=o,2=l,3=l,4=i,s,c,h,u,a,n,g,

getByates()

在创建String的时候,可以使用byte[]数组来创建。同样,也可以使用String的getBytes()方法来将字符串转换成字节数组。String提供了很多重载的getBytes()方法,需要注意的是,在使用这些方法的时候必须编码的问题。

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

这段代码在不同的平台上运行的结果是不一样的。因为我们没有指定编码的格式,所以在该方法对字符串进行编码的时候就会按照系统默认的编码格式,比如在中文操作系统中可能会使用GBK或者GBK2313进行编码,在英文操作系统中可能会使用IOS-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 offset, int len)//局部匹配boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)//局部匹配

字符串有一些列的方法用于比较两个字符串的关系。前四个返回Boolean的方法很容易理解,前三个就是String和要比较的目标的内容是否相同,一样就返回true,核心代码如下:

int n = value.length;while (n-- != 0) {    if (v1[i] != v2[i]        return false;    i++;}

v1 v2分别代表String的字符数组 和 目标对象的字符数组。第四个和前三个唯一的区别就是它会将两个字符数组的内容都使用toUpperCase方法转换成大写在进行比较,以此来忽略大小写进行比较。相同则返回true,在这里,有几个编程技巧值得我们学习,我们看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 equalsIgnoreCasre (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(n1)+s[1]31(n2)+...+s[n1]

s[i]是String的第i个字符,n是String的长度。那么这里为什么用31,而不是用其他数呢?计算机的乘法设计到位移计算。当一个数乘以2时,就直接拿该数左移一位即可。选择31的原因是因为31是一个素数。

所谓素数:
质数又称素数。指在一个大于1的自然数中,除了以 和 这个数本身外,没有被其他自然数整除的数。

在存储数据计算hash地址的时候,我们希望尽量有同样的hash地址,所谓冲突。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就会越长,从而降低了查询效率。所以在选择系数的时候要选择尽量长的系数并让乘法尽量不要溢出的系数,如果计算出来的hash地址越大,所谓的”冲突”就越少,查询起来的效率就越高。
31可以由i*31 == (i << 5) -1 来表示,现在很多虚拟机都做很多相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占有5bits。

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

前面我们介绍,java7中的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 oldChar, Char newChar)String replace(CharSequence target, CharSequence replacement)

copyValueOf 和 valueOf

String的底层是由char[]实现的:通过一个char[] 类型的value值。早期的String的构造器的实现呢?不会拷贝数组,直接将参数的char[]数组作为String的value属性,然后test[0] = ‘A’,将导致字符串的变化。为了避免这个问题,提供了copyValueOf方法,每次都拷贝新的字符数组来构造新的字符串。但是现在的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 strings = (new StringBuilder(string.valueOf(string))).append("chuang").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()方法其实也是调用Integer.toString()方法来实现的。
String i1 = “” + 1; 这行代码其实就是String i1 = (new StringBuilder()).append(i).toString();

原创粉丝点击