转载

Java核心技术36篇②——老生常谈 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
复制代码

字符串常量的创建过程:

char["Hello World!".length()]

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

而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。

原文  https://juejin.im/post/5c2759d3e51d45176760e22c
正文到此结束
Loading...