[关闭]
@pastqing 2015-09-28T09:34:40.000000Z 字数 4992 阅读 3318

JAVA String类源码解析(一)

java


PASS.1 不可改变的String ?

众所周知java中的String对象时不可改变的, 实际中我们经常做类似于String a = "abc" + new String(b) ; 的操作, 实际上都是创建了一个全新的String对象, 这个新的对象包含了修改后的字符串内容。 下面来看看String构造函数

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {
  3. private final char value[];
  4. private int hash; // Default to 0
  5. }

从上面的代码中,我们可以看出String的本质就是char[], 然而这个字符数组被final关键字修饰, 那么问题就来了, final是个么?

补充final关键字的几个意义:
1. 如果final修饰类的话, 那么这个类是不能被继承的。
2. final修饰方法的话, 那么这个方法是不能被重写。
3. final修饰变量的话, 那么这个变量在运行期间时不能被修改的。
这里应该重点理解第三点

  1. public static void main(String[] args) {
  2. final StringBuffer a = new StringBuffer("松哥");
  3. final StringBuffer b = new StringBuffer("songgeb");
  4. System.out.println("未改变: " + a);
  5. System.out.println("a的地址: " + a.hashCode());
  6. a = b; //这里编译就会报错了~~
  7. a.append("帅") ;
  8. System.out.println("改变: " + a);
  9. }
  10. /*
  11. 通过上面的例子, 我们对第三点的理解应该是这样:
  12. 1. 若变量是基本类型, 则它的值是不能变的
  13. 2. 若变量是对象, 则它的引用所指的堆栈区中的内存地址是不能变得, 而内容时可以变滴。
  14. */

以上也就解决了, 为啥String是不可改变的问题。 String里面的成员变量是私有的而且被final修饰, 因此对String本身是无法改变的, 只能重新new一个出来了。。。其实想改变还是有办法的, 只要想办法访问私有变量value就行, 至于怎么访问, 需要用的java中的反射技术了, 以后再谈

PASS.2 警惕toString()无意识的递归

这里我完全引用一下thinking in java 中的例子:

  1. /*
  2. java中的每个类从根本上都是继承自object, 因此容器类都有toString这个方法, 例如ArrayList, 调用它的toStirng方法会遍历其中包含的每一个对象, 调用每个对象的toString方法。 下面的例子是要打印每个对象的内存地址, 这时就会陷入无意识的递归。
  3. */
  4. public class demo_3 {
  5. public String toString() {
  6. return "address: " + this + "\n" ;
  7. }
  8. public static void main(String[] args) {
  9. // TODO Auto-generated method stub
  10. List<demo_3> a = new ArrayList<demo_3>();
  11. for( int i =0 ; i < 5; ++i){
  12. a.add(new demo_3());
  13. }
  14. System.out.println(a);
  15. }
  16. }

发生以上错误的原因就是在这句 “ + this ” 上, 编译器看到一个String对象后面跟着一个 + 再后面对象不是String, 于是他就会调用this的toString方法了, 然后就递归了。。。。怎么解决呢?是的用super.toString()

PASS.3 codePoints 与 codeUnit

  1. public class demo_4 {
  2. public static void main(String[] args) throws UnsupportedEncodingException {
  3. /*
  4. * \u1D56属于增补字符范围
  5. */
  6. String a = "\u1D56B";
  7. String b = "\uD875\uDD6B";
  8. System.out.println(a);
  9. System.out.println(a.length());
  10. System.out.println(b);
  11. System.out.println(b.length());
  12. //也就是说length返回的是代码单元的数量
  13. }
  14. }

下面是String其中的一个构造函数:

  1. public String(int[] codePoints, int offset, int count) {
  2. if (offset < 0) {
  3. throw new StringIndexOutOfBoundsException(offset);
  4. }
  5. if (count < 0) {
  6. throw new StringIndexOutOfBoundsException(count);
  7. }
  8. // Note: offset or count might be near -1>>>1.
  9. if (offset > codePoints.length - count) {
  10. throw new StringIndexOutOfBoundsException(offset + count);
  11. }
  12. final int end = offset + count;
  13. // Pass 1: 计算字符数组的大小, 以分配空间
  14. int n = count;
  15. for (int i = offset; i < end; i++) {
  16. int c = codePoints[i];
  17. if (Character.isBmpCodePoint(c))//这里是判断这个字符是不是属于bmp范围
  18. continue;
  19. else if (Character.isValidCodePoint(c))//判断是否越界
  20. n++;
  21. else throw new IllegalArgumentException(Integer.toString(c));
  22. }
  23. // Pass 2: 填充value
  24. final char[] v = new char[n];
  25. for (int i = offset, j = 0; i < end; i++, j++) {
  26. int c = codePoints[i];
  27. if (Character.isBmpCodePoint(c))
  28. v[j] = (char)c;
  29. else
  30. Character.toSurrogates(c, v, j++);
  31. }
  32. this.value = v;
  33. }
  34. //可以看出, 构造的思路很简单, 但是必须要考虑的很全面。 像以前写c++时, 要考虑一个类能不能被复制, 赋值, 等等情况。

