JAVA String类源码解析(一)
PASS.1 不可改变的String ?
众所周知java中的String对象时不可改变的, 实际中我们经常做类似于String a = “abc” + new String(b) ; 的操作, 实际上都是创建了一个全新的String对象, 这个新的对象包含了修改后的字符串内容。 下面来看看String构造函数.
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash; // Default to 0
}
从上面的代码中,我们可以看出String的本质就是char[], 然而这个字符数组被final关键字修饰, 那么问题就来了, final是个么?
补充: final关键字的几个意义: 1. 如果final修饰类的话, 那么这个类是不能被继承的。 2. final修饰方法的话, 那么这个方法是不能被重写。 3. final修饰变量的话, 那么这个变量在运行期间时不能被修改的。 这里应该重点理解第三点
public static void main(String[] args) {
final StringBuffer a = new StringBuffer("松哥");
final StringBuffer b = new StringBuffer("songgeb");
System.out.println("未改变: " + a);
System.out.println("a的地址: " + a.hashCode());
a = b; //这里编译就会报错了~~
a.append("帅") ;
System.out.println("改变: " + a);
}
/*
通过上面的例子, 我们对第三点的理解应该是这样:
1. 若变量是基本类型, 则它的值是不能变的
2. 若变量是对象, 则它的引用所指的堆栈区中的内存地址是不能变得, 而内容时可以变滴。
*/
以上也就解决了, 为啥String是不可改变的问题。 String里面的成员变量是私有的而且被final修饰, 因此对String本身是无法改变的, 只能重新new一个出来了。。。其实想改变还是有办法的, 只要想办法访问私有变量value就行, 至于怎么访问, 需要用的java中的反射技术了, 以后再谈
PASS.2 警惕toString()无意识的递归
这里我完全引用一下thinking in java 中的例子:
/*
java中的每个类从根本上都是继承自object, 因此容器类都有toString这个方法, 例如ArrayList, 调用它的toStirng方法会遍历其中包含的每一个对象, 调用每个对象的toString方法。 下面的例子是要打印每个对象的内存地址, 这时就会陷入无意识的递归。
*/
public class demo_3 {
public String toString() {
return "address: " + this + "\n" ;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
List<demo_3> a = new ArrayList<demo_3>();
for( int i =0 ; i < 5; ++i){
a.add(new demo_3());
}
System.out.println(a);
}
}
发生以上错误的原因就是在这句 “ + this ” 上, 编译器看到一个String对象后面跟着一个 + 再后面对象不是String, 于是他就会调用this的toString方法了, 然后就递归了。。。。怎么解决呢?是的用super.toString()
PASS.3 codePoints 与 codeUnit
- codePoints是代码点, 表示的是例如’A’, ‘王’ 这种字符
- codeUnit是代码单元, 它根据编码不同而不同, 可以理解为是字符编码的基本单元, 比如一个char(8位1个字节)就是一个单元 我们知道, java中的char是两个字节, 也就是16位的。这样也反映了一个char只能表示从u+0000~u+FFFF范围的unicode字符, 在这个范围的字符也叫BMP(basic Multiligual Plane ), 超出这个范围的叫增补字符。 看下面这个例子:
public class demo_4 {
public static void main(String[] args) throws UnsupportedEncodingException {
/*
* \u1D56属于增补字符范围
*/
String a = "\u1D56B";
String b = "\uD875\uDD6B";
System.out.println(a);
System.out.println(a.length());
System.out.println(b);
System.out.println(b.length());
//也就是说length返回的是代码单元的数量
}
}
下面是String其中的一个构造函数:
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
final int end = offset + count;
// Pass 1: 计算字符数组的大小, 以分配空间
int n = count;
for (int i = offset; i < end; i++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))//这里是判断这个字符是不是属于bmp范围
continue;
else if (Character.isValidCodePoint(c))//判断是否越界
n++;
else throw new IllegalArgumentException(Integer.toString(c));
}
// Pass 2: 填充value
final char[] v = new char[n];
for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
v[j] = (char)c;
else
Character.toSurrogates(c, v, j++);
}
this.value = v;
}
//可以看出, 构造的思路很简单, 但是必须要考虑的很全面。 像以前写c++时, 要考虑一个类能不能被复制, 赋值, 等等情况。
PASS.4 equals还是==?
初学java者在比较字符串时, 常常容易混淆equals和==的不同。 下面我们讲一下两者的不同。首先来看个例子:
public static void main(String[] args) {
String x = new String("java"); //创建对象x,其值是java
String y = new String("java"); //创建对象y,其值是java
System.out.println(x == y); // false, 使用关系相等比较符比较对象x和y
System.out.println(x.equals(y)); // true, 使用对象的equals()方法比较对象x和y
String m = "java"; //创建对象m,其值是java
String n = "java"; //创建对象n,其值是java
System.out.println(m == n); // true, 使用关系相等比较符比较对象m和n
System.out.println(m.equals(n)); // true, 使用关对象的equals()方法比较对象m和n
}
}
那么问题就来了, 相信大部分人第一个输出为flase能很明白, 但是第三个输出为true就有点晕了。 第三个输出为true是因为java常量池的原因, 类似于c++中的堆栈里的常量区。 我们先来看看equlas的源码, 再来解释常量池的东西。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) { // 判断是不是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;
}
看源码的好处就是, 能看到优雅的代码, 清晰的思路, 全面的考虑。比如里面的while语句。 以前读c++的源码的时候, 就会发现里面每个函数小算法都实现的很精美, 往往几行搞定, 效率很高。总结来说java String 的实现就是对字符数组的各种操作, 这样就可以写自己的String类了。
下面解释jvm对String的处理, 这里我参考了小学徒的成长历程的博文
String a = “Hello World” ;
如上,字符串a是常量, 同时String是不可改变的, 因此我们可以共享这个常量。 为了提高效率, 节省资源就有了常量池这个东西。常量池的一个作用就是存放编译期间生产的各种字面值常量和引用。同时根据jvm的垃圾回收机制吧, 在这个常量区中的对象基本不会被回收的。看下面的例子:
public static void main(String [] args ){
String a = "Hello" ;
String b = "Hello" + " World" ;
String c = "Hello World" ;
}
用javac 编译文件, 用javap -c 查看编译后的字节码, 如下:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Hello
2: astore_1
3: ldc #3 // String Hello World
5: astore_2
6: ldc #3 // String Hello World
8: astore_3
9: return
}
ldc指的是将常量值从常量池中拿出并且压入栈中, 可以看出第3 、6行取出的是同一个常量值, 这说明了,在编译期间,该字符串变量的值已经确定了下来,并且将该字符串值缓存在缓冲区中,同时让该变量指向该字符串值,后面如果有使用相同的字符串值,则继续指向同一个字符串值
不过, 一旦使用了变量或者调用了方法, 那就不一样了。 看下面的例子:
public static void main(String [] args ){
String a = "Hello" ;
String b = new String("Hello");
}
用javap -c 返回的自己码为:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Hello
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #2 // String Hello
9: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: return
}