一、String 简介
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
... ...
}
基本属性:
boolean isEmpty(); int length(); int hashCode();
常用操作:
1. 查找
char charAt(); int codePointAt(); int codePointBefore(); int codePointCount();
int indexOf(); int lastIndexOf();
boolean contains();
boolean endsWith(); boolean startsWith();2. 比较
int compartTo(); int compareToIgnoreCase();
boolean equals(); boolean equalsIgnoreCase();
boolean contentEquals();
boolean matches(); boolean regionMatches();3. 拼接/分割
String concat(); static String join();
String[] split();4. 替换
String replace(); String replaceAll(); String replaceFirst();5. 求子串
String substring(); CharSequence subSequence();6. 转换/格式化
static String valueOf();
static String format();
String trim();
String toLowerCase(); String toUpperCase();
char[] toCharArray(); void getChars(); byte[] getBytes();
String toString();
其他:
static String copyValueOf(); int offsetByCodePoints(); String intern();
二、源码分析
2.1 关于 String hashCode() 方法
除了 String 在值一旦被初始化就不能被改变外,其 hash 值也是不改变的。从 hashCode() 方法中可以看出:当且仅当 h == 0 && value.length > 0
时,才计算 hash 值。也就意味着 hash 值计算后不再重复计算。
public int hashCode() {
int h = hash;
/** 当且仅当 h 没有计算过,并且 value 有值的情况下计算 h 的值 */
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;
}
hash的计算是使用数学公式:hash = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
。hashCode 可以保证相同的字符串的 hash 值肯定相同,但是,hash 值相同 value 值不一定相同。
2.2 关于 String 的构造方法
- 构造一个空的 String
public String() {
this.value = "".value;
}
- 使用 char[]、String 构造
使用一个 String 初始化另一个 String 时,直接将源 String 中的属性(value 数组和 hash 值)赋值给目标 String。因为 String 是不可变的,所以也就不用担心改变源 String 的值会影响到目标 String 的值。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
String 就是使用字符数组(char[])实现的,所以可以使用一个字符数组来创建一个 String,值得注意的是,当我们使用字符数组创建String的时候,会用到Arrays.copyOf
方法和Arrays.copyOfRange
方法。这两个方法是将原有的字符数组中的内容逐一的复制到String中的字符数组中。
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
public String(char value[], int offset, int count) {
/** check boundary */
// 如果符合条件,并且 count == 0 的时候,this.value = "".value
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
- 使用 byte[] 构造
在 Java 中,String 实例中保存有一个字符数组(char[]),字符数组(char[])是以 Unicode 码来存储的。String 和 char 为内存形式,byte 是网络传输或存储的序列化形式。所以在很多传输和存储的过程中需要将byte[] 数组和 String 进行相互转化。所以,String 提供了一系列重载的构造方法来将 byte[] 转化成 String。
提到 byte[] 和 String 之间的相互转换就不得不关注编码问题。在生成新的 String 的时候,需要提供字符集格式,来将 byte[] 解码成 Unicode 的 char[]。
public String(byte bytes[], int offset, int length) {
/** check boundary */
this.value = StringCoding.decode(bytes, offset, length);
}
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
/** check boundary */
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
public String(byte bytes[], int offset, int length, Charset charset) {
/** check boundary */
this.value = StringCoding.decode(charset, bytes, offset, length);
}
// 以下方法是对上述方法的再次包装
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
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);
}
可以看到,在 String 的众多构造器中,使用了 StringCoding.decode() 方法来对 byte[] 进行解码,使用的解码的字符集就是我们指定的 charsetName 或者 charset。如果没有指明解码使用的字符集的话,那么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;
}
}
- 使用 StringBuffer 和 StringBuider 构造
作为 String 的两个“兄弟”,StringBuffer 和 StringBuider 当然也可以被当做构造 String 的参数。当然,这两个构造方法是很少用到的,因为当有了 StringBuffer 和 StringBuider 对象后,可以调用其 toString() 来获取 String 对象。
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
- 使用一个特殊的 String(char[] value, boolean share) 构造
String 除了提供了很多公有的供程序员使用的构造方法以外,还提供了一个保护类型的构造方法。
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(char[] value, boolean share)
直接将 value 的引用赋值给 String 的 value。也就是说,这个方法构造出来的String和参数传过来的char[] value
共享同一个数组。
为什么Java会提供这样一个方法呢? 首先,我们分析一下使用该构造函数的好处:
首先,性能好,这个很简单,一个是直接给数组赋值(相当于直接将String的value的指针指向char[]数组),一个是逐一拷贝。当然是直接赋值快了。
其次,共享内部数组节约内存。
但是,该方法之所以设置为 protected,是因为一旦该方法设置为公有,在外面可以访问的话,那就破坏了字符串的不可变性。假设以下情景:
// 假设当 String(char[] value, boolean share) 变为 public 后
char[] arr = new char[] {'h', 'e', 'l', 'l', '0'};
String str = new String(arr, true);
arr[0] = 'a';
System.out.print(str); // Output: aello
如果构造方法没有对 arr 进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对arr的修改就相当于修改了字符串。所以,从安全性角度考虑,这样做是安全的。对于调用他的方法来说,由于无论是原字符串还是新字符串,其 value 数组本身都是 String 对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。
在 Java 8 中有很多方法使用这种“共享、节约内存,性能好,安全”的做法,如:String 的 replace()、valueOf(),Integer、Long 和 StringBuffer 的 toString() 等。
2.3关于 String 的长度
- 从 String 源码的角度来看
String 类中有很多重载的构造函数,其中有几个是支持用户传入 length 来执行长度的,而 length 是使用 int 类型定义的,也就是说,String定义的时候,最大支持的长度就是int的最大范围值。 - 从编译期的角度来看
在编译期,定义字符串的时候也是有长度限制的。如:
String s = "11111...1111"; //其中有10万个字符"1"
当使用 javac 编译时,会抛出”常量字符串过长“的错误。
现在问题来了,明明 String 的构造函数指定的长度是可以支持 int 大小的,为什么像以上形式定义的时候无法编译呢?
其实,形如 String s = "abc";
定义 String 的时候,abc 被我们称之为字面量,这种字面量在编译之后会以常量的形式进入到 Class 常量池。因为要进入常量池,就要遵守常量池的有关规定。
- 常量池限制
Class 文件中常量池的格式规定了:当参数类型为 String,并且长度大于等于 65535 的时候,就会导致编译失败。 - 运行期限制
String在运行期长度超过 Integer.MAX_VALUE,就可能会抛出异常。(约等于4G)(在 JDK 1.9 之前)
2.4 关于 String intern() 方法
public native String intern();
可以看到,intern() 是 native 方法,其作用是“将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,无论放没放入,都会把串池中的对象返回”
/** 字符串常量池 */
String s1 = "a";
String s2 = "b";
String s3 = "ab"; // equals: String s3 = "a" + "b";
String s4 = s1 + s2; // equals: new StringBUilder().append(s1).append(s2); as well new String("ab");
System.out.println(s3 == s4); // false
String s = s4.intern();
System.out.println(s3 == s4); // false
System.out.println(s == s3); // true
System.out.println(s == s4); // false
/** 补充一点 */
String s5 = s1 + s2;
System.out.println(s5 == s4); // false
从上述代码中看出,s4 调用 intern() 后,将本身拥有的放入常量池(如果常量池中没有的话)并返回常量池的对象,而其 s4 本身并不会改变。
2.5 关于 substring() 和 CharSequence subSequence()
public String substring(int beginIndex) {
/** check boundary */
int subLen = value.length - beginIndex;
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String substring(int beginIndex, int endIndex) {
/** check boundary */
int subLen = endIndex - beginIndex;
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public CharSequence subSequence(int beginIndex, int endIndex) {
return this.substring(beginIndex, endIndex);
}
public String(char value[], int offset, int count) {
/** check boundary */
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
由上述代码看出:在 substring 中,使用 new String() 创建了一个新的字符串。
如果 JDK 版本小于 1.7,当使用 String substring() 方法时一定要注意,因为可能产生内存泄露,如下:
/** JDK 6 */
public String substring(int beginIndex, int endIndex) {
//check boundary
return new String(offset + beginIndex, endIndex - beginIndex, value);
}
/** JDK 6 */
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
从上面的代码看出:如果你有一个很长很长的字符串,但是当你使用substring进行切割的时候你只需要很短的一段。这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。
在 JDK 6 中,一般用 str = str.substring(x, y) + ""
来解决该问题,原理其实就是生成一个新的字符串并引用他。
2.6 关于 static String valueOf()
/** 避免了 NPE 的发生 */
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
/** 对于基本类型 boolean 也做出了相应的处理 */
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
/** int、long、float、double 的实现都是使用了 其对应包装类的 toString() */
public static String valueOf(int i) {
return Integer.toString(i);
}
/** copyValueOf() 等价于 valueOf() */
public static String valueOf(char data[]) {
return new String(data);
}
public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[]) {
return new String(data);
}
public static String copyValueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
可以看出:在使用 valueOf() 时,对于 Object、基本类型(包括 boolean)都进行了相应的处理。
2.7 关于 byte[] getBytes()
在创建 String 的时候,可以使用 byte[] 数组,将一个字节数组转换成字符串。同样,我们可以将一个字符串转换成字节数组,String 提供了很多重载的 getBytes() 方法。
public byte[] getBytes() {
return StringCoding.encode(value, 0, value.length);
}
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);
}
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, value, 0, value.length);
}
值得注意的是,在使用这些方法的时候一定要注意编码问题。比如:
String str = "你好!";
byte[] bytes = str.getBytes();
这段代码在不同的平台上运行得到结果是不一样的。由于我们没有指定编码方式,所以在该方法对字符串进行编码的时候就会使用系统的默认编码方式。比如:在中文操作系统中可能会使用GBK或者GB2312进行编码,在英文操作系统中有可能使用iso-8859-1进行编码。这样写出来的代码就和机器环境有很强的关联性,可能导致乱码。所以,为了避免不必要的麻烦,我们要指定编码方式。
2.8 关于比较方法
String 提供了许多比较方法来比较两个字符串的关系。
- 对于 xxxIgnoreCase() 忽略大小写比较来说,是先转换为大写或小写(Character.toUpperCase() 或 Character.toLowerCase())后再进行比较。
- 对于 equals() 的比较方法有一个很棒的编程技巧。
public boolean equals(Object anObject) {
// 首先判断要比较的对象和当前对象是不是同一个对象
if (this == anObject) {
return true;
}
// 再判断 anObject 是不是 String 类型的
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;
}
虽然代码写的内容比较多,但是可以很大程度上提高比较的效率。
- 对于 contentEquals() 的两个重载方法
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}
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;
}
因为 StringBuffer 需要考虑线程安全问题,再加锁之后调用 nonSyncContentEquals((AbstractStringBuilder)cs)
方法。对于具体的比较而言,依旧是先“宏观”比较,再“微观”比较。
2.9 关于 boolean isEmpty() 和 int length()
public boolean isEmpty() {
return value.length == 0;
}
public int length() {
return value.length;
}
二者并无方法上的相互调用,不过都是使用 value.length 来获取信息。
三、套路
3.1 String 与 byte[] 转换
3.2 String 与 数值类型转换
3.3 String 三兄弟转换
参考资料
- HollisChuang’s Blog:https://www.hollischuang.com/archives/99