JDK源码-java.lang.String

  1.开篇明志
  
  本文来看看String的源码。
  
  2.Java7 API String介绍
  
  String 类代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。
  
  字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。例如:
  
  String str = "abc";
  
  1
  
  1
  
  等效于:
  
  char data[] = {'a', 'b', 'c'};
  
  String str = new String(data); //其实从这里看出,底层使用char[]数据实现的
  
  1
  
  2
  
  1
  
  2
  
  下面给出了一些如何使用字符串的更多示例:
  
  System.out.println("abc");
  
  String cde = "cde";
  
  System.out.println("abc" + cde);
  
  String c = "abc".substring(2,3);
  
  String d = cde.substring(1, 2);
  
  1
  
  2
  
  3
  
  4
  
  5
  
  1
  
  2
  
  3
  
  4
  
  5
  
  String 类包括的方法可用于检查序列的单个字符、比较字符串、搜索字符串、提取子字符串、创建字符串副本并将所有字符全部转换为大写或小写。大小写映射基于 Character 类指定的 Unicode 标准版。
  
  Java 语言提供对字符串串联符号(”+”)以及将其他对象转换为字符串的特殊支持。字符串串联是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的。字符串转换是通过 toString 方法实现的,该方法由 Object 类定义,并可被 Java 中的所有类继承。
  
  除非另行说明,否则将 null 参数传递给此类中的构造方法或方法将抛出 NullPointerException。
  
  String 表示一个 UTF-16 格式的字符串,其中的增补字符 由代理项对 表示(有关详细信息,请参阅 Character 类中的 Unicode 字符表示形式)。索引值是指 char 代码单元,因此增补字符在 String 中占用两个位置。
  
  String 类提供处理 Unicode 代码点(即字符)和 Unicode 代码单元(即 char 值)的方法。
  
  3.源码分析
  
  类
  
  /
  
  * @since JDK1.0
  
  */
  
  public final class String
  
  implements java.io.Serializable, Comparable<String>, CharSequence {
  
  //省略中间无数代码,哈哈哈哈哈哈哈哈
  
  从该类的声明中我们可以看出String是final类型的,始于jdk1.0,表示该类不能被继承,同时该类实现了三个接口:java.io.Serializable、 Comparable<String>、 CharSequence
  
  属性
  
  /** The value is used for character storage. */
  
  private final char value[];
  
  这是一个字符数组,并且是final类型,他用于存储字符串内容,从fianl这个关键字中我们可以看出,String的内容一旦被初始化了是不能被更改的。 虽然有这样的例子: String s = “a”; s = “b” 但是,这并不是对s的修改,而是重新指向了新的字符串, 从这里我们也能知道,String其实就是用char[]实现的。
  
  /** Cache the hash code for the string */
  
  private int hash; // Default to 0
  
  缓存字符串的hash Code,默认值为 0
  
  /** use serialVersionUID from JDK 1.0.2 for interoperability */
  
  private static final long serialVersionUID = -6849794470754667710L;
  
  /**
  
  * Class String is special cased within the Serialization Stream Protocol.
  
  *
  
  * A String instance is written into an ObjectOutputStream according to
  
  * <a href="{@docRoot}/../platform/serialization/spec/output.html">
  
  * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
  
  */
  
  private static final ObjectStreamField[] serialPersistentFields =
  
  new ObjectStreamField[0];
  因为String实现了Serializable接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
  
  4.构造方法
  
  String类作为一个java.lang包中比较常用的类,自然有很多重载的构造方法.
  
  使用char[]字符数组、字符串构造一个String
  
  //字符串数组构造函数
  
  public String(char value[]) {
  
  this.value = Arrays.copyOf(value, value.length);
  
  }
  
  //使用字符串构造函数
  
  public String(String original) {
  
  this.value = original.value;
  
  this.hash = original.hash;

  我们知道,其实String就是使用字符数组(char[])实现的。所以我们可以使用一个字符数组来创建一个String,那么这里值得注意的是,当我们使用字符数组创建String的时候,会用到Arrays.copyOf方法和Arrays.copyOfRange方法。这两个方法是将原有的字符数组中的内容逐一的复制到String中的字符数组中。
  
  同样,我们也可以用一个String类型的对象来初始化一个String。这里将直接将源String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后是不可以改变的,所以也就不用担心改变源String的值会影响到目标String的值。
  
  使用byte[]字节数组构造一个String
  
  public String(byte bytes[], String charsetName)
  
  throws UnsupportedEncodingException {
  
  this(bytes, 0, bytes.length, charsetName);

  在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[]数组,而又保证不出现乱码,那就要指定其解码方式
  
  同样使用byte[]字节数组来构造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的时候,如果没有指明解码使用的字符集的话,那么java.lang.StringCoding的decode方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作。
  
  //默认编码 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;
  使用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的,在牺牲了效率的情况下保证了线程安全。
  
  StringBuilder.toString()源码如下:
  
  public String toString() {
  
  // Create a copy, don't share the array
  
  return new String(value, 0, count);
  
  保护类型的构造方法
  
  /*
  
  * Package private constructor which shares value array for speed.
  
  * this constructor is always expected to be called with share==true.
  
  * a separate constructor is needed because we already have a public
  
  * String(char[]) constructor that makes a copy of the given char[].
  
  */
  
  String(char[] value, boolean share)

  从代码中我们可以看出,该方法和 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[]数组),一个是逐一拷贝。当然是直接赋值快了。
  
  其次,共享内部数组节约内存
  
  此外,该方法没有显示加访问权限,默认为protected修饰的, 因为一旦该方法设置为公有,在外面可以访问的话,那就破坏了字符串的不可变性。
  
  例如如下情形:
  
  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方法,他们不会导致元数组中有大量空间不被使用,因为他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短!)。
  
  5.其他方法
  
  length() 返回字符串长度
  
  isEmpty() 返回字符串是否为空
  
  charAt(int index) 返回字符串中第(index+1)个字符
  
  char[] toCharArray() 转化成字符数组
  
  trim() 去掉两端空格
  
  toUpperCase() 转化为大写
  
  toLowerCase() 转化为小写
  
  String concat(String str) //拼接字符串
  
  String replace(char oldChar, char www.yigouylpt2.cn 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)
  
  split方法
  
  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,www.acnet.cn 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 www.haiyuanylpt.com );
  
  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) {//比较字节数组,一样返回true, 否则返回false
  
  if (v1[i] != v2[i])
  
  return false;
  
  v1 v2分别代表String的字符数组和目标对象的字符数组。 第四个和前三个唯一的区别就是他会将两个字符数组的内容都使用toUpperCase方法转换成大写再进行比较,以此来忽略大小写进行比较。相同则返回true,不想同则返回false
  
  接下来看看equals方法:
  
  public boolean equals(Object www.yxin7.com/ 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方法:
  
  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]
  
  1
  
  1
  
  s[i]是string的第i个字符,n是String的长度。那为什么这里用31,而不是其它数呢? 计算机的乘法涉及到移位计算。当一个数乘以2时,就直接拿该数左移一位即可!选择31原因是因为31是一个素数!
  
  所谓素数:
  
  质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。
  
  在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率!所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
  
  31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!
  
  在java乘法中如果数字相乘过大会导致溢出的问题,从而导致数据的丢失.
  
  而31则是素数(质数)而且不是很长的数字,最终它被选择为相乘的系数的原因不过与此!
  
  在Java中,整型数是32位的,也就是说最多有2^32= www.yigouylpt2.com 4294967296个整数,将任意一个字符串,经过hashCode计算之后,得到的整数应该在这4294967296数之中。那么,最多有 4294967297个不同的字符串作hashCode之后,肯定有两个结果是一样的, hashCode可以保证相同的字符串的hash值肯定相同,但是,hash值相同并不一定是value值就相同。

posted @ 2017-06-06 10:42  王二狗的人生  阅读(502)  评论(0编辑  收藏  举报