2017年我给同事讲的Java基础——String陷阱Integer缓存和手写连接池
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,就只能猜。知道了,以后所有字符串比较都反着写。这就是培训的价值——不是讲高级概念,是把踩过的坑标出来,让别人不用再踩一次。
更多推荐

所有评论(0)