Java 中数据类型分为两大类:基础数据类型(byte,short,int,long,float,double,char,boolean)和引用类型(String 类型和基础数据类型的包装类),可以看出 String 类型是非常特殊的,同时也是编写代码过程中使用比较频繁的一种类型,为了更好的了解该类型,决心钻研一下 String 类源码,希望能有所收获。
public final class String implements Serializable, Comparable<String>, CharSequence 复制代码
从该类的声明中我们可以看出String是final类型的,表示该类不能被继承,同时该类实现了三个接口。
String 的底层是由 char 数组构成的
private final char[] value; private int hash; 复制代码
由于底层 char 数组是 final 的,所以 String 对象是不可变的,且不可被继承。 value:是一个 private
final 修饰的 char 数组,String 类是通过该数组来存在字符串的。 hash:是一个 private 修饰的 int
变量,用来存放 String 对象的 hashCode。
private staticfinal long serialVersionUID = -6849794470754667710L; private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; 复制代码
因为String实现了Serializable接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
private static class CaseInsensitiveComparator implements Comparator<String>, Serializable 复制代码
该类同样实现了 Comparator 和 Serializable 接口,用于 String 类对象的排序。
public int compare(String var1, String var2) { int var3 = var1.length(); int var4 = var2.length(); int var5 = Math.min(var3, var4); for(int var6 = 0; var6 < var5; ++var6) { char var7 = var1.charAt(var6); char var8 = var2.charAt(var6); if (var7 != var8) { var7 = Character.toUpperCase(var7); var8 = Character.toUpperCase(var8); if (var7 != var8) { var7 = Character.toLowerCase(var7); var8 = Character.toLowerCase(var8); if (var7 != var8) { return var7 - var8; } } } } return var3 - var4; } 复制代码
从源码来看,在该类中存在一个 compare 方法,方法对两个对象的比较流程如下:循环的次数为长度最小的字符串的长度;从头开始比较每个字符,如果不相等则转换为大写再比较,再不相等转换为小写比较,最后字符之间相减。减法操作会将获取字符的 hashCode,然后相减(汉字也是如此)。如果循环过程中字符比较都相等,最后返回两个字符串对象长度的差值。
public String() { this.value = "".value; } 复制代码
String s = new String(); System.out.println(s); //值为"",也就是空字符串 System.out.println(s.hashCode()); //hash未赋初始值,所以默认值为0,后期再详细讲hashCode方法 复制代码
public String(String var1) { this.value = var1.value; this.hash = var1.hash; } 复制代码
首先声明一下 Java 的语法是允许在一个类中访问该类的实例对象的私有属性的。但是其他类就不可了。
public String(char[] var1) { this.value = Arrays.copyOf(var1, var1.length); } //var2是字符数组开始截取的位置,从0开始;var3是截取的长度 public String(char[] var1, int var2, int var3) { if (var2 < 0) { throw new StringIndexOutOfBoundsException(var2); } else { if (var3 <= 0) { if (var3 < 0) { throw new StringIndexOutOfBoundsException(var3); } if (var2 <= var1.length) { this.value = "".value; return; } } if (var2 > var1.length - var3) { throw new StringIndexOutOfBoundsException(var2 + var3); } else { this.value = Arrays.copyOfRange(var1, var2, var2 + var3); } } } 复制代码
同样都是用字符数组创建 String,前者是复制完整的字符数组到 String 中的 value 值,后者是从截取字符数组的一部分内容复制到 String 中。使用 Arrays.copyOf 方法或 Arrays.copyOfRange 方法进行复制,创建一个新的字符串对象,随后修改的字符数组不影响新创建的字符串。
public String(int[] var1, int var2, int var3) { if (var2 < 0) { throw new StringIndexOutOfBoundsException(var2); } else { if (var3 <= 0) { if (var3 < 0) { throw new StringIndexOutOfBoundsException(var3); } if (var2 <= var1.length) { this.value = "".value; return; } } if (var2 > var1.length - var3) { throw new StringIndexOutOfBoundsException(var2 + var3); } else { int var4 = var2 + var3; int var5 = var3; int var7; for(int var6 = var2; var6 < var4; ++var6) { var7 = var1[var6]; if (!Character.isBmpCodePoint(var7)) {//判断是否为负数,正数为true if (!Character.isValidCodePoint(var7)) { throw new IllegalArgumentException(Integer.toString(var7)); } ++var5; } } char[] var10 = new char[var5]; var7 = var2; for(int var8 = 0; var7 < var4; ++var8) { int var9 = var1[var7]; if (Character.isBmpCodePoint(var9)) { var10[var8] = (char)var9; } else { Character.toSurrogates(var9, var10, var8++); } ++var7; } this.value = var10; } } } 复制代码
需要注意的是:作为参数的 int 数组中值,至少需要满足“大写字母(A-Z):65 (A)~ 90(Z);小写字母(a-z):97(a) ~ 122(z);字符数字(‘0’ ~ ‘9’):48(‘0’) ~ 57(‘9’)”的条件。当数组中值为其他数字时,得到的字符串结果可能为空或特殊符号。
在 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[] 数组,而又保证不出现乱码,那就要指定其解码方式。
通过字节数组构造 String 有很多形式,会使用 StringCoding.decode 方法进行解码,按照是否指定解码方式分的话可以分为两种:
a、
public String(byte[] var1, int var2, int var3) { checkBounds(var1, var2, var3); this.value = StringCoding.decode(var1, var2, var3); } public String(byte[] var1) { this((byte[])var1, 0, var1.length); } 复制代码
这两种构造方法没有指定编码格式,默认使用 ISO-8859-1 编码格式进行编码操作。
static char[] decode(byte[] var0, int var1, int var2) { String var3 = Charset.defaultCharset().name(); try { return decode(var3, var0, var1, var2); } catch (UnsupportedEncodingException var6) { warnUnsupportedCharset(var3); try { return decode("ISO-8859-1", var0, var1, var2); } catch (UnsupportedEncodingException var5) { MessageUtils.err("ISO-8859-1 charset not available: " + var5.toString()); System.exit(1); return null; } } } 复制代码
b、
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) 复制代码
当构造方法参数中带有 charsetName 或者 charset 的时候,使用的解码的字符集就是我们指定的 charsetName 或者 charset。
public String(StringBuffer var1) { synchronized(var1) { this.value = Arrays.copyOf(var1.getValue(), var1.length()); } } public String(StringBuilder var1) { this.value = Arrays.copyOf(var1.getValue(), var1.length()); } 复制代码
这两个构造方法是很少用到的,平时多使用 StringBuffer.toString 方法或者 StringBuilder.toString 方法。其中 StringBuffer.toString 是调用 String(char[] var1, boolean var2)
;而 StringBuilder.toString 则是调用 String(char[] var1, int var2, int var3)
。关于效率问题,Java 的官方文档有提到说使用 StringBuilder 的 toString 方法会更快一些,原因是 StringBuffer 的 toString 方法是 synchronized 的,在牺牲了效率的情况下保证了线程安全。
String(char[] var1, boolean var2) { this.value = var1; } 复制代码
从代码中我们可以看出,该方法和 String(char[] value)有两点区别:
在网上看到说是有两点区别,关于第二点区别,关键在于调用方调用该构造方法前是怎么处理的,在 StringBuffer.toString 方法中可以详细的看出 new String(char[] var1, boolean var2) 的使用。
public synchronized String toString() { if (this.toStringCache == null) { this.toStringCache = Arrays.copyOfRange(this.value, 0, this.count); } return new String(this.toStringCache, true); } 复制代码
其中 toStringCache 表示缓存,用来保存上一次调用 toString 的结果,如果 value 的字符串序列发生改变,就会将它清空。首先判断 toStringCache 是否为 null,如果是先将 value 通过 Arrays.copyOfRange 方法复制到缓存里,然后使用 toStringCache new一个 String。
综上,我认为该构造方法虽然无法拿来直接使用,可是在别的地方可以使用,比如说 StringBuffer.toString 方法中。至于为何要再构建这么一个特殊的构造方法,而不是直接使用 String(char[] value) 方法,原因在于 StringBuffer 中的 toStringCache 属性存在,它的意义不支持在 toString 方法中直接使用 String(char[] value) 方法。(该部分仅为个人观点,如有差错,请指正!)
讲到这里,就需要讲下 String 类中还有没有其他的方法像这个构造函数那样“性能好的、节约内存的、安全”。其实在 Java7 之前 String 类中也有很多这样的方法,比如 substring,replace,concat,valueOf
等方法,实际上它们使用的是 String(int var1, int var2, char[] var3)方法来实现。
但是在 Java 7 之后,substring 已经不再使用这种“优秀”的方法了,以下是 Java8 中的源码:
public String substring(int var1) { if (var1 < 0) { throw new StringIndexOutOfBoundsException(var1); } else { int var2 = this.value.length - var1; if (var2 < 0) { throw new StringIndexOutOfBoundsException(var2); } else { return var1 == 0 ? this : new String(this.value, var1, var2); } } } 复制代码
Java8 中 substring 方法涉及到的 new String 源码如下:
public String(char[] var1, int var2, int var3) { if (var2 < 0) { throw new StringIndexOutOfBoundsException(var2); } else { if (var3 <= 0) { if (var3 < 0) { throw new StringIndexOutOfBoundsException(var3); } if (var2 <= var1.length) { this.value = "".value; return; } } if (var2 > var1.length - var3) { throw new StringIndexOutOfBoundsException(var2 + var3); } else { this.value = Arrays.copyOfRange(var1, var2, var2 + var3); } } } 复制代码
会对传进来的 value 值通过 Arrays.copyOfRange 方法进行拷贝。
反观 Java6 中 substring 方法涉及到的 new String 源码如下:
String(int var1, int var2, char[] var3) { this.value = var3; this.offset = var1; this.count = var2; } 复制代码
同 new String(char[] var1, boolean var2) 构造函数的第二个特点一样,构造出来的 String 和参数传过来的 char[] value 共享同一个数组 。这就可能会造成内存泄漏问题。
看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。
下面是示例代码:
String aLongString = "...averylongstring..."; String aPart = aLongString .substring(20, 40); //aPart字符串共享aLongString部分数据 return aPart; 复制代码
在这里 aLongString 只是临时的,真正有用的是 aPart,其长度只有 20 个字符,但是它的内部数组却是从 aLongString 那里共享的,因此虽然 aLongString 本身可以被回收,但它的内部数组却不能释放。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。
新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的 substring 比原来的更健壮。
length() 返回字符串长度 isEmpty() 返回字符串是否为空 charAt(int index) 返回字符串中第(index+1)个字符(数组索引) char[] toCharArray() 转化成字符数组 trim()去掉两端空格 toUpperCase()转化为大写 toLowerCase()转化为小写 boolean matches(String regex) 判断字符串是否匹配给定的regex正则表达式 boolean contains(CharSequence s) 判断字符串是否包含字符序列 s String[] split(String regex, int limit) 按照字符 regex将字符串分成 limit 份 String[] split(String regex) 按照字符 regex 将字符串分段 复制代码
public String concat(String var1) { int var2 = var1.length(); if (var2 == 0) { return this; } else { int var3 = this.value.length; char[] var4 = Arrays.copyOf(this.value, var3 + var2); var1.getChars(var4, var3); return new String(var4, true); } } 复制代码
拼接 var1 会生成一个新的字符串对象,对原有字符串无影响。
byte[] getBytes() 使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。 byte[] getBytes(String var1) 使用指定的字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。 byte[] getBytes(Charset var1) 使用给定的 charset 将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。 void getBytes(int var1, int var2, byte[] var3, int var4) 已过时 复制代码
值得注意的是,在使用这些方法的时候一定要注意编码问题。比如: String s = "你好,世界!"; byte[] bytes = s.getBytes();
这段代码在不同的平台上运行得到结果是不一样的。由于没有指定编码方式,所以在该方法对字符串进行编码的时候就会使用系统的默认编码方式。
在中文操作系统中可能会使用 GBK 或者 GB2312 进行编码,在英文操作系统中有可能使用 iso-8859-1 进行编码。这样写出来的代码就和机器环境有很强的关联性了,为了避免不必要的麻烦,要指定编码方式。
public boolean equals(Object var1) { if (this == var1) { return true; } else { if (var1 instanceof String) { String var2 = (String)var1; int var3 = this.value.length; if (var3 == var2.value.length) { char[] var4 = this.value; char[] var5 = var2.value; for(int var6 = 0; var3-- != 0; ++var6) { if (var4[var6] != var5[var6]) { return false; } } return true; } } return false; } } public int hashCode() { int var1 = this.hash; if (var1 == 0 && this.value.length > 0) { char[] var2 = this.value; for(int var3 = 0; var3 < this.value.length; ++var3) { var1 = 31 * var1 + var2[var3]; } this.hash = var1; } 复制代码
在 Java 中基于各种数据类型分析 == 和 equals 的区别 一节中讲到 String 类有重写自己的 equals 和 hashCode 方法,所以它俩需要一起讲述。
首先是 equals 方法,它比较的流程是:字符串对象相同(即自我比较);类型一致且长度相等时,比较字符内容是否相同。
然后是 hashCode 方法,如果 hash 值不等于 0,且 value.length 大于 0,则进行 hash 值计算。这里重点说下 var1 == 0
这一判定条件,var1 是一个 int 类型的值,默认值为 0,因此 0 可以表示可能未执行过 hash 计算,但不能表示一定未执行过 hash 计算,原因是我们现在还不确定 hash 计算后是否会产生 0 值;
执行 hash 计算后,会不会产生值为 0 的 hash呢?根据 hash 的计算逻辑,当 val2[0] = 0 时,根据公式 var1 = 31 * var1 + val2[i];
进行计算, var1 的值等于 0。但是经过查询 ASCII 表发现,null 的 ASCII 值为 0 。显然 val2[0]中永远不可能存放 null,因此 hash 计算后不会产生 0 值, var1== 0 可以作为是否进行过 hash 计算的判定条件。
最后得到计算公式为:
val2[0]*31^(n-1) + val2[1]*31^(n-2) + ... + val2[n-1] 复制代码
为什么要使用这个公式,就是在存储数据计算 hash 地址的时候,我们希望尽量减少有同样的 hash 地址。如果使用相同 hash 地址的数据过多,那么这些数据所组成的 hash 链就更长,从而降低了查询效率。
所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的 hash 地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
选择31作为因子的原因: 为什么 String hashCode 方法选择数字31作为乘子
有这样一道面试题“ 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
”答案:不对,两个对象的 hashCode()相同,equals()不一定 true。
String str1 = "通话"; String str2 = "重地"; System.out.println(String.format("str1:%d | str2:%d", str1.hashCode(),str2.hashCode())); System.out.println(str1.equals(str2)); //结果 str1:1179395 | str2:1179395 false 复制代码
很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。
boolean equals(Object anObject); 比较对象 boolean contentEquals(StringBuffer sb); 与StringBuffer对象比较内容 boolean contentEquals(CharSequence var1); 与字符比较内容 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) 可忽略大小写局部匹配 复制代码
contentEquals 方法
public boolean contentEquals(CharSequence var1) { if (var1 instanceof AbstractStringBuilder) { if (var1 instanceof StringBuffer) { synchronized(var1) { return this.nonSyncContentEquals((AbstractStringBuilder)var1); } } else { return this.nonSyncContentEquals((AbstractStringBuilder)var1); } } else if (var1 instanceof String) { return this.equals(var1); } else { char[] var2 = this.value; int var3 = var2.length; if (var3 != var1.length()) { return false; } else { for(int var4 = 0; var4 < var3; ++var4) { if (var2[var4] != var1.charAt(var4)) { return false; } } return true; } } } 复制代码
String 、StringBuilder、StringBuffer 都实现了 CharSequence 接口,所以上述方法可以接收这三种类型的参数。另外 StringBuilder、StringBuffer 继承了 AbstractStringBuilder 父类,所以它俩通过 nonSyncContentEquals 方法进行比较,注意 StringBuffer 需要考虑线程安全,加锁之后再调用。
compareTo 方法
public int compareTo(String var1) { int var2 = this.value.length; int var3 = var1.value.length; int var4 = Math.min(var2, var3); char[] var5 = this.value; char[] var6 = var1.value; for(int var7 = 0; var7 < var4; ++var7) { char var8 = var5[var7]; char var9 = var6[var7]; if (var8 != var9) { return var8 - var9; } } return var2 - var3; } 复制代码
通过下面这个例子进行展示:
String s1 = new String("abc"); String s2 = new String("abcdfg"); System.out.println(s2.compareTo(s1)); //3 System.out.println(s1.compareTo(s2)); //-3 s2 = new String("fghjkl"); System.out.println(s2.compareTo(s1)); //5 复制代码
当两个对象内容完全一致时,返回结果为 0;在字符串最小长度下,如果有不同的字符,系统则自动转换为 int 类型做差值。
boolean startsWith(String var1, int var2) 测试此字符串从指定索引开始的子字符串是否以指定前缀开始 boolean startsWith(String var1) 测试此字符串是否以指定的前缀开始。 boolean endsWith(String var1) 测试此字符串是否以指定的后缀结束。 复制代码
int indexOf(int var1) 返回指定字符(int 转 char)在此字符串中第一次出现处的索引。从0索引开始 int indexOf(int var1, int var2) 返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索。var2 小于字符串的长度 int indexOf(String var1) 返回指定子字符串在此字符串中第一次出现处的索引,从0索引开始。var1必须是此字符串的一个连续子集,否则返回-1 int indexOf(String var1, int var2) 返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始。 int lastIndexOf(int var1) 返回指定字符(int 转 char)在此字符串中最后一次出现处的索引。从0索引开始 int lastIndexOf(int var1, int var2) 返回在此字符串中最后一次出现指定字符处的索引,从指定的索引开始搜索。var2 小于字符串的长度 int lastIndexOf(String var1) 返回指定子字符串在此字符串中最后一次出现处的索引,从0索引开始。var1必须是此字符串的一个连续子集,否则返回-1 int lastIndexOf(String var1, int var2) 返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始。 复制代码
String substring(int var1) 返回一个新的字符串,它是此字符串的一个子字符串。从var1开始截取,截取长度为此字符串的长度减去var1 String substring(int var1, int var2) 返回一个新字符串,它是此字符串的一个子字符串。从var1开始截取,截取长度为var2。 CharSequence subSequence(int var1, int var2) 返回一个新的字符序列,它是此序列的一个子序列。 复制代码
String replace(char oldChar, char newChar) 返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 String replaceFirst(String regex, String replacement) 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 String replaceAll(String regex, String replacement) 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 String replace(CharSequence var1, CharSequence var2) 使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。 复制代码
public String replace(char var1, char var2) { if (var1 != var2) { int var3 = this.value.length; int var4 = -1; char[] var5 = this.value; do { ++var4; } while(var4 < var3 && var5[var4] != var1); if (var4 < var3) { char[] var6 = new char[var3]; for(int var7 = 0; var7 < var4; ++var7) { var6[var7] = var5[var7]; } while(var4 < var3) { char var8 = var5[var4]; var6[var4] = var8 == var1 ? var2 : var8; ++var4; } return new String(var6, true); } } return this; } 复制代码
replace 的参数可以是 char 或者 CharSequence,即可以支持字符的替换, 也支持字符串的替换。当参数为 char 时,是通过自己自定义的方法来更换字符;当参数为 CharSequence 时,实际调用的是 replaceAll 方法。replaceAll 和 replaceFirst 的参数是 regex,即基于规则表达式的替换。区别是一个全部替换,一个只替换第一个。
static String valueOf(Object var0) static String valueOf(char[] var0) static String valueOf(char[] var0, int var1, int var2) static String valueOf(boolean var0) static String valueOf(int var0) static String valueOf(long var0) static String valueOf(float var0) static String valueOf(double var0) 复制代码
valueOf 都是静态函数,不需要实例化 String 对象,直接调用用于将其他基本数据类型转换为 String 类型。
public native String intern(); 复制代码
intern 方法是 Native 调用,它的作用是在全局字符串常量池里寻找等值的对象的引用,如果没有找到则在常量池中存放当前字符串对象的引用并返回该引用,否则直接返回常量池中已存在的 String 对象引用。
intern 方法与常量池有着很大的联系,通过学习该方法的使用,便于我们了解内存分配的概念,所以该方法会在后续章节里详细讲解。想要了解的朋友可以先了解一下常量池的知识,前往 Java 中方法区与常量池 即可。