2017年讲的那些Java基础——String陷阱、Integer缓存和手写连接池

一、这不是网上抄的教程,是内部培训讲义

2017年,做Java基础培训。做的PPT没有花哨的排版,每页都是"一个问题 + 一段代码 + 一个解释"。这些问题都是开发中真实踩过的坑,不是教科书上的课后习题。

现在回头看这些PPT,内容不算高级——但解释方式有自己的理解。把这个讲义整理成文。

二、“1”.equals(str) 而不是 str.equals(“1”)

先看一段会抛空指针的代码:

String str = null;
if (str.equals("1")) {
    System.out.println("好");
} else {
    System.out.println("坏");
}
// 抛 NullPointerException

为什么?str 是 null,没有 .equals() 方法可以调。正确的写法是把常量放前面:

if ("1".equals(str)) {  // "1" 永远不会是 null

这个看起来简单,但在实际项目中出错率极高——因为参数从页面传过来,有时候真的有可能是 null。一开始养成了 str.equals("1") 的习惯,后面写一百个判断都不会想到要反过来。

三、大量字符串拼接为什么不能用 +

String s = "";
for (int i = 0; i < 10000; i++) {
    s = s + i;  // 每次循环创建一个新String对象
}

Java没有操作符重载——除了基本类型的 +。String 能用 + 是因为编译器做了手脚s + i 被编译成 new StringBuilder(s).append(i).toString()。但循环里每次 + 都 new 一个 StringBuilder,10000次循环就是10000个对象创建和销毁。

正确做法是自己维护一个 StringBuffer(线程安全)或 StringBuilder(非线程安全):

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}
String s = sb.toString();  // 只创建一次String

四、Integer = 5 的陷阱——IntegerCache

Integer a = 5;
Integer b = 5;
System.out.println(a == b);  // true

Integer c = 200;
Integer d = 200;
System.out.println(c == d);  // false  为什么?!

因为 Integer a = 5 实际上被编译器转成了 Integer.valueOf(5),而 valueOf 有一段缓存逻辑:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

IntegerCache.low 是 -128,high 默认是 127。所以 -128到127之间的 Integer 是直接从缓存数组里拿的同一个对象。200 超出了范围,走了 new Integer(200),a 和 b 是不同对象,== 比较的是地址。

结论:Integer之间比大小用 equals(),永远别用 ==== 比较的是栈上的地址,Integer 是引用类型。

五、引用传递——Long型修改为什么失效

public void test(Long value) {
    value = 999L;  // 调用方拿不到这个修改!
}

简单类型是值传递——传的是值的拷贝。复杂类型是引用传递——传的是地址。但 Long 是 immutable 的,value = 999L 等价于 value = Long.valueOf(999),这是一个新的 Long 对象,新分配了地址。相当于在函数内部把参数指向了一个新对象,外层的引用还指着旧对象。

要修改 Long 类型的值,只能通过包装一个可变对象(比如用 long[]AtomicLong)。或者干脆用基本类型 long + 返回值。

六、wait 和 sleep 的区别——不是让你背的

  • wait() 是 Object 的方法,必须在 synchronized 块里调用,释放锁,等待 notify() 唤醒
  • sleep() 是 Thread 的方法,不释放锁,到时间自动醒

一个简单记忆法:wait 是"我把锁交出来,别人先用,用完了叫我",sleep 是"我抱着锁睡一会,谁也别动我的东西"。

七、手写一个连接池——五个需求

连接池的本质不是"拿来就用",而是理解这几个问题:

1) 初始化:最大连接数、初始连接数、增量
2) 获取连接:拿一个空闲的,没有就等
3) 自动扩展:连接不够了,但有上限,最多不能超过最大连接数
4) 等待连接:所有连接都在用,新请求要等,设超时
5) 关闭连接:不是真关,是归还到池子里

核心数据结构就是一个 List<Connection> 加上一个 List<Connection>(一个空闲池、一个使用中池):

public class SimplePool {
    private List<Connection> free = new ArrayList<>();
    private List<Connection> used = new ArrayList<>();
    private int maxSize;
    private int step;    // 增量

    public synchronized Connection get() {
        if (free.isEmpty()) {
            if (used.size() >= maxSize) {
                wait(timeout);  // 等别人归还
            } else {
                expand(step);   // 不够但没到上限,创建新连接
            }
        }
        Connection c = free.remove(0);
        used.add(c);
        return c;
    }

    public synchronized void release(Connection c) {
        used.remove(c);
        free.add(c);
        notify();  // 唤醒一个等待的
    }
}

不到100行代码,但连wait/notify、增量扩容、超时等待全用上了。理解了这段代码,用HikariCP的时候就知道它在底层做什么。

八、结语

这些内容不难,难的是在写代码的时候意识到自己在踩这些坑str.equals("1") 抛空指针的时候,如果不知道是因为 str 是 null,就只能猜。知道了,以后所有字符串比较都反着写。这就是培训的价值——不是讲高级概念,是把踩过的坑标出来,让别人不用再踩一次

更多推荐