Java StringBuffer、排序与包装类型
本篇是 2017 年 Java 学习笔记系列的第十三天,三个主题:可变字符序列 StringBuffer 与 StringBuilder 的选型逻辑,冒泡和选择这两个入门级排序算法的原理与代码,以及基本类型包装类、自动装箱拆箱机制和 Integer 缓存那道经典面试题。
本篇目标
- 讲清 String、StringBuffer、StringBuilder 三兄弟的区别与选型
- 能手写冒泡排序和选择排序,并知道生产环境该用什么
- 理解八种包装类存在的意义与自动装箱拆箱
- 说清 Integer 缓存的范围和
==比较的坑
三兄弟:String、StringBuffer、StringBuilder
上一篇说过 String 不可变,每次拼接都新建对象;StringBuffer 和 StringBuilder 则内部维护一个可自动扩容的字符数组,append、insert、delete 都是原地修改,不产生新对象。两者的唯一区别在线程安全:StringBuffer 是 JDK 1.0 就有的元老,每个方法都加了 synchronized,多线程安全但慢;StringBuilder 是 JDK 5 补上的”单线程版”,去掉锁换速度。
选型一句话:单线程拼接用 StringBuilder(默认答案);缓冲区被多线程共享时才考虑 StringBuffer;少量一次性拼接用 String 的 + 就行。为什么单线程不该用 StringBuffer?synchronized 的加锁解锁是有成本的,在确定没有线程竞争的场合,这些锁纯属浪费——这也是 JDK 5 专门补一个 StringBuilder 的原因。常见 API:
StringBuilder sb = new StringBuilder("hello"); |
两个注意点:substring() 的返回值是 String 而不是 StringBuilder,截取不会修改原缓冲区;与 String 互转靠构造方法 new StringBuilder(str) 进去、toString() 出来。另外作为参数传递时二者行为不同:String 传进方法后在方法内”修改”,调用方的引用不受影响(因为改的是新对象);StringBuilder 传进去后被 append,调用方拿同一个缓冲区,内容真的变了——面试常拿这一点考参数传递。
冒泡排序与选择排序
排序算法是训练双重循环和”先画图再写码”思维的最好素材。两者都是 O(n²) 的教学级算法,工程上没人手写,但原理必须能讲、代码必须能默。
冒泡:相邻交换,大数沉底
每轮让相邻元素两两比较,大的往后换;一轮下来,最大值像气泡一样”冒”到末尾。n 个数需要 n-1 轮:
public static void bubbleSort(int[] arr) { |
内层条件里的 -i 是关键:每结束一轮,末尾就多一个排定位置的元素。冒泡只在逆序时交换相邻元素,相等元素不会交换,所以它是稳定排序。还有一个常见的优化:如果某一轮下来一次交换都没发生,说明数组已经有序,可以立旗提前退出循环,对基本有序的数组效果显著。
选择:每轮挑最小的放最前面
固定位置 i,让它依次与后面每个元素比较,小的就换到 i 处;第一轮结束后,最小值落在 0 号索引,第二轮确定 1 号,以此类推:
public static void selectSort(int[] arr) { |
两者比较次数同为 O(n²),但选择排序的交换次数明显更少;代价是它不稳定,相等元素的相对顺序可能被打乱。所谓”稳定性”在多关键字排序时才有意义:比如先按年龄排、再按姓名排,稳定排序能保证年龄相同的人仍保持姓名的先后。初学阶段先记住结论:冒泡稳定、选择不稳定。
Arrays:生产环境的排序入口
真实开发中排序、打印、查找都交给工具类 Arrays:
int[] arr = {24, 69, 80, 57, 13}; |
binarySearch 要求数组先有序,找不到时返回负数而不是抛异常。若你想保留元素原始索引,就别对原数组排序后再二分。
包装类:给基本类型穿件对象的外衣
为什么需要包装类?Java 的集合与泛型只认对象,int 放不进 ArrayList;而且把值封装成对象之后,才能随身携带方法——比如字符串转数字,以及 Integer.MAX_VALUE 这类边界常量。可以说包装类是基本类型通往 Java 对象世界的桥梁。对应关系共八种:byte→Byte、short→Short、int→Integer、long→Long、float→Float、double→Double、char→Character、boolean→Boolean。注意 int 和 char 是两个”异类”,并非简单首字母大写,记不住的往往是这两个。
最常用的转换:
int a = Integer.parseInt("123"); // String → int,最常用 |
JDK 5 引入了自动装箱与拆箱,让基本类型和包装类型之间可以隐式转换:
Integer ii = 100; // 自动装箱,等价于 Integer.valueOf(100) |
便利背后是陷阱:Integer x = null; x + 1 会直接抛出 NullPointerException,因为拆箱发生在 null 上。使用包装类型参与运算前,先做 null 判断。另一个性能层面的提醒:在循环里累加请用基本类型,若误用包装类型,每一轮都在拆箱、装箱、产生新对象,白白增加内存分配压力。
Integer 的缓存:一道经典面试题
Integer i1 = 97, i2 = 97; |
玄机在自动装箱的底层实现 Integer.valueOf():它内置了一个缓存,-128 到 127 之间的 Integer 对象预先创建好并反复复用;超出范围的值才 new 新对象。所以 97 拿到的是同一个缓存对象,== 为 true;197 是两个不同对象,== 为 false。顺带一提,new Integer(97) 这种构造方式在现代 JDK 已被标记为废弃,显式构造请用 Integer.valueOf()。
启示只有一条:包装类型比较内容,永远用 equals,或拆成基本类型再用 ==。用 == 比装箱对象,是新手最容易踩、面试官最爱问的坑。补充一点:不止 Integer,Byte、Short、Long、Character 也做了类似的缓存(范围同样是 -128~127 或对应区间),设计动机都一样——小数值使用频率高,缓存复用能显著减少对象创建。
经典练习
把数组拼成 “[1, 2, 3]” 格式
int[] arr = {1, 2, 3}; |
现在一行 Arrays.toString(arr) 就能得到同样结果,但手写一遍能真切体会 StringBuilder 存在的意义。
字符串反转
String s = "abc"; |
一行解决——这正是”String 转成 StringBuilder 干活、干完再转回 String”的典型场景。
小结
- 拼接三兄弟:不可变的 String、线程安全的 StringBuffer、单线程高效的 StringBuilder,默认选 StringBuilder。
- 冒泡是相邻交换大数沉底,选择是每轮挑最小放前面;二者都是 O(n²) 教学算法,生产排序用
Arrays.sort。 - 包装类让基本类型能进集合、能带方法;
parseInt与valueOf是最常用的转换入口。 - Integer 缓存范围 -128~127:装箱对象比较请用 equals,
==是坑。