PASS.4 equals还是==?

初学java者在比较字符串时, 常常容易混淆equals和==的不同。 下面我们讲一下两者的不同。首先来看个例子:

  1. public static void main(String[] args) {
  2. String x = new String("java"); //创建对象x,其值是java
  3. String y = new String("java"); //创建对象y,其值是java
  4. System.out.println(x == y); // false, 使用关系相等比较符比较对象x和y
  5. System.out.println(x.equals(y)); // true, 使用对象的equals()方法比较对象x和y
  6. String m = "java"; //创建对象m,其值是java
  7. String n = "java"; //创建对象n,其值是java
  8. System.out.println(m == n); // true, 使用关系相等比较符比较对象m和n
  9. System.out.println(m.equals(n)); // true, 使用关对象的equals()方法比较对象m和n
  10. }
  11. }

那么问题就来了, 相信大部分人第一个输出为flase能很明白, 但是第三个输出为true就有点晕了。 第三个输出为true是因为java常量池的原因, 类似于c++中的堆栈里的常量区。 我们先来看看equlas的源码, 再来解释常量池的东西。

  1. public boolean equals(Object anObject) {
  2. if (this == anObject) {
  3. return true;
  4. }
  5. if (anObject instanceof String) { // 判断是不是String的一个实例化
  6. String anotherString = (String)anObject;
  7. int n = value.length;
  8. if (n == anotherString.value.length) {
  9. char v1[] = value;
  10. char v2[] = anotherString.value;
  11. int i = 0;
  12. while (n-- != 0) {
  13. if (v1[i] != v2[i])
  14. return false;
  15. i++;
  16. }
  17. return true;
  18. }
  19. }
  20. return false;
  21. }

看源码的好处就是, 能看到优雅的代码, 清晰的思路, 全面的考虑。比如里面的while语句。 以前读c++的源码的时候, 就会发现里面每个函数小算法都实现的很精美, 往往几行搞定, 效率很高。总结来说java String 的实现就是对字符数组的各种操作, 这样就可以写自己的String类了。

下面解释jvm对String的处理, 这里我参考了小学徒的成长历程的博文

String a = "Hello World" ;

如上,字符串a是常量, 同时String是不可改变的, 因此我们可以共享这个常量。 为了提高效率, 节省资源就有了常量池这个东西。常量池的一个作用就是存放编译期间生产的各种字面值常量和引用。同时根据jvm的垃圾回收机制吧, 在这个常量区中的对象基本不会被回收的。看下面的例子:

  1. public static void main(String [] args ){
  2. String a = "Hello" ;
  3. String b = "Hello" + " World" ;
  4. String c = "Hello World" ;
  5. }

用javac 编译文件, 用javap -c 查看编译后的字节码, 如下:

  1. public static void main(java.lang.String[]);
  2. Code:
  3. 0: ldc #2 // String Hello
  4. 2: astore_1
  5. 3: ldc #3 // String Hello World
  6. 5: astore_2
  7. 6: ldc #3 // String Hello World
  8. 8: astore_3
  9. 9: return
  10. }

ldc指的是将常量值从常量池中拿出并且压入栈中, 可以看出第3 、6行取出的是同一个常量值, 这说明了,在编译期间,该字符串变量的值已经确定了下来,并且将该字符串值缓存在缓冲区中,同时让该变量指向该字符串值,后面如果有使用相同的字符串值,则继续指向同一个字符串值

不过, 一旦使用了变量或者调用了方法, 那就不一样了。 看下面的例子:

  1. public static void main(String [] args ){
  2. String a = "Hello" ;
  3. String b = new String("Hello");
  4. }

用javap -c 返回的自己码为:

  1. public static void main(java.lang.String[]);
  2. Code:
  3. 0: ldc #2 // String Hello
  4. 2: astore_1
  5. 3: new #3 // class java/lang/String
  6. 6: dup
  7. 7: ldc #2 // String Hello
  8. 9: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
  9. 12: astore_2
  10. 13: return
  11. }

从上面可以看出, 从常量区中拿出来放到栈中, 再从栈中拿出来, 然后调用String的一个构造函数, 通过关键字new进行创建对象, 然后将新的引用赋给b。 从这也能看出来用这种构造函数初始化一个字符串, 效率是不高的, 我们尽量少用。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注