别做自以为是的优化

这是在刷 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 对象确实可能带来真实开销,但也不能一概把它移到循环外——如果每轮都需要一个独立的新对象,那么创建它就是逻辑的一部分,而不是多余开销。

读完这一大段,我心服口服。这也让我明白,有时候一段代码更好的地方不在性能,而在它更不容易被误用。想到这里,我又记起了之前自学时听过的一条开发规范:变量只在需要它的时候再声明。

这是我自己踩的坑,记下来,也提醒自己:这是我自己踩的坑,记下来,也提醒自己:不是你觉得像优化,它就真的是优化。