public final class String
implements java.io.Serializable, Comparable, CharSequence {
//定义用于字符存储的value数组,由于使用了final修饰,表示该value[]一经初始化就不能进行修改
private final char value[];
//缓存字符串的hashcode值
private int hash; // Default to 0
}
final关键字修饰的类不能进行被继承,同时String实现了Comparable,CharSequence ,Serializable接口
接下来我们来看下String的构造方法:
1.无参构造
public String() {
this.value = "".value;
}
2.带有一个字符串对象的有参构造
//初始化一个新创建的字符串对象,它代表了相同的字符序列作为参数;换句话说,新创建的字符串参数字符串的副本,注意这里相当于是复制了一个副本给新的对象,把原对象的hash值也赋给了新的对象
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
我们来验证一下
public static void main(String[] args) {
String str="Shaw_Young";
String s1 = new String(str);
String s2 = new String(str);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
最后输出的hashcode都是一样的
3.带有char[]字符串的有参构造
带有value[]数组的有参构造,在上面的代码片段中,已经定义了value,实质上就一个类型为char的数组,这里新创建的对象同样是一个副本,把传入的char[]数组,原封不动的拷贝到定义的数组中
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
这里会引申出,如果传入的length大于实际value值,结果会怎么样?
public static void main(String[] args) {
String[] str1 = {"123", "234", "345", "456", "789"};
String[] strings = Arrays.copyOf(str1, 8);
for (String string : strings) {
System.out.print(string + ";");
}
}
结果输出: 123;234;345;456;789;null;null;null;
由上面的结果输出可以看出,自动填充了null值
4.带有三个参数的构造方法: 1、char数组,2、偏移量,子数组的第一个字符的索引;3、需要定义的数组的长度
分配一个新的字符串,它包含字符的字符数组参数。偏移量参数是子数组的第一个字符的索引和统计参数指定于数组的长度,子数组的内容复制,字符数组的后序修改不会影响新创建的字符串。
public String(char value[], int offset, int count) {
if (offset < 0) {//偏移量小于0时,抛出数组下标越界
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {数组长度小于0时,抛出数组下标越界
throw new StringIndexOutOfBoundsException(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);
}
这里调用了Arrays数组的copyOfRange方法,从offset这个数组下标开始复制,复制到offset+count结束
public static void main(String[] args) {
char[] str = {'1','2','3','4','5','6','7','8','9','a'};
String var = new String(str,1,5);
char[] chars = var.toCharArray();
for (char aChar : chars) {
System.out.print(aChar+",");
}
}
返回结果为: 2,3,4,5,6,
由此可以推断出就是从offset(给定的char数组的下标索引开始)进行复制,count表示一共复制几个
5带有三个参数的构造方法:1、Unicode代码点阵列的数组,2、偏移量,需要复制的第一个字符的索引;3、需要定义的数组的长度
分配一个新的字符串,它包含字符的Unicode代码点阵列的子数组参数。偏移量参数是索引子数组的第一个代码点和计算参数指定子数组的长度。子数组的内容转换为字符;int数组的后续修改不会影响新创建的字符串。
说白了就是基于unicode进行操作,实际开发中,我是没有用过该方法
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= codePoints.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//final 修饰的基本类型就是一个常量,一经初始化就不能被更改
final int end = offset + count;
// Pass 1: Compute precise size of char[]
// 精确计算char长度,遍历代码点数组,取出有效的代码点长度
int n = count;
for (int i = offset; i < end; i++) {
int c = codePoints[i];
// 判断代码点是不是BMP,其实只要看高16位是否为0即可
if (Character.isBmpCodePoint(c))//codePoint是否有效的代码
continue;
else if (Character.isValidCodePoint(c))
n++;
else throw new IllegalArgumentException(Integer.toString(c));
}
// Pass 2: Allocate and fill in char[]
// 初始化字符串数组长度,并对其进行赋值操作
final char[] v = new char[n];
for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))//如果是unicode则直接复制
v[j] = (char)c;
else
Character.toSurrogates(c, v, j++);//如果不是则进行补充字符
}
this.value = v;
}
Character部分源码
public static final char MIN_VALUE = '\u0000';
public static final char MAX_VALUE = '\uFFFF';
public static final int MIN_CODE_POINT = 0x000000;
public static final int MAX_CODE_POINT = 0X10FFFF;
public static boolean isBmpCodePoint(int codePoint) {
return codePoint >>> 16 == 0;//优化后的源码,判断最高位是否为0
// Optimized form of:
// codePoint >= MIN_VALUE && codePoint <= MAX_VALUE
// We consistently use logical shift (>>>) to facilitate
// additional runtime optimizations.
}
这段代码的判断思路是判断codePoint是否在MIN_CODE_POINT(0x000000)和MAX_CODE_POINT(0X10FFFF)之间,但代码作者对这个判断进行了优化。之所以这样优化是因为非增补字符的范围是0x0000-0xffff,高16位都是0,而增补字符就不一定了所以 codePoint >>> 16可以提取出高16位的位信息。又由于codePoint的合理的最大值是MAX_CODE_POINT(0X10FFFF),自然而然的plane < ((MAX_CODE_POINT + 1) >>> 16)就是用来判断codePoint的高16位是否比MAX_CODE_POINT+1的高16位小。这就达成了codePoint <= MAX_CODE_POINT的判断效果。那么codePoint >= MIN_CODE_POINT的判断从何而来?其实plane < ((MAX_CODE_POINT + 1) >>> 16)已经做到了codePoint >= MIN_CODE_POINT的效果,因为如果是非增补字符,那么plane妥妥的是0,如果codePoint是个负数,那么plane < ((MAX_CODE_POINT + 1) >>> 16)妥妥的不会成立。(此时的低16位的高位为1)
public static boolean isValidCodePoint(int codePoint) {
// Optimized form of:
// codePoint >= MIN_CODE_POINT && codePoint <= MAX_CODE_POINT
int plane = codePoint >>> 16;
return plane < ((MAX_CODE_POINT + 1) >>> 16);
}
从源码注释中可以看出,要么保证dst[index+1],dst[index]都成功,要么都不成功
lowSurrogate()返回的主要代理(低代理代码单元)表示指定的代理对补充字符(Unicode代码点)utf - 16编码,如果指定的字符不是一个补充字符,则返回一个未指定的字符
highSurrogate()返回的主要代理(高代理代码单元)表示指定的代理对补充字符(Unicode代码点)utf - 16编码。如果指定的字符不是一个补充字符,则返回一个未指定的字符
static void toSurrogates(int codePoint, char[] dst, int index) {
// We write elements "backwards" to guarantee all-or-nothing
dst[index+1] = lowSurrogate(codePoint);
dst[index] = highSurrogate(codePoint);
}
6.带有四个参数的构造方法: 1、字节数组 2、需要复制的第一个字节索引(偏移量) 3、长度 4、支持的字符集编码
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
这里涉及到checkBounds方法,我们先来看一下checkBounds源码
这里就是判断截取的数组索引+长度是否在数组内,并且他是一个私有的方法,不提供外被内使用
private static void checkBounds(byte[] bytes, int offset, int length) {
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
}
StringCoding部分源码
static char[] decode(String charsetName, byte[] ba, int off, int len)
throws UnsupportedEncodingException
{
StringDecoder sd = deref(decoder);
String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
|| csn.equals(sd.charsetName()))) {
sd = null;
try {
Charset cs = lookupCharset(csn);
if (cs != null)
sd = new StringDecoder(cs, csn);
} catch (IllegalCharsetNameException x) {}
if (sd == null)
throw new UnsupportedEncodingException(csn);
set(decoder, sd);
}
return sd.decode(ba, off, len);
}
7.下面几个构造方法都是对6的构造方法的调用
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
}
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
8.其它的构造方法
创建一个序列化的字符串数组,由于当前使用同步关键字,当传入的参数传入时,不会影响当前赋值的对象
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
同上面一样的 未加入同步关键字而已,因为本来来讲builder就是线程不安全的
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
私有的构造方法,不被外部使用
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
至此构造方法以及解读完了,下面我们来看下String类其他提供外部使用的方法
获取字符串长度,一个字符无论中文还是英文计算为1
public int length() {
return value.length;
}
判断字符串是否为空,其实是判断字符串的长度,所以我们在项目中基本都需要先判断字符串是否为null,再判断 isEmpty(),否则会抛出空指针异常
public boolean isEmpty() {
return value.length == 0;
}
由于String类本身就是char数组来实现,所有这里取字符串的某个字符,其实就是通过索引获取char数组中的索引下面的某个字符
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
获取字符串当前索引下的字符的unicode值
public int codePointAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointAtImpl(value, index, value.length);
}
获取字符串当前索引前一个的字符的unicode值
public int codePointBefore(int index) {
int i = index - 1;
if ((i < 0) || (i >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointBeforeImpl(value, index, 0);
}
获取字符串当前索引后一个的字符的unicode值
public int codePointBefore(int index) {
int i = index - 1;
if ((i < 0) || (i >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointBeforeImpl(value, index, 0);
}
返回指定起始到结束段内字符个数
public int codePointCount(int beginIndex, int endIndex) {
if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
throw new IndexOutOfBoundsException();
}
return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
}
返回指定索引加上codepointOffset后得到的索引值
public int offsetByCodePoints(int index, int codePointOffset) {
if (index < 0 || index > value.length) {
throw new IndexOutOfBoundsException();
}
return Character.offsetByCodePointsImpl(value, 0, value.length,
index, codePointOffset);
}
这几个函数用得比较少,并且可以看到其本质上都是用Character这个类的一些静态方法来实现。这些功能在平常并不经常使用,个人认为,如果使用的话那应该是在对未知字符串进行处理,且重点在异常处理上。
这里说明一下,16 位unicode编码的所有 65,536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1,112,064 个字符。那些超出原来的16 位限制的字符被称作增补字符。Java的char类型是固定16bits的。代码点在U+0000 — U+FFFF之内到是可以用一个char完整的表示出一个字符。但代码点在U+FFFF之外的,一个char无论如何无法表示一个完整字符。这样用char类型来获取字符串中的那些代码点在U+FFFF之外的字符就会出现问题。
增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的 Unicode 的 16 位设计无法表示的字符。从 U+0000 至 U+FFFF 之间的字符集有时候被称为基本多语言面 (BMP UBasic Multilingual Plane )。因此,每一个 Unicode 字符要么属于 BMP,要么属于增补字符。
将字符串复制到dst数组中,复制到dst数组中的起始位置可以指定。值得注意的是,该方法并没有检测复制到dst数组后是否越界。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
获取当前字符串的二进制
使用字符集,将结果存储到新的字节数组中。
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}
使用给定的Charset ,将结果存储到新的字节数组中。
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, value, 0, value.length);
}
public byte[] getBytes() {
return StringCoding.encode(value, 0, value.length);
}
将String字符串转成二进制的几种方式,可以指定byte数组,也能让其返回一个byte数组。本质上,其实都是调用了StringCoding.encode()这个静态方法。
public boolean equals(Object anObject)
public int hashCode()
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;
}
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()和equals()两个方法比较重要且有所关系就放一起了,equals()是string能成为广泛用于Map[key,value]中key的关键所在。
public boolean contentEquals(CharSequence cs)
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
char v1[] = value;
int n = v1.length;
if (n != cs.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != cs.charAt(i)) {
return false;
}
}
return true;
}
这个主要是用来比较String和StringBuffer或者StringBuild的内容是否一样。可以看到传入参数是CharSequence ,这也说明了StringBuffer和StringBuild同样是实现了CharSequence。源码中先判断参数是从哪一个类实例化来的,再根据不同的情况采用不同的方案,不过其实大体都是采用上面那个for循环的方式来进行判断两字符串是否内容相同。
public int compareTo(String anotherString)
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
这个就是String对Comparable接口中方法的实现了。其核心就是那个while循环,通过从第一个开始比较每一个字符,当遇到第一个较小的字符时,判定该字符串小。
public int compareToIgnoreCase(String str)
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
这个也是比较字符串大小,规则和上面那个比较方法基本相同,差别在于这个方法忽略大小写。可以看到这是通过一个String 内部一个static的内部类实现的,那么为什么还要特地写一个内部类呢,这样其实就是为了代码复用,这样在其他情况下也可以使用这个static内部类。
public boolean regionMatches(int toffset, String other, int ooffset, int len)
public boolean regionMatches(int toffset, String other, int ooffset,
int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;
// Note: toffset, ooffset, or len might be near -1>>>1.
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
比较该字符串和其他一个字符串从分别指定地点开始的n个字符是否相等。看代码可知道,其原理还是通过一个while去循环对应的比较区域进行判断,但在比较之前会做判定,判定给定参数是否越界。
public boolean startsWith(String prefix, int toffset)
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
判断当前字符串是否以某一段其他字符串开始的,和其他字符串比较方法一样,其实就是通过一个while来循环比较。
public int indexOf(int ch, int fromIndex)
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
可以看到这里在if中有一句
ch < Character.MIN_SUPPLEMENTARY_CODE_POINT
而在Character中看到
public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000;
这表明在java中char存储的值通常都是比ox010000小的,就是BMP类型的字符。
而当比这个值大的时候,就是增补字符了,那么会调用Character先判断是否是有效的字符,再进一步处理。
public int lastIndexOf(int ch, int fromIndex)
public int lastIndexOf(int ch, int fromIndex) {
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
int i = Math.min(fromIndex, value.length - 1);
for (; i >= 0; i--) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return lastIndexOfSupplementary(ch, fromIndex);
}
}
和indexOf基本一致,只是顺序反过来。
static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
这个是上面indexOf的一个重载,主要是实现找到某个子串在当前字符串的起始位置,若没找到,则返回-1。
大致说下这里的实现思路:先是进行一系列的初始判定,比如子串长度不能大于当前字符串。然后在当前字符串中找到子串的第一个字符的位置 i ,从这个位置开始,和子串每一个字符比较。若完全匹配,则返回结果,如果在这个过程中,某个字符不匹配,则从 i+1 的位置开始继续寻找子串第一个字符的位置,后继续比较。
public String substring(int beginIndex)
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);
}
这个方法可以返回字符串中一个子串,看最后一行可以发现,其实就是指定头尾,然后构造一个新的字符串。
public String concat(String str)
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
concat的作用是将str拼接到当前字符串后面,通过代码也可以看出其实就是建一个新的字符串。
public String replace(char oldChar, char newChar)
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
替换操作,主要是将原来字符串中的oldChar全部替换成newChar。看这里实现,主要是先找到第一个所要替换的字符串的位置 i ,将i之前的字符直接复制到一个新char数组。然后从 i 开始再对每一个字符进行判断是不是所要替换的字符。
public String trim()
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
这个函数平时用的应该比较多,删除字符串前后的空格,原理是通过找出前后第一个不是空格的字符串,返回原字符串的该子串。