这是在刷 LeetCode「两数相加」时踩到的一个坑。
我先自己写了一版用数组实现的解法,发给 AI 检查。它确认没什么大问题后,顺手给了我这道题的参考答案,我便开始研究起来。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
int carry = 0;
while (l1 != null || l2 != null || carry != 0) {
int sum = carry;
if (l1 != null) {
sum += l1.val;
l1 = l1.next;
}
if (l2 != null) {
sum += l2.val;
l2 = l2.next;
}
carry = sum / 10;
cur.next = new ListNode(sum % 10);
cur = cur.next;
}
return dummy.next;
}
这段代码我盯了半天,也没弄明白它的思路,甚至一度以为是这份答案写得有问题。于是请它逐行分析了一遍,这才慢慢看明白。
理解之后,我自然想自己从头写一遍,看看是不是真的会了。结果真有一处写得不一样:
...
int carry = 0;
int sum;
while (l1 != null || l2 != null || carry != 0) {
sum = carry;
...
起初我还沾沾自喜:「我写的比这份答案还好。」
但转念一想,如果这真算优化,AI 怎么可能连这一点都想不到?带着这个问题,我又问了它,想看看它怎么说。
你认为在循环体外声明
int sum;,在循环体内写sum = carry;,和这份答案相比,哪种方式更好?还是说,这其实只是个小打小闹的优化?
它先回了我一句:
性能层面:你的优化收益是精确的 0。
居然是 0 耶!我顿时有点失落。随后它补充道:
Java 会在方法调用时创建栈帧,并一次性为局部变量表分配空间。
int sum无论声明在循环内还是循环外,都不会因为这个位置变化而多出或少掉一次分配。声明在循环内,不会让 JVM 每轮“重新分配”;声明在循环外,也不会“省下分配”。所以,“把变量移到循环外来省开销”这个优化,在 Java 里连小打小闹都算不上——它压根没有减少任何开销。
风格层面:标准答案确实更好。
int sum = carry;把声明和初始化放在了同一行,也把变量的作用域限制在了循环内。这行代码表达得很清楚:sum只在循环内使用,而且每次循环都以carry为初始值。这符合 Java 社区常说的最小作用域原则,《Effective Java》里也有对应条目:变量应该在首次需要它的地方声明,作用域能多窄就多窄。作用域每扩大一寸,能误用它的代码就多一段。
结论
这不是“优化与不优化”之争,而是“语义防错”之争。在这个场景里,两者没有值得关注的性能差异;工程上,这份答案赢在把声明和初始化放在一起,并把作用域压到了最小。
你想减少重复开销的思路本身没错,只是不适用于这里的基本类型变量。循环内
new对象确实可能带来真实开销,但也不能一概把它移到循环外——如果每轮都需要一个独立的新对象,那么创建它就是逻辑的一部分,而不是多余开销。
读完这一大段,我心服口服。这也让我明白,有时候一段代码更好的地方不在性能,而在它更不容易被误用。想到这里,我又记起了之前自学时听过的一条开发规范:变量只在需要它的时候再声明。
这是我自己踩的坑,记下来,也提醒自己:这是我自己踩的坑,记下来,也提醒自己:不是你觉得像优化,它就真的是优化。