如何正确实现两数相加的链表算法(避免空指针编译错误)

本文详解 leetcode「两数相加」题目的标准解法,重点剖析因未初始化 `next` 节点导致的编译期错误(如 `cannot assign field "val" because ".next" is null`),并提供安全、高效、一次遍历的链表加法实现。

你在代码中遇到的错误:

temp.next.val = rem; // ❌ 编译报错:Cannot assign field "val" because ".next" is null

根本原因在于:temp 当前指向一个刚创建的 ListNode(例如 dummy = new ListNode(0)),其 next 字段默认为 null。而你直接对 temp.next.val 赋值,相当于尝试访问 null.val —— 即使尚未运行到该行,现代 Java 编译器(尤其是启用了严格空检查的环境,如某些 IDE 或 Lombok/Checker Framework 集成)也会在编译期提前捕获这一必然空指针风险,并拒绝编译。

更严重的是,你的原始思路存在三重逻辑缺陷

  • ✅ 错误地对同一链表 l1 调用了两次 reverse1(l1) 和 reverse2(l1)(应为 reverse2(l2));
  • ❌ 用反转 + 转整数再相加的方式极易溢出(例3中 7位9+4位9远超 int/long 范围);
  • ❌ 构造结果链表时未创建新节点,而是试图向未初始化的 next 写值,违反链表基本构造规则。

✅ 正确解法:不反转、不转数字、不依赖大整数,仅用一次遍历模拟手工加法(带进位),时间 O(max(m,n)),空间 O(1)(不含输出链表)。

✅ 标准安全实现(推荐)

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0); // 哨兵头节点,简化边界处理
        ListNode curr = 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; int digit = sum % 10; curr.next = new ListNode(digit); // ✅ 关键:先创建新节点,再链接! curr = curr.next; } return dummy.next; // 跳过哨兵 } }

? 关键要点解析

  • curr.next = new ListNode(digit) 是唯一安全写法:每次循环都显式 new 一个节点并赋给 curr.next,确保 curr.next 永不为 null,后续 curr = curr.next 才能安全推进。
  • 无需反转:题目明确“数字以逆序存储”,即 l1 = [2,4,3] 表示 342,个位在前——这恰好匹配加法从低位开始的自然顺序,直接遍历即可
  • 进位处理统一:carry 初始为 0,循环条件包含 || carry != 0,确保最高位进位(如 999 + 1 = 1000)也能被正确生成。
  • 空节点兼容:通过 if (l1 != null) 等判断,天然支持两链表长度不等的情况(如例3)。

⚠️ 注意事项

  • 不要尝试复用输入节点或修改原链表结构(除非题目允许且明确要求空间优化);
  • 避免 int/long 转换:链表长度可能达 100,数值远超 2^63,整数转换必溢出;
  • ListNode 构造必须显式调用:new ListNode(val),不能假设 next 已就绪;
  • 若使用 Kotlin 或启用了 @Nullable 注解的 Java 项目,此错误会更早暴露——这是编译器在帮你预防运行时崩溃。

掌握这种“边遍历边构造”的模式,是解决链表类问题的核心范式。它简洁、健壮、符合题目约束,也是面试官期望看到的标准答案。