本篇是 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");
sb.append(" world"); // 尾部追加,返回自身,可链式调用
sb.insert(5, ","); // 在指定位置插入
sb.delete(0, 3); // 删除 [0, 3) 区间
sb.reverse(); // 整体反转
String s = sb.toString(); // 用完再转回 String

两个注意点:substring() 的返回值是 String 而不是 StringBuilder,截取不会修改原缓冲区;与 String 互转靠构造方法 new StringBuilder(str) 进去、toString() 出来。另外作为参数传递时二者行为不同:String 传进方法后在方法内”修改”,调用方的引用不受影响(因为改的是新对象);StringBuilder 传进去后被 append,调用方拿同一个缓冲区,内容真的变了——面试常拿这一点考参数传递。

冒泡排序与选择排序

排序算法是训练双重循环和”先画图再写码”思维的最好素材。两者都是 O(n²) 的教学级算法,工程上没人手写,但原理必须能讲、代码必须能默。

冒泡:相邻交换,大数沉底

每轮让相邻元素两两比较,大的往后换;一轮下来,最大值像气泡一样”冒”到末尾。n 个数需要 n-1 轮:

public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) { // 比较的轮数
for (int j = 0; j < arr.length - 1 - i; j++) { // 末尾 i 个已排好,不再参与
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

内层条件里的 -i 是关键:每结束一轮,末尾就多一个排定位置的元素。冒泡只在逆序时交换相邻元素,相等元素不会交换,所以它是稳定排序。还有一个常见的优化:如果某一轮下来一次交换都没发生,说明数组已经有序,可以立旗提前退出循环,对基本有序的数组效果显著。

选择:每轮挑最小的放最前面

固定位置 i,让它依次与后面每个元素比较,小的就换到 i 处;第一轮结束后,最小值落在 0 号索引,第二轮确定 1 号,以此类推:

public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}

两者比较次数同为 O(n²),但选择排序的交换次数明显更少;代价是它不稳定,相等元素的相对顺序可能被打乱。所谓”稳定性”在多关键字排序时才有意义:比如先按年龄排、再按姓名排,稳定排序能保证年龄相同的人仍保持姓名的先后。初学阶段先记住结论:冒泡稳定、选择不稳定。

Arrays:生产环境的排序入口

真实开发中排序、打印、查找都交给工具类 Arrays:

int[] arr = {24, 69, 80, 57, 13};
Arrays.sort(arr); // 底层是双轴快排,O(n log n)
System.out.println(Arrays.toString(arr)); // [13, 24, 57, 69, 80]
int idx = Arrays.binarySearch(arr, 57); // 二分查找,前提:数组已排序

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,最常用
String s = String.valueOf(123); // int → String,比 "" + 123 更规范

JDK 5 引入了自动装箱与拆箱,让基本类型和包装类型之间可以隐式转换:

Integer ii = 100;   // 自动装箱,等价于 Integer.valueOf(100)
ii += 200; // 自动拆箱成 int 参与计算,再装箱回去

便利背后是陷阱:Integer x = null; x + 1 会直接抛出 NullPointerException,因为拆箱发生在 null 上。使用包装类型参与运算前,先做 null 判断。另一个性能层面的提醒:在循环里累加请用基本类型,若误用包装类型,每一轮都在拆箱、装箱、产生新对象,白白增加内存分配压力。

Integer 的缓存:一道经典面试题

Integer i1 = 97,  i2 = 97;
Integer i3 = 197, i4 = 197;
System.out.println(i1 == i2); // true
System.out.println(i3 == i4); // false
System.out.println(i1.equals(i2)); // true
System.out.println(i3.equals(i4)); // true

玄机在自动装箱的底层实现 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};
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
sb.append(arr[i]);
if (i < arr.length - 1) {
sb.append(", ");
}
}
sb.append("]");
System.out.println(sb); // [1, 2, 3]

现在一行 Arrays.toString(arr) 就能得到同样结果,但手写一遍能真切体会 StringBuilder 存在的意义。

字符串反转

String s = "abc";
String reversed = new StringBuilder(s).reverse().toString();
System.out.println(reversed); // cba

一行解决——这正是”String 转成 StringBuilder 干活、干完再转回 String”的典型场景。

小结

  • 拼接三兄弟:不可变的 String、线程安全的 StringBuffer、单线程高效的 StringBuilder,默认选 StringBuilder。
  • 冒泡是相邻交换大数沉底,选择是每轮挑最小放前面;二者都是 O(n²) 教学算法,生产排序用 Arrays.sort。
  • 包装类让基本类型能进集合、能带方法;parseInt 与 valueOf 是最常用的转换入口。
  • Integer 缓存范围 -128~127:装箱对象比较请用 equals,== 是坑。