根据java官网文档的描述,String类代表字符串,是常量,他们的值在创建之后是不可变的,究竟String类型是怎么实现这些的呢?
final关键字
在探讨String类型的原理之前,我们应该先弄清楚关于final关键字的使用:
1> 如果final修饰的是类的话,那么这个类是不能被继承的
2> 如果final修饰的是方法的话,那么这个方法是不能被重写的
3> 如果final修饰的是变量的话,那么这个变量的值在运行期间是不能被修改的
当然,关于具体的赋值等操作,可以参考《对象与内存管理》中的最后一点,这里就不再重复了。
String类与final的不解之缘
现在,我们开始探讨String类吧,下面只是String类的部分源代码:
1 public final class String 2 implements java.io.Serializable, Comparable<String>, CharSequence 3 { 4 /** The value is used for character storage. */ 5 private final char value[]; //用来存储字符串转换而来的字符数组 6 7 /** The offset is the first index of the storage that is used. */ 8 private final int offset; //字符串起始字符在字符数组的位置 9 10 /** The count is the number of characters in the String. */ 11 private final int count; //字符串分解成字符数组后字符的数目 12 }
从上面代码,我们知道:
1> String类是被final修饰的,从安全角度来说,通过final修饰后的String类是不能被其他类继承的,在最大程度上的保护了该类,从效率上来说,提高了该类的效率,因为final修饰后会比没有用final修饰的快。
2> value[], offet, count也是被final修饰的,这时候三个变量的值必须在编译期间就被确定下来,并且在运行期间不能再被修改了。因此,每次我们每次进行字符串修改、拼接的时候,并不能直接修改当前String对象的值,只能重新创建一个新的对象。
3>我们创建String对象的时候,String对象还使用字符数组(char[])来存储我们的字符串的。
CodePoint & CodeUnit
1. CodePoint:一个完整的字符就是一个CodePoint(代码点),例如:'A', 'B', 'C'
2. CodeUnit :一个char就是一个CodeUnit(代码单元),想'A', 'B'这些,他们就只使用了了一个CodeUnit;但是如果是使用UTF-16表示的字符,就需要两个CodeUnit
下面我们看一个代码:
1 public class CodePoint { 2 public static void main(String[] args){ 3 /* 4 * 这个字符串中有一个Unicode编码表中附加级别的字符。(是一个数学符号) 5 * '\u' 后面只能跟 4 个字符,\u1D56B 表示的是 \u1D56 字符和字母 B, 6 * 所以使用 codePointCount 获得的代码点数量就是 2 了。 7 */ 8 String str1="\u1D56B"; 9 /* 10 * 这个字符串是上面那个字符串采用UTF-16编码方法拆成的两个连续的代码单元中的值 11 * 下面这种方式是采用代理对的方式来表示这个字符, 12 * 虽然说采用两个 Unicode 的代理对来表示,但这只是表示 Unicode 中的一个代码点 13 */ 14 String str2="\uD875\uDD6B"; 15 16 System.out.println(str1); 17 System.out.println(str1.length()); //打印结果:2 18 System.out.println(str1.codePointCount(0,str1.length())); //打印结果:2 19 20 System.out.println(str2); 21 System.out.println(str2.length()); //打印结果:2 22 System.out.println(str2.codePointCount(0,str2.length())); //打印结果:1 23 } 24 }
从上面的打印结果我们也可以看出,length() 和 codePointCount()方法的区别是:
1> codePointCount方法返回的是代码点的数量
2> length()方法返回的是代码单元的数量
String类常用的构造方法
其实呢,一般情况下,我们使用String类的时候很少通过构造方法来创建String对象的,因为这是不推荐的,但是不知道大家知不知道,
1 String str = "abc"; //只创建了一个String对象
这种通过直接量创建String对象其实就等效于下面使用了通过字符串构造方法创建对象的。
1 char data[] = {'a', 'b', 'c'}; 2 String str = new String(data); //只创建了一个String对象
但是一般情况下使用第二这种方式太麻烦了,所以我们都推荐使用第一种方式创建String对象。
下面我们开始讲解一下String类常用的构造方法吧。
1>无参数的构造方法,这个创建的String对象的值为"",注意是是"",这个就等效于我们String str = "";具体关于参数的相信不用讲大家都应该知道了吧,不记得了的朋友可以看回前面final中列举出的代码注释。
1 /** 2 * Initializes a newly created {@code String} object so that it represents 3 * an empty character sequence. Note that use of this constructor is 4 * unnecessary since Strings are immutable. 5 */ 6 public String() { 7 this.offset = 0; 8 this.count = 0; 9 this.value = new char[0]; 10 }
2>使用Stirng对象作为构造方法的参数,需要注意的是,通过该构造方法创建String对象将会产生2个字符串对象,所以不推荐使用(具体为什么是两个,可以参考博文《小学徒进阶系列—JVM对String的处理》)
1 public String(String original) { 2 int size = original.count; //获取源字符串的字符数量 3 char[] originalValue = original.value; //获取源字符串的字符数组 4 char[] v; //用于新字符串对象存储字符数组 5 if (originalValue.length > size) { 6 int off = original.offset; //获取源字符串起始字符在字符数组的位置 7 v = Arrays.copyOfRange(originalValue, off, off+size); //返回将源字符数组复制到新的字符数组中 8 } else { 9 // The array representing the String is the same 10 // size as the String, so no point in making a copy. 11 v = originalValue; 12 } 13 this.offset = 0; 14 this.count = size; 15 this.value = v; 16 }
其实在构造方法中的各行代码里,我想大家在看这行代码的时候,最想知道的应该是Arrays.copyOfRange(char[] original,int from,int to)中各个参数的含义吧,官网中的解释是这样子的:
public static char[] copyOfRange(char[] original, int from, int to) 将指定数组的指定范围复制到一个新数组。该范围的初始索引 (from) 必须位于 0 和 original.length(包括)之间。original[from] 处的值放入副本的初始元素中(除非 from == original.length 或 from == to)。原数组中后续元素的值放入副本的后续元素。该范围的最后索引 (to)(必须大于等于 from)可以大于 original.length,在这种情况下,'\\u000' 被放入索引大于等于 original.length - from 的副本的所有元素中。返回数组的长度为 to - from。
参数: original - 将要从其复制一个范围的数组 from - 要复制的范围的初始索引(包括) to - 要复制的范围的最后索引(不包括)。(此索引可以位于数组范围之外)。
返回: 包含取自原数组指定范围的新数组,截取或用 0 填充以获得所需长度
抛出: ArrayIndexOutOfBoundsException - 如果 from < 0 或 from > original.length() IllegalArgumentException - 如果 from > to NullPointerException - 如果 original 为 null
3>使用字符数组作为String构造方法的参数,前面你我们已经知道了,使用String str = "abc",相当于使用该构造方法创建对象
1 public String(char value[]) { 2 int size = value.length; 3 this.offset = 0; 4 this.count = size; 5 this.value = Arrays.copyOf(value, size); 6 }
4>同样使用字符数组作为String构造方法的参数,但是并不是全部都是用来构造字符串对象的,而是只使用从offerset起的count个字符作为String对象的值。
1 public String(char value[], int offset, int count) { 2 if (offset < 0) { 3 throw new StringIndexOutOfBoundsException(offset); 4 } 5 if (count < 0) { 6 throw new StringIndexOutOfBoundsException(count); 7 } 8 // Note: offset or count might be near -1>>>1. 9 if (offset > value.length - count) { 10 throw new StringIndexOutOfBoundsException(offset + count); 11 } 12 this.offset = 0; 13 this.count = count; 14 this.value = Arrays.copyOfRange(value, offset, offset+count); 15 }
String类常用的方法
在这里,我重点讲解关于String类常用的方法,同时分析它的源代码,具体使用方法和执行结果,读者可以自行尝试哦,我就不累赘的写出来啦,而且我把这些代码缩起来了,避免整篇博文都是源代码,看的辛苦,大家需要看哪个方法的源代码,就直接展开哪个方法就行了,好啦,言归正传,我们开始分析吧。
1> public char charAt(int index)
返回指定索引处的 char 值。索引范围为从 0 到 length() - 1。序列的第一个 char 值位于索引 0 处,第二个位于索引 1 处,依此类推,这类似于数组索引。下面是该方法的源码解析:
1 public char charAt(int index) { 2 if ((index < 0) || (index >= count)) { //如果index索引 < 0 或者 > 字符串长度, 3 throw new StringIndexOutOfBoundsException(index); //抛出超出边界的异常 4 } 5 return value[index + offset]; //如果索引合法,直接取出在字符数组中索引对应的值 6 }