基于 JDK 1.8.0_151
public final class String implements
java.io.Serializable,
Comparable<String>,
CharSequence {}
在Java中,被 final 修饰符的修饰的类不允许被其他类继承。
所以 String 是不能被我们继承的。
属性
// 该值用于字符存储
private final char value[];
// 缓存字符串的哈希码
private int hash; // Default to 0
// 序列化 ID
private static final long serialVersionUID = -6849794470754667710L;
// serialPersistentFields 用于指定哪些字段需要被默认序列化
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
String 字符串是常量,其值在实例创建后就不能被修改。可以看到其内部实现为 final 修饰的字符数组。
构造方法
String 类一共有 16 个构造方法,其中 2 个标识为@Deprecated
,即不再建议使用;1 个包访问权限的构造方法。因为 String 是不可变的,所以构造方法如此之多就不足为奇了。
// 创建空的字符序列
public String() {
this.value = "".value;
}
// 使用字符串类型的对象来初始化
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
因为 String 一旦创建之后就是不可以变的,所以以上两个构造方法就有点鸡肋了。
字符数组构造方法
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 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);
}
当我们使用字符数组创建 String 的时候,会用到 Arrays.copyOf 方法或 Arrays.copyOfRange 方法。这两个方法是将原有的字符数组中的内容逐一的复制到 String 中的字符数组中。会创建一个新的字符串对象,所以随后对于该字符数组的修改不会影响新创建的字符串。
字节数组构造方法
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);
}
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, 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);
}
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
这些构造方法是指通过 charset 来解码指定的 byte 数组,将其解码成 Unicode(UTF-16) 的 char[] 数组,构造成新的 String。
这里的 bytes 字节流是使用 charset 进行编码的,想要将他转换成 Unicode 的 char[] 数组,而又保证不出现乱码,那就要指定其解码方式。
如果没有指定编码方式,就使用系统的默认编码格式Charset.defaultCharset().name()
。
代码点数组构造方法
包访问权限的构造方法
/*
* 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) {
// assert share : "unshared not supported";
this.value = value;
}
share
参数仅仅作为一个标识,为了与重载方法String(char[] value)
区分。- 直接将参数 value 的引用赋值给 String 的 value。那么这个方法构造出来的 String 和参数传过来的 char[] value 共享同一个数组。虽然有利于提高性能,但是字符串不可变的特性也被打破了。
StringBuffer & StringBuilder 构造方法
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());
}
提供此构造方法是为了简化到 StringBuilder 和 StringBuffer 的迁移。通过 toString 方法从字符串生成器中获取字符串可能运行的更快,因此通常作为首选。
获得字符数组
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
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 char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
以上三个方法都是调用了 System.java 里的 native 方法:
public static native void arraycopy
(Object src, int srcPos, Object dest, int destPos, int length);
获得字节数组
public byte[] getBytes() { return StringCoding.encode(value, 0, value.length); }
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, 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);
}
与 new String
相反,getBytes
为编码操作。同样需要注意编码方式。为了避免不必要的麻烦,要指定编码方式。
hashCode
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;
}
可以看到为了避免频繁计算哈希的开销,此处使用了 String 类的hash 属性作为缓存。
在 Java 中,整型数是 32 位的,大小范围为Integer.MIN_VALUE<-->Integer.MAX_VALUE
(-2147483648<–>2147483647)
所以一定会有碰撞产生,即不同的字符串对应相同的哈希值。
hashCode 可以保证相同的字符串的 hash 值肯定相同,但是 hash 值相同并不一定是 value 值就相同。
intern
String.intern()
的作用:
如果常量池中存在当前字符串的引用, 就会直接返回当前字符串的引用。如果常量池中没有此字符串, 则在堆中创建该字符串实例并将此字符串的引用放入常量池中后, 再返回。
http://blog.csdn.net/x_iya/article/details/79156459
valueOf
public static String valueOf(Object obj) {return (obj == null) ? "null" : obj.toString();}
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 valueOf(boolean b) {return b ? "true" : "false";}
public static String valueOf(char c) {char data[] = {c};return new String(data, true);}
public static String valueOf(int i) {return Integer.toString(i);}
public static String valueOf(long l) {return Long.toString(l);}
public static String valueOf(float f) {return Float.toString(f);}
public static String valueOf(double d) {return Double.toString(d);}
valueOf是一系列的重载方法,完成相应类型的元素到String的转换。
题外篇
运算符重载
Java中的仅有的两个”运算符重载”都在 String 中:+
和 +=
。当然Java中并没有”运算符重载”的概念,所有相应的转换都是通过语法糖实现的。
public class TestString {
public static void main(String[] args) {
String string1 = "Hello";
String string2 = string1 + " World";
}
}
我们反编译下查看它是如何实现的
javac TestString.java
javap -c TestString
Compiled from "TestString.java"
public class TestString {
public TestString();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String World
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
}
string2 则对应于
new StringBuilder().append("Hello").append(" World").toString();
常量折叠
见博文:http://blog.csdn.net/x_iya/article/details/79051208
switch 对字符串的支持
public class TestString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default: break;
}
}
}
接着反编译
Compiled from "TestString.java"
public class TestString {
public TestString();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String world
2: astore_1
3: aload_1
4: astore_2
5: iconst_m1
6: istore_3
7: aload_2
8: invokevirtual #3 // Method java/lang/String.hashCode:()I
11: lookupswitch { // 2
99162322: 36
113318802: 50
default: 61
}
36: aload_2
37: ldc #4 // String hello
39: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
42: ifeq 61
45: iconst_0
46: istore_3
47: goto 61
50: aload_2
51: ldc #2 // String world
53: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ifeq 61
59: iconst_1
60: istore_3
61: iload_3
62: lookupswitch { // 2
0: 88
1: 99
default: 110
}
88: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
91: ldc #4 // String hello
93: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
96: goto 110
99: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
102: ldc #2 // String world
104: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
107: goto 110
110: return
}
lookupswitch 为 switch 对应的字节码。
字符串”hello”对应的哈希码为99162322,”world”对应的哈希码为113318802。
所对应的代码大致为:
public static void main(String args[]) {
String str = "world";
String s;
switch((s = str).hashCode()) {
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
default: break;
}
}
所以字符串的 switch 是通过 equals() 和 hashCode() 方法来实现的(因为有可能发生哈希碰撞,所以使用 equals 方法进行检查)。
其实 swich 只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后在使用 switch 的。
总结
- 在 Java 中,字符串实例是不可以改变的(内容),一旦创建出来就不能再去修改。有所形似对字符串对象作出的改变都是通过创建一个新的对象来完成。所以会把大量的时间放在垃圾回收上。
- 如果你需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder。(底层也是通过可变长的字符数组实现)
- 使用 new 的方式创建字符串对象比直接使用
""
一般多一个String 实例的构建。 - Java中 String 作为一种对象也是在堆中分配的内存,即使是intern(JDK7+)。
参考:
http://www.hollischuang.com/archives/99#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99