一文讲透:String 为什么不可变?StringBuffer 与 StringBuilder 的区别
一文讲透String 为什么不可变StringBuffer 与 StringBuilder 的区别图解字符串是开发和校招笔试中的常客字符串转整形数字、字符串相加……面试中也频繁被问到String、StringBuffer 和 StringBuilder 之间的区别。本文用图解 源码的方式讲清楚三个问题String 为什么不可修改哪两种类型是可以修改的这两种可修改的类型有什么区别目录一、String 为什么不可变1.1 从源码看final 类 private final value 数组1.2 「不可变」的真正含义所有修改都是创建新对象1.3 误区澄清不是因为 final 所以内容不能改1.4 为什么要设计成不可变不可变对象的好处二、想修改字符串怎么办两种可修改的类型2.1 直接用 String 修改效率非常低2.2 StringBuilder内容可修改的字符串2.3 String 与 StringBuilder 的相互转换三、StringBuffer 与 StringBuilder 的区别面试题四、总结一、String 为什么不可变1.1 从源码看final 类 private final value 数组String 是一种不可变对象字符串中的内容是不可改变的。这是因为String 类在设计时就是不可改变的。来看 JDK 1.8 中 String 类的部分实现——String 类中的字符实际保存在内部维护的value 字符数组中publicfinalclassStringimplementsjava.io.Serializable,ComparableString,CharSequence{/** * The value is used for character storage. */privatefinalchar[]value;// 字符真正存放的地方// ...提供大量读取 value 的方法但没有任何修改 value 内容的方法}从源码中可以读出三层设计意图String 类被 final 修饰表明该类不能被继承防止子类重写行为破坏不可变性value 被 private final 修饰private外部访问不到valuefinalvalue 自身的值不能改变即不能修改引用的指向但是指向中的内容是可以修改的没有任何修改入口String 类内部没有提供任何修改 value 内容的方法所有 APIreplace、substring、concat……都只会返回新对象。1.2 「不可变」的真正含义所有修改都是创建新对象所有涉及到可能修改字符串内容的操作都是创建一个新对象、改变的是新对象而原对象丝毫不动。以最熟悉的s world为例publicstaticvoidmain(String[]args){Stringshello;s world;// 并没有在原字符串上追加而是产生了新对象System.out.println(s);// 输出hello world}所以像下面这样的替换操作Stringstrhelloworld;System.out.println(str.replaceAll(l,_));// he__owor_dSystem.out.println(str.replaceFirst(l,_));// he_loworldSystem.out.println(str);// helloworld原字符串纹丝不动注意事项由于字符串是不可变对象替换不会修改当前字符串而是产生一个新的字符串。必须用变量接收返回值否则修改毫无意义。1.3 误区澄清不是因为 final 所以内容不能改网上有人说字符串不可变是因为其内部保存字符的数组被 final 修饰了因此不能改变——这种说法是错误的。final修饰引用类型时仅表明该引用变量不能引用其他对象指向不能变但其引用对象中的内容依然可以修改publicstaticvoidmain(String[]args){finalint[]array{1,2,3,4,5};array[0]100;// 编译通过内容可以修改System.out.println(Arrays.toString(array));// [100, 2, 3, 4, 5]// array new int[]{4, 5, 6}; // 编译报错无法为最终变量 array 分配值}一句话总结final 修饰类表明该类不想被继承final 修饰引用类型表明该引用不能引用其他对象但引用对象中的内容是可以修改的。字符串真正不可变是因为类不能被继承 value 拿不到也换不掉 类中没有任何修改内容的方法所有 API 都返回新对象。1.4 为什么要设计成不可变不可变对象的好处既然修改这么麻烦为什么 String 要设计成不可变的呢方便实现字符串对象池字符串常量池如果 String 可变对象池就需要考虑写时拷贝的问题了——共享同一个对象时一个引用的修改会波及所有使用者不可变对象是线程安全的多线程可以放心共享无需加锁不可变更方便缓存 hash code作为 key 时可以更高效地保存到 HashMap 中。二、想修改字符串怎么办两种可修改的类型2.1 直接用 String 修改效率非常低注意尽量避免直接对 String 类型对象进行修改因为 String 类是不能修改的所有的修改都会创建新对象效率非常低下。publicstaticvoidmain(String[]args){longstartSystem.currentTimeMillis();Strings;for(inti0;i10000;i){si;// 每次循环都创建新对象}longendSystem.currentTimeMillis();System.out.println(end-start);// 耗时很长startSystem.currentTimeMillis();StringBuffersbfnewStringBuffer();for(inti0;i10000;i){sbf.append(i);// 原地修改无新对象}endSystem.currentTimeMillis();System.out.println(end-start);startSystem.currentTimeMillis();StringBuildersbdnewStringBuilder();for(inti0;i10000;i){sbd.append(i);// 原地修改无新对象}endSystem.currentTimeMillis();System.out.println(end-start);}可以看到在对 String 类进行修改时效率是非常慢的循环拼接 10000 次会产生约 10000 个临时对象。因此尽量避免对 String 的直接修改如果要修改建议尽量使用 StringBuffer 或者 StringBuilder。2.2 StringBuilder内容可修改的字符串由于 String 的不可更改特性为了方便字符串的修改Java 中又提供了StringBuilder 和 StringBuffer两个类——这就是内容可以修改的两种类型。这两个类大部分功能是相同的以 StringBuilder 为例注意不能直接赋值字符串只能通过构造方法传入publicstaticvoidmain(String[]args){StringBuildersb1newStringBuilder(hello);// 不能写 sb1 helloStringBuildersb2sb1;// 追加即尾插 -- 字符、字符串、整形数字sb1.append( );// hellosb1.append(world);// hello worldsb1.append(123);// hello world123System.out.println(sb1);// hello world123System.out.println(sb1sb2);// trueappend 是原地修改对象没变System.out.println(sb1.charAt(0));// 获取 0 号位上的字符 hSystem.out.println(sb1.length());// 获取字符串的有效长度 14System.out.println(sb1.capacity());// 获取底层数组的总大小sb1.setCharAt(0,H);// 设置任意位置的字符 - Hello world123sb1.insert(0,Hello world!!!);// 指定位置插入 - Hello world!!!Hello world123System.out.println(sb1);System.out.println(sb1.indexOf(Hello));// 获取 Hello 第一次出现的位置System.out.println(sb1.lastIndexOf(hello));// 获取 hello 最后一次出现的位置sb1.deleteCharAt(0);// 删除首字符sb1.delete(0,5);// 删除 [0, 5) 范围内的字符Stringstrsb1.substring(0,5);// 截取 [0, 5) 区间中的字符以 String 方式返回System.out.println(str);sb1.reverse();// 字符串逆转strsb1.toString();// 将 StringBuilder 以 String 方式返回System.out.println(str);}StringBuilder 常用方法一览作用 返回值方法作用返回值StringBuilder append(String str)在尾部追加相当于 String 的可追加 boolean、char、char[]、double、float、int、long、Object、String 等返回自身支持链式调用StringBuilder insert(int offset, String str)在 offset 位置插入八种基本类型 String Object 数据返回自身StringBuilder deleteCharAt(int index)删除 index 位置的字符返回自身StringBuilder delete(int start, int end)删除[start, end)区间内的字符返回自身StringBuilder replace(int start, int end, String str)将[start, end)位置的字符替换为 str返回自身StringBuilder reverse()反转字符串返回自身char charAt(int index)获取 index 位置的字符返回该字符charvoid setCharAt(int index, char ch)将 index 位置的字符设置为 ch无返回值int indexOf(String str)返回 str 第一次出现的位置返回下标int indexOf(String str, int fromIndex)从 fromIndex 位置开始查找 str 第一次出现的位置返回下标int lastIndexOf(String str)返回 str 最后一次出现的位置返回下标int lastIndexOf(String str, int fromIndex)从 fromIndex 开始找 str 最后一次出现的位置返回下标String substring(int start)从 start 开始一直到末尾的字符以String方式返回不改变自身String substring(int start, int end)将[start, end)范围内的字符以String方式返回不改变自身int length()获取字符串的有效长度返回长度int capacity()获取底层保存字符串空间总的大小返回容量void ensureCapacity(int minimumCapacity)扩容无返回值String toString()将所有字符按照 String 的方式返回返回String对象从上述例子可以看出String 和 StringBuilder 最大的区别在于 String 的内容无法修改而 StringBuilder 的内容可以修改。频繁修改字符串的情况考虑使用 StringBuilder。2.3 String 与 StringBuilder 的相互转换注意String 和 StringBuilder 类不能直接转换。如果要想互相转换可以采用如下原则String 变为 StringBuilder利用 StringBuilder 的构造方法或append()方法StringBuilder 变为 String调用toString()方法。Stringshello;// String - StringBuilderStringBuildersbnewStringBuilder(s);// 方式一构造方法sb.append( world);// 方式二append// StringBuilder - StringStringretsb.toString();三、StringBuffer 与 StringBuilder 的区别面试题String、StringBuffer、StringBuilder 的区别——这是一道高频面试题核心区别如下String 的内容不可修改StringBuffer 与 StringBuilder 的内容可以修改StringBuffer 与 StringBuilder 大部分功能是相似的StringBuffer 采用同步处理属于线程安全操作而 StringBuilder 未采用同步处理属于线程不安全操作。对比维度StringStringBufferStringBuilder内容可变性不可变修改必产生新对象可修改可修改线程安全安全不可变天然安全安全方法加了同步处理不安全未做同步处理性能频繁修改时极低较高同步有少许开销最高使用场景内容基本不变、作为常量共享多线程环境下频繁修改单线程环境下频繁修改绝大多数场景首选四、总结String 不可变的原因源码级String 类被final修饰不能被继承防止子类破坏不可变性value 数组被private final修饰外部访问不到、引用也不能换类中没有任何修改 value 内容的方法所有涉及修改的操作replace、substring、……都是创建新对象注意误区不是因为 final 导致内容不可变——final 修饰引用类型只是指向不能变内容本可以改不可变的好处方便实现字符串常量池、线程安全、方便缓存 hash code两种可修改的类型StringBuffer与StringBuilder内容可直接原地修改频繁修改字符串时用它们替代 Stringappend返回自身、支持链式调用toString()转回 String两者的区别StringBuffer 同步处理、线程安全StringBuilder 未同步、线程不安全但效率最高——单线程首选 StringBuilder多线程共享时选 StringBufferString 与 StringBuilder不能直接相互赋值/转换只能通过构造方法 / appendString → StringBuilder和toStringStringBuilder → String。如果这篇文章对你有帮助欢迎点赞 收藏 ⭐ 关注后续会继续更新 Java 系列文章