手记

彻底搞定String、StringBuilder、StringBuffer【大宝的编程手记】

[TOC]

字符串就是一连串的字符序列,Java提供了String、StringBuilder、StringBuffer三个类来封装字符串

String

String类是不可变类,String对象被创建以后,对象中的字符序列是不可改变的,直到这个对象被销毁

为什么是不可变的

jdk1.8
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //jdk1.9中将char数组替换为byte数组,紧凑字符串带来的优势:更小的内存占用,更快的操作速度。
    //构造函数
     public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    //构造函数
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    //返回一个新的char[]
    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;
    }
 }

根据上面的代码,我们看看String究竟是怎么保证不可变的。

  • String类被final修饰,不可被继承
  • string内部所有成员都设置为私有变量,外部无法访问
  • 没有向外暴露修改value的接口
  • value被final修饰,所以变量的引用不可变。
  • char[]·为引用类型仍可以通过引用修改实例对象,为此String(char value[])构造函数内部使用的copyOf而不是直接将value[]复制给内部变量`。
  • 在获取value时,并没有将value的引用直接返回,而是采用了arraycopy()的方式返回一个新的char[]
  • String类中的函数也处处透露着不可变的味道,比如:replace()
public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                //重新创建新的char[],不改变原有对象中的值
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                //最后返回新创建的String对象
                return new String(buf, true);
            }
        }
        return this;
    }

当然不可变也不是绝对的,还是可以通过反射获取到变value引用,然后通过value[]修改数组的方式改变value对象实例

        String a = "Hello World!";
        String b = new String("Hello World!");
        String c = "Hello World!";

       //通过反射修改字符串引用的value数组
        Field field = a.getClass().getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(a);
        System.out.println(value);//Hello World!
        value[5] = '&';
        System.out.println(value);//Hello&World!

        // 验证b、c是否被改变
        System.out.println(b);//Hello&World! 
        System.out.println(c);//Hello&World!

写到这里该如何引出不可变的好处呢?忘记反射吧,我们聊聊不可变的好处吧

不可变的优点

保证了线程安全

同一个字符串实例可以被多个线程共享。

保证了基本的信息安全

比如,网络通信的IP地址,类加载器会根据一个类的完全限定名来读取此类诸如此类,不可变性提供了安全性。

字符串缓存(常量池)的需要

具统计,常见应用使用的字符串中有大约一半是重复的,为了避免创建重复字符串,降低内存消耗和对象创建时的开销。JVM提供了字符串缓存的功能——字符串常量池。如果字符串是可变的,我们就可以通过引用改变常量池总的同一个内存空间的值,其他指向此空间的引用也会发生改变。

支持hash映射和缓存。

因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

不可变的缺点

由于它的不可变性,像字符串拼接、裁剪等普遍性的操作,往往对应用性能有明显影响。

为了解决这个问题,java为我们提供了两种解决方案

  • 字符串常量池
  • StringBuilder、StringBuffer是可变的

字符串常量池

还是刚才反射的示例

        String a = "Hello World!";
        String b = new String("Hello World!");
        String c = "Hello World!";
        //判断字符串变量是否指向同一块内存
        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(b == c);

        // 通过反射观察a, b, c 三者中变量value数组的真实位置
        Field a_field = a.getClass().getDeclaredField("value");
        a_field.setAccessible(true);
        System.out.println(a_field.get(a));

        Field b_field = b.getClass().getDeclaredField("value");
        b_field.setAccessible(true);
        System.out.println(b_field.get(b));

        Field c_field = c.getClass().getDeclaredField("value");
        c_field.setAccessible(true);
        System.out.println(c_field.get(c));
        //通过反射发现String对象中变量value指向了同一块内存

输出

false
true
false
[C@6f94fa3e
[C@6f94fa3e
[C@6f94fa3e

字符串常量的创建过程:

  1. 判断常量池中是否存在"Hello World!"常量,如果有直接返回该常量在池中的引用地址
  2. 如果没有,先创建一个char["Hello World!".length()]数组对象,然后在常量池中创建一个字符串对象并用数组对象初始化字符串对象的成员变量value,然后将这个字符串的引用返回,比如赋值给a

由此可见,a和c对象指向常量池中相同的内存空间不言自明。

而b对象的创建是建立在以上的创建过程的基础之上的。
"Hello World!"常量创建完成时返回的引用,会经过String的构造函数。

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

构造函数内部将引用的对象成员变量value赋值给了内部成员变量value,然后将新创建的字符创对象引用赋值给了b,这个过程发生在堆中。

再来感受下下面这两行代码有什么区别

  String b = new String(a);
  String b = new String("Hello World!");

StringBuilder和StringBuffer

二者都是可变的

为了弥补String的缺陷,Java先后提供了StringBuffer和StringBuilder可变字符串类。

二者都继承至AbstractStringBuilder,AbstractStringBuilder使用了char[] value字符数组

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
}

可以看出AbstractStringBuilder类和其成员变量value都没有使用final关键字。

value数组的默认长度

StringBuilder和StringBuffer的value数组默认初始长度是16

    public StringBuilder() {
        super(16);
    }
    public StringBuffer() {
        super(16);
    }

如果我们拼接的字符串长度大概是可以预计的,那么最好指定合适的capacity,避免多次扩容的开销。

扩容产生多重开销:抛弃原有数组,创建新的数组,进行arrycopy。

二者的区别

StringBuilder是非线程安全的,StringBuffer是线程安全的。

StringBuffer类中的方法使用了synchronized同步锁来保证线程安全。
关于锁的话题非常大,会单独成文来说明,这里推荐一篇不错的博客,有兴趣的可以看看

0人推荐
随时随地看视频
慕课网APP