Java源码学习 String(1)

java.lang.String是使用频率非常高的类。要想更好的使用java.lang.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中的所有类继承。

一、定义

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

从该类的声明中我们可以看出String是final类型的,表示该类不能被继承,同时该类实现了三个接口:java.io.Serializable 、Comparable 、CharSequence

二、属性

private final char value[];

这是一个字符数组,用于存储字符串内容,并且是final型的,所以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。

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
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) {
        throw new StringIndexOutOfBoundsException(count);
    }
    // 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类型的对象来初始化一个String。这里直接将原String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后是不可以改变的,所以就不用担心改变原String的值会影响到目标Sting的值。

同样我们可以使用一个字符数组(char[])来创建一个String,那么这里值得注意的是,当我们使用字符数组创建String的时候,会用到Arrays.copyOf方法和Arrays.copyOfRange方法。这两个方法是将所有的字符数组中的内容逐一复制到String的字符数组中。改变char value[]也不会改变新创建的String对象。

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

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

在Java中,String实例中保存有一个char[]字符数组,char[]字符数组是以Unicode码来存储的,String 和 char 为内存形式,byte是以网络传输或存储的序列化形式。所以在很多传输和存储的过程中需要将byte[ ] 数组和String进行相互转化。所以,String提供了一系列重载的构造方法来将一个字符数组转化成String,提到byte[]和String之间的相互转换就不得不关注编码问题。因为bytes字节流是使用charset进行编码的,所以String(byte[] bytes, Charset charset) 是通过charset来解码指定的byte数组,将其解码成unicode的char[]数组,够造成新的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

java.lang.StringBuilder 与 java.lang.StringBuffer 同是继承于 java.lang.AbstractStringBuilder,则从功能实现上,AbstractStringBuilder 是核心,StringBuilder仅仅只是功能的继承;StirngBuffer在功能继承上做了一个synchronized加锁的操作,从而实现线程安全性。

java.lang.AbstractStringBuilder

先来看AbstractStringBuilder这个类,与 java.lang.String 类似,其底层仍是通过字符数组实现字符串的存储。不同的是多了一个 count 参数,以用于记录实际存储的字符个数,而不是字符数组 value 的长度。类声明、属性及构造方法源码如下:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /** The value is used for character storage.*/
    char[] value;
    /** The count is the number of characters used.  */
    int count;
    /** This no-arg constructor is necessary for serialization of subclasses. */
    AbstractStringBuilder() {
    }
    /** Creates an AbstractStringBuilder of the specified capacity.*/
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
}

与 java.lang.String 相比,同是字符数组存储字符串,但 String 中声明的字符数组是 final 类型表示不可修改,而 AbstractStringBuilder 中则可以修改,这也就是为啥 StringBuilder、StringBuffer可实现字符串修改功能了。下面来看部分常用方法的具体实现。

append方法

append 的重构方法比较多,但原理是类似的。功能都是将字符串、字符数组等添加到原字符串中,并返回新的字符串 AbstractStringBuilder。步骤如下:(1)对传入形参正确性进行检查;(2)对原字符数组长度进行检查,判断是否能容纳新加入的字符;(3)对原字符数组进行相应添加操作。
以形参为 String 在 append 方法源码为例。

 public AbstractStringBuilder append(String str) {
         if (str == null) str = "null";
         int len = str.length();
         ensureCapacityInternal(count + len);
         str.getChars(0, len, value, count);
         count += len;
         return this;
 private void ensureCapacityInternal(int minimumCapacity) {
         // overflow-conscious code
         if (minimumCapacity - value.length > 0)
             expandCapacity(minimumCapacity);
     }
     }
 void expandCapacity(int minimumCapacity) {
          int newCapacity = value.length * 2 + 2;
          if (newCapacity - minimumCapacity < 0)
              newCapacity = minimumCapacity;
          if (newCapacity < 0) {
             if (minimumCapacity < 0) // overflow
                  throw new OutOfMemoryError();
              newCapacity = Integer.MAX_VALUE;
          }
         value = Arrays.copyOf(value, newCapacity);
     }

研究这段源码可以发现:如果可以提前预估出最终的数组长度并在创建对象时提前设置数组大小,对程序运行效率的提高是十分有帮助的。(减少了不断扩容、拷贝的内在及时间成本)

delete,replace,insert 方法

delete:可实现删除指定数组起始、终止位置之间的字符。将指定终止位置之后的字符依次向前移动 len 个字符,将起始位置的字符开始依次覆盖掉,相当于字符数组拷贝。

replace:字符数组拷贝。

insert:在数组指定位置插入字符,底层也是字符数组拷贝。

  public AbstractStringBuilder delete(int start, int end) {
          if (start < 0)
              throw new StringIndexOutOfBoundsException(start);
          if (end > count)
              end = count;
          if (start > end)
              throw new StringIndexOutOfBoundsException();
          int len = end - start;
          if (len > 0) {
              System.arraycopy(value, start+len, value, start, count-end);
              count -= len;
          }
          return this;
      }
      
 public AbstractStringBuilder replace(int start, int end, String str) {
         if (start < 0)
             throw new StringIndexOutOfBoundsException(start);
         if (start > count)
             throw new StringIndexOutOfBoundsException("start > length()");
         if (start > end)
             throw new StringIndexOutOfBoundsException("start > end"); 
         if (end > count)
             end = count;
         int len = str.length();
         int newCount = count + len - (end - start);
         ensureCapacityInternal(newCount);
         System.arraycopy(value, end, value, start + len, count - end);
         str.getChars(value, start);
         count = newCount;
         return this;
     }
  public AbstractStringBuilder insert(int index, char[] str, int offset, int len)     {
         if ((index < 0) || (index > length()))
            throw new StringIndexOutOfBoundsException(index);
         if ((offset < 0) || (len < 0) || (offset > str.length - len))
            throw new StringIndexOutOfBoundsException(
                 "offset " + offset + ", len " + len + ", str.length " + str.length);
         ensureCapacityInternal(count + len);
         System.arraycopy(value, index, value, index + len, count - index);
         System.arraycopy(str, offset, value, index, len);
         count += len;
         return this;
     } 
toString 方法
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);

接下来再来看java.lang.StringBuilderjava.lang.StringBuffer

java.lang.StringBuilder

StringBuilder 是一个 final 类,不能被继承。

public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence {}

其内部代码中显式声明(不包括继承等隐式属性)的只有一个属性:serialVersionUID(序列化ID)。其构造方法的内部实现也是通过 super 方法调用父类构造方法实现,具体如下所示:

//initial capacity
public StringBuilder() {
         super(16);
     }
 //Custom capacity
public StringBuilder(int capacity) {
         super(capacity);
     }
     
public StringBuilder(String str) {
         super(str.length() + 16);
         append(str);
     }
 
public StringBuilder(CharSequence seq) {
         this(seq.length() + 16);
         append(seq);
     }    
append 方法

仅以一个 append 方法为例具体看看其内部实现,代码如下:

public StringBuilder append(String str) {
         super.append(str);
         return this;
     }

在该方法内部仍然是一个 super 方法,调用父类在方法实现,只是做了一层外壳。其它的 delete,replace,insert 方法源代码也是如此,这里就不一一展示了。

toString 方法

append,delete,replace,insert等方法不同的是,toString 方法不是通过 super 方法调用父类的实现。但其实现中所用到的 value,count 属性依然是从父类中继承的,其实现仍然很简单,如下所示:

 public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

java.lang.StringBuffer

其类声明和构造方法与 StringBuilder 完全一样。各功能方法内部实现上也完全一样,具体实现调用 super 方法通过父类实现。唯一的不同之处便是:功能方法前面多了一个同步关键字 synchronized

public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence
 { 
     /** use serialVersionUID from JDK 1.0.2 for interoperability */
     static final long serialVersionUID = 3388685877147921107L;
 }
append方法
 public synchronized StringBuffer append(Object obj) {
          super.append(String.valueOf(obj));
          return this;
      }

java.lang.String 相比,同是字符数组存储字符串,但 String 中声明的字符数组是 final 类型表示不可修改,而 AbstractStringBuilder 中则可以修改,这也就是为啥 StringBuilderStringBuffer可实现字符串修改功能了。
StringBuilder 与 StringBuffer 均是 final 类,无法再被继承。
关于效率问题,Java的官方文档有提到说使用StringBuilder的toString方法会更快一些,原因是StringBuffer的toString方法是synchronized的,在牺牲了效率的情况下保证了线程安全。

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

String 除了提供了很多公有的供程序员使用的构造方法以外,还提供了一个保护类型的构造方法(Java 7)。

String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

从代码中我们可以看出,该方法和 String(char[] value)有两点区别:

  1. 该方法多了一个参数: boolean share ,其实这个参数在方法体中根本没被使用,也给了注释,目前不支持使用false,只使用true。那么可以断定,加入这个share的只是为了区分于String(char[] value)方法,不加这个参数就没办法定义这个函数,只有参数不能才能进行重载。
  2. 第二个区别就是具体的方法实现不同。String(char[] value)方法在创建String的时候会用到 会用到Arrays的copyOf方法将value中的内容逐一复制到String当中,而这个String(char[] value, boolean share)方法则是直接将value的引用赋值给String的value。也就是说,这个方法构造出来的String和参数传过来的char[] value共享同一个数组。

那么,为什么Java会提供这样一个方法呢? 首先,我们分析一下使用该构造函数的好处 那么,为什么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的修改就相当于修改了字符串。

在Java 7 之前有很多String里面的方法都使用这种“性能好的、节约内存的、安全”的构造函数。比如:substring、replace、concat、valueOf等方法(实际上他们使用的是public String(char[], int, int)方法,原理和本方法相同,已经被本方法取代)。

但是在Java 7中,substring已经不再使用这种“优秀”的方法了,为什么呢? 虽然这种方法有很多优点,但是他有一个致命的缺点,那就是他很有可能造成内存泄露。 看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。下面是示例代码。

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

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

但是这种share数组的方法还是有一些其他方法在使用的,比如说concat方法和replace方法,他们不会导致元数组中有大量空间不被使用,因为他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短!

关于内存泄露问题可以学习这个文章
String使用不当可能导致内存泄露

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值