第7章:通用编程(General Programming)
一、导读
1.1 本章学习目标
第7章涵盖了 Java 日常编程中的通用实践和技巧。这些看似琐碎的细节,实际上对代码质量有着深远的影响。本章通过9条经验法则,帮助你掌握日常编程的最佳实践。
本章的核心学习目标包括:
- 掌握局部变量的最佳使用方式:理解作用域最小化原则。
- 学会使用 for-each 循环:理解它相比传统 for 循环的优势。
- 掌握字符串拼接的正确方式:理解 StringBuilder 的使用场景。
- 理解基本类型与包装类型的区别:避免自动装箱的性能陷阱。
- 学会使用反射和原生方法:理解它们的适用场景和局限性。
1.2 为什么第七章如此重要
通用编程实践是代码质量的基石。局部变量的作用域、循环的选择、字符串拼接的方式、基本类型的使用——这些看似微小的决策,累积起来会显著影响代码的可读性、性能和正确性。
1.3 核心问题引导
如何最小化局部变量的作用域?
for-each 循环相比传统 for 循环有什么优势?
什么时候应该使用 StringBuilder?什么时候 String 拼接就够了?
自动装箱在什么场景下会导致性能问题?
反射的性能开销有多大?什么场景下可以接受?
二、核心概念详解
7.1 将局部变量的作用域最小化(Item 57)
7.1.1 作用域最小化原则
// 反模式:变量作用域过大
Set<String> items = getItems();
Iterator<String> it = items.iterator();
// ... 200 行代码 ...
while (it.hasNext()) {
System.out.println(it.next());
}
// 正确做法:在需要时才声明
Set<String> items = getItems();
// ... 200 行代码 ...
for (String item : items) {
System.out.println(item);
}7.1.2 for 循环的优势
for 循环相比 while 循环,可以将迭代变量的作用域限制在循环体内:
// for 循环:迭代变量作用域在循环体内
for (Element e : collection) {
// e 只在这里可见
process(e);
}
// e 在这里不可见
// while 循环:迭代变量可能在外部声明
Iterator<Element> it = collection.iterator();
while (it.hasNext()) {
Element e = it.next();
process(e);
}
// it 在这里仍然可见7.1.3 技巧:先使用后初始化
// 反模式:在循环外初始化
Element e = null;
for (Iterator<Element> it = collection.iterator(); it.hasNext(); ) {
e = it.next();
// ...
}
// 正确做法:在循环头中声明
for (Iterator<Element> it = collection.iterator(); it.hasNext(); ) {
Element e = it.next();
// ...
}7.2 了解和使用库(Item 59)
7.2.1 使用标准库的优势
经过充分测试:标准库的代码经过了大量用户的检验。
性能优化:标准库通常经过精心优化。
可维护性:使用标准库的代码更容易被其他开发者理解。
功能丰富:标准库提供了大量的功能,避免重复造轮子。
// 反模式:自己实现随机数
Random random = new Random();
int n = random.nextInt(max);
// 正确做法:使用 ThreadLocalRandom(多线程环境)
int n = ThreadLocalRandom.current().nextInt(max);
// 反模式:自己实现数组复制
int[] dest = new int[src.length];
for (int i = 0; i < src.length; i++) {
dest[i] = src[i];
}
// 正确做法:使用 System.arraycopy
int[] dest = new int[src.length];
System.arraycopy(src, 0, dest, 0, src.length);
// 更好的做法:使用 Arrays.copyOf
int[] dest = Arrays.copyOf(src, src.length);7.3 如果需要精确的结果,避免使用 float 和 double(Item 60)
// 反模式:使用 double 进行货币计算
System.out.println(1.03 - 0.42); // 0.6100000000000001
System.out.println(1.00 - 9 * 0.10); // 0.09999999999999995
// 正确做法1:使用 BigDecimal
BigDecimal a = new BigDecimal("1.03");
BigDecimal b = new BigDecimal("0.42");
System.out.println(a.subtract(b)); // 0.61
// 正确做法2:使用 int 或 long(以分为单位)
int cents = 103 - 42; // 61 cents
System.out.println(cents); // 617.4 基本类型优于包装类型(Item 61)
7.4.1 基本类型与包装类型的区别
| 特性 | 基本类型 | 包装类型 |
|---|---|---|
| 值 | 实际值 | 对象的引用 |
| 默认值 | 0, false, null | null |
| 内存 | 栈上 | 堆上 |
| 性能 | 快 | 慢(需要装箱拆箱) |
// 反模式:使用包装类型进行比较
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(缓存范围内)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(超出缓存范围)
// 正确做法:使用基本类型
int a = 100;
int b = 100;
System.out.println(a == b); // true7.4.2 自动装箱的性能陷阱
// 反模式:自动装箱创建大量对象
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // 每次循环都创建一个新的 Long 对象
}
System.out.println(sum);
}
// 正确做法:使用基本类型
public static void main(String[] args) {
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}7.5 如果其他语言更适合,就不要使用 Java(Item 67 相关)
这不是说 Java 不好,而是说每种语言都有自己的适用场景。如果你的需求更适合用 Python 做数据分析、用 Rust 做系统编程、用 JavaScript 做前端开发,那就用那些语言。Java 的优势在于企业级应用、大型系统、Android 开发等场景。
三、实战案例
7.1 字符串拼接性能对比
public class StringConcatenation {
public static void main(String[] args) {
int n = 100_000;
// 方式1:String 拼接(O(n²))
long start = System.nanoTime();
String result = "";
for (int i = 0; i < n; i++) {
result += i; // 每次创建新的 String 对象
}
long time1 = System.nanoTime() - start;
// 方式2:StringBuilder(O(n))
start = System.nanoTime();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(i);
}
String result2 = sb.toString();
long time2 = System.nanoTime() - start;
// 方式3:String.join(O(n))
start = System.nanoTime();
String[] parts = new String[n];
for (int i = 0; i < n; i++) {
parts[i] = String.valueOf(i);
}
String result3 = String.join("", parts);
long time3 = System.nanoTime() - start;
System.out.printf("String +=: %,d ns%n", time1);
System.out.printf("StringBuilder: %,d ns%n", time2);
System.out.printf("String.join: %,d ns%n", time3);
}
}7.2 局部变量作用域优化
// 反模式:变量作用域过大
public class DataProcessor {
private List<String> buffer = new ArrayList<>();
public void process() {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("data.txt"));
String line;
while ((line = reader.readLine()) != null) {
buffer.add(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try { reader.close(); } catch (IOException e) {}
}
}
// buffer 仍然持有引用,无法被 GC
}
}
// 正确做法:最小化作用域
public class DataProcessor {
public List<String> process() {
List<String> buffer = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
buffer.add(line);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return buffer;
}
}四、本章小结
第7章系统讲解了通用编程的核心知识:
最小化局部变量作用域:提高代码可读性,减少错误。
了解和使用库:避免重复造轮子,利用经过验证的代码。
避免 float/double 进行精确计算:使用 BigDecimal 或整数。
基本类型优于包装类型:避免自动装箱的性能陷阱。
字符串拼接:循环中使用 StringBuilder,少量拼接用 +。
这些通用编程实践看似简单,却是写出高质量代码的基础。
五、思考与练习
思考题
如何判断一个局部变量的作用域是否过大?
for-each 循环在什么情况下不能使用?
自动装箱的缓存范围是什么?为什么?
BigDecimal 的性能开销有多大?什么场景下需要关注?
标准库有哪些常用的工具类?
编程练习
优化字符串拼接:将一个使用 String += 的循环改为 StringBuilder。
消除自动装箱:找出代码中的自动装箱点并消除。
最小化变量作用域:重构一段代码,将所有变量的作用域最小化。