很多开发者已经习惯了让 AI 补全代码,输入一个方法名,接下来的逻辑自动生成,连注释、单元测试、SQL 都能一键生成。但假如我们做一个极端假设——AI 从未诞生,所有代码都要从第一个字符开始手写,开发工作会变成什么样?
这个问题并不是单纯的“怀旧”,它其实是开发者基本功的一面镜子:那些被 AI 接管或掩盖掉的环节,恰恰是手写代码时代必须亲自掌握的核心能力。本文不打算煽情地讨论“AI 会不会取代程序员”,而是从手写代码的真实工作流出发,拆解一个开发者在不依赖 AI 时的完整技术栈:算法、SQL、JDBC、异常排查、工程化配置。你会发现,即使 AI 工具已经非常普及,这些手写能力依然是代码质量的下限。
无论你是在校学生、刚入门的新手,还是已经使用 AI 辅助开发的工程师,这篇文章都能帮你重新审视“手写代码”到底难在哪里,以及如何在 AI 时代有意识地保留这些基本功。
1. 假如 AI 从未诞生,手写代码的“永恒困境”是什么
1.1 一个真实的假设,不是历史课
先明确一个前提:这里说的“AI 从未诞生”,不是回到没有任何智能辅助的远古时代,而是指代码的生成、补全、自动排错这些能力全部失效。你仍然可以使用 IDE、编译器、版本控制工具,但没人替你写代码,也没人告诉你下一步该写什么。
在这种状态下,一次最简单的开发任务,比如“写一个接口查询用户列表”,你会遇到以下问题:
- 数据库表结构怎么设计,字段类型选什么;
- JDBC 连接串怎么写,驱动类全限定名是什么;
- 查询条件该用
Statement还是PreparedStatement,为什么; - 连接用完要怎么关闭,异常时会不会造成连接泄漏;
- 返回结果还要不要做空值判断;
- 代码该怎么分层,业务逻辑放到哪里。
这些问题单独看都不难,但全部堆在一起,就构成了“手写代码的困境”。它们不是某一个技术点难到无法解决,而是大量细节要求你持续保持精确,任何一个环节记忆模糊,都会让程序停在编译期或运行期。
1.2 永恒困境的三个层面
手写代码的困境可以归纳为三个层面,分别对应编程能力的三种“硬伤”:
| 困境层面 | 典型表现 | 本质原因 |
|---|---|---|
| 语法层 | 方法名拼写错误、括号不匹配、缺少导入包 | 对语言 API 记忆不牢,依赖 IDE 提示 |
| 逻辑层 | 边界条件没考虑、空指针、死循环、数据覆盖 | 对算法流程和数据结构理解不够 |
| 环境层 | 类库冲突、数据库连接失败、配置文件加载不到 | 对运行环境和构建过程缺乏整体认知 |
这三层困境层层递进。AI 工具可以帮你快速跳过语法层,甚至帮你“猜”出逻辑层可能存在的问题,但环境层的问题是 AI 最难替你判断的,因为它是运行时上下文相关的,需要你对整个系统有真实理解。
1.3 为什么“手写”依然值得练习
有人会问:既然有 AI,为什么还要练手写?答案是:AI 的生成质量取决于你的判断质量。如果你连正常代码长什么样都不熟悉,就无法识别 AI 生成结果是否合理,也无法在编译报错时定位到真正的根因。
手写代码不是目的,而是建立“程序感觉”最直接的方式。这种感觉包括:
- 看到一段代码能快速预判它的运行结果;
- 遇到异常时能根据堆栈定位到具体代码行;
- 做设计时能提前识别高风险模块。
这些能力是 AI 工具无法“继承”给开发者的,只能在手动编写、调试、修改中积累。
2. 手写代码的真实工作流:一条必须由人走完的链路
2.1 从需求到代码:人工决策链
在没有 AI 生成的情况下,一个完整的功能开发通常要经过以下决策链:
- 需求分析:明确要做什么,输入输出是什么;
- 技术选型:选择语言、框架、数据库、构建工具;
- 数据库设计:确定表和字段,考虑索引与外键;
- 接口设计:定义方法签名、参数类型、返回值,约定异常;
- 编码实现:按分层结构逐个类去写;
- 单元测试:准备测试数据,验证核心方法;
- 部署验证:在本地或测试环境启动,观察日志和结果。
这条链路中的每一步都依赖开发者自己的判断。AI 能帮你把第 5 步的一部分代码补全,但需求分析、技术选型、数据库设计、接口定义这些前置工作,仍然需要真实经验支撑。
2.2 手写代码时代的环境依赖
手写代码时代,环境依赖会显得格外重要。一个典型 Java 后端项目的环境至少包括:
- JDK:配置
JAVA_HOME和PATH; - 构建工具:Maven 或 Gradle,依赖坐标要自己写;
- 数据库:MySQL、Oracle 等,服务要启动并保证可连接;
- 应用服务器或内置容器:Tomcat、Jetty;
- 版本控制:Git,分支和提交流程。
如果环境不一致,就会出现“我本地能跑,到测试环境就报错”的经典问题。手写代码的困境之一,就是你必须对这些环境变量、配置文件、依赖版本有明确意识,否则在一个干净环境中根本无法启动项目。
一个最典型的例子是 JDBC 连接数据库。很多人认为这只是五个标准步骤,但当“找不到驱动类”或“连接超时”出现时,如果对Class.forName的过程不理解,排查会非常吃力。
2.3 AI 出现后哪些环节被接管了
为了更清楚地理解“手写”意味着什么,可以对比 AI 工具接管了哪些工作:
| 工作环节 | 传统手写方式 | AI 辅助方式 | 手写仍不可替代的部分 |
|---|---|---|---|
| 代码生成 | 逐行编写 | 自动补全、生成整个方法 | 逻辑评审、代码走查 |
| SQL 编写 | 手写语句并 explain | 根据表结构生成 SQL | 索引设计、事务边界 |
| 单元测试 | 手写 Mock 和断言 | 自动生成测试骨架 | 测试用例的有效性 |
| 异常排查 | 阅读堆栈跟踪和日志 | 解释报错并给建议 | 定位根因并修复 |
| 架构设计 | 人工权衡 | 提供方案选项 | 最终决策和兜底 |
从这个表格可以看出,AI 接管的是“执行效率”部分,而“判断”和“责任”部分仍然留给开发者。这也是为什么手写代码的基本功在 AI 时代不但没有过时,反而变得更重要。
3. 手写代码的核心能力拆解
3.1 算法与数据结构:没有 AI 时你必须有思路
算法是手写代码中最典型的“逻辑层困境”。如果没有 AI,遇到一个问题需要自己设计算法并写出可运行代码。以经典的快速排序为例,完整手写需要理解递归、分区和指针移动:
// 文件路径:src/main/java/com/example/algorithm/QuickSort.java public class QuickSort { public static void quickSort(int[] arr, int left, int right) { if (left >= right) { return; } int pivot = partition(arr, left, right); quickSort(arr, left, pivot - 1); quickSort(arr, pivot + 1, right); } private static int partition(int[] arr, int left, int right) { int pivot = arr[right]; int i = left; for (int j = left; j < right; j++) { if (arr[j] < pivot) { swap(arr, i, j); i++; } } swap(arr, i, right); return i; } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }这段代码没有使用任何复杂 API,但初学者很容易在partition中出错。常见问题包括:
- 分区时选少了某一边,导致递归栈溢出;
- 循环边界用错,出现数组越界;
- 交换逻辑写反,导致排序结果错误。
这类问题如果没有 AI 帮忙 review,就只能靠单步调试和反复阅读来发现。手写算法的价值在于,它能训练你从“会读代码”变成“会构造代码”。
3.2 数据库操作:SQL 和事务必须手写准确
在没有 AI 辅助的时代,SQL 是必须亲手写且写准确的。一个简单的用户表设计如下:
-- 文件路径:sql/init.sql CREATE TABLE `user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `email` VARCHAR(100) DEFAULT NULL, `age` INT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;手写 SQL 时,需要同时考虑查询正确性和执行性能。以查询为例:
SELECT id, username, email, age, create_time FROM user WHERE username = ? ORDER BY id DESC LIMIT 0, 10;这段 SQL 看似简单,但它涉及了几个核心概念:
- 使用
WHERE限定范围,避免全表扫描; - 对
username建立索引,加速查询; - 使用
LIMIT分页,避免一次加载过多数据; - 使用
PreparedStatement的占位符?,而不是拼接字符串,防止 SQL 注入。
如果只依赖 AI 生成 SQL,而不理解这些设计原因,一旦线上出现慢查询或 SQL 注入漏洞,你将很难从执行计划中定位问题。
事务边界也是一个典型的手写困境。比如转账场景,必须先扣款再加款,两个操作必须在同一个事务中提交或回滚:
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 1; UPDATE account SET balance = balance + 100 WHERE user_id = 2; COMMIT;在实际开发中,事务范围要放多大,隔离级别怎么选,锁竞争如何避免,这些问题都依赖对数据库原理的理解。手写数据库代码是培养这种理解最扎实的路径。
3.3 异常处理与日志排查:找不到 AI 问时怎么办
手写代码时,异常处理是你面对错误的第一道防线。Java 中常见的异常体系包括:
| 异常类型 | 示例 | 处理方式 |
|---|---|---|
| 编译期异常 | IOException、SQLException | 捕获或向上抛出 |
| 运行时异常 | NullPointerException、ArrayIndexOutOfBoundsException | 通常通过代码逻辑避免 |
| 错误 | OutOfMemoryError、StackOverflowError | 一般不做捕获,要调整设计 |
手写代码时,最容易掉进“为了处理异常而处理”的误区:要么空捕获,要么把所有异常包成一个Exception。一个相对规范的手写异常处理方式是:
// 文件路径:src/main/java/com/example/service/UserService.java import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class UserService { public String findUsernameById(Long id) { String url = "jdbc:mysql://127.0.0.1:3306/user_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; String user = "root"; String password = "your_password"; String sql = "SELECT username FROM `user` WHERE id = ?"; try (Connection conn = DriverManager.getConnection(url, user, password); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rs.getString("username"); } } return null; } catch (SQLException e) { // 记录异常,而不是吞掉 System.err.println("查询用户失败, id=" + id + ", sqlState=" + e.getSQLState()); throw new RuntimeException("数据库查询失败", e); } } }这段代码演示了几个手写过程中的关键点:
- 使用
try-with-resources自动关闭Connection、PreparedStatement、ResultSet,这是 JDK 7 之后的标准写法; - 在
catch中打印尽可能多的上下文信息,包括参数id和SQLState; - 捕获到异常后,用
throw new RuntimeException向上传递,而不是吞掉异常假装成功; - 数据库密码等敏感信息不应该硬编码在类里,应该使用配置文件和环境变量。
手写代码的困境之一,就是面对异常时需要在“忽略、重试、包装、抛出”之间做出选择。这个选择能力不是从 AI 中获得的,而是从一次次的故障处理中成长起来的。
4. 手写代码实战:一个没有 AI 辅助的 Java 查询案例
这一章我们完整走一遍手写代码流程,体会一下没有 AI 时如何从零构建一个小功能。这里以“查询用户列表”为需求,使用纯 JDBC 实现,不引入 Spring。目的是展示手写 JDBC 时最容易出错、也最需要逻辑闭环的环节。
4.1 项目结构与需求
需求很简单:从user表查询指定年龄以上的用户列表,按 ID 倒序排列,最多返回 10 条。
项目结构建议如下:
user-demo/ ├── pom.xml ├── sql/ │ └── init.sql └── src/main/java/com/example/ ├── model/ │ └── User.java ├── dao/ │ └── UserDao.java └── Main.java这里不使用 Spring Boot,仅用 Maven 管理 JDBC 驱动依赖。你可以感受到,在没有 AI 的情况下,连“引入哪些依赖”都需要亲手确定。
pom.xml中只需要 MySQL 驱动:
<!-- 文件路径:user-demo/pom.xml --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>user-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies> </project>4.2 手写 JDBC 连接与查询
先定义实体类User:
// 文件路径:user-demo/src/main/java/com/example/model/User.java package com.example.model; import java.time.LocalDateTime; public class User { private Long id; private String username; private String email; private Integer age; private LocalDateTime createTime; public User() { } public User(Long id, String username, String email, Integer age, LocalDateTime createTime) { this.id = id; this.username = username; this.email = email; this.age = age; this.createTime = createTime; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", email='" + email + '\'' + ", age=" + age + ", createTime=" + createTime + '}'; } // getter / setter 可以自行补充,为了控制篇幅这里省略 public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } }接着是 DAO 层。这里要写 JDBC 查询逻辑,注意使用PreparedStatement防止参数拼接导致的 SQL 注入:
// 文件路径:user-demo/src/main/java/com/example/dao/UserDao.java package com.example.dao; import com.example.model.User; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; public class UserDao { private final String url; private final String username; private final String password; public UserDao(String url, String username, String password) { this.url = url; this.username = username; this.password = password; } public List<User> findUsersOlderThan(int minAge, int limit) { String sql = "SELECT id, username, email, age, create_time " + "FROM `user` WHERE age > ? ORDER BY id DESC LIMIT ?"; List<User> users = new ArrayList<>(); try (Connection conn = DriverManager.getConnection(url, username, password); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, minAge); ps.setInt(2, limit); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setEmail(rs.getString("email")); user.setAge(rs.getInt("age")); Timestamp createTime = rs.getTimestamp("create_time"); if (createTime != null) { user.setCreateTime(createTime.toLocalDateTime()); } users.add(user); } } return users; } catch (SQLException e) { System.err.println("数据库查询失败, minAge=" + minAge + ", limit=" + limit); e.printStackTrace(); throw new RuntimeException("查询用户失败", e); } } }这里有几个手写代码时容易忽略的细节:
LIMIT ?参数使用setInt设置,MySQL JDBC 驱动支持这种方式;rs.getTimestamp("create_time")要先转成Timestamp,再变成LocalDateTime,避免类型转换异常;- 所有资源都通过 try-with-resources 自动关闭,避免连接泄漏;
- 如果表结构或字段名不匹配,
rs.getXXX会抛出异常,这也是手写代码最常见的运行时问题之一。
4.3 手写资源释放与事务
上面示例已经体现了资源释放,但实际项目中,事务问题往往更难处理。例如,如果findUsersOlderThan需要在一个事务中查询并对结果做更新,则不能简单地每次都获取新连接,而是要让多个操作共享同一个Connection。
// 文件路径:user-demo/src/main/java/com/example/dao/UserDao.java // 片段:事务内执行查询和更新 public void updateUserAgeWithTransaction(Long userId, int newAge) { String selectSql = "SELECT age FROM `user` WHERE id = ? FOR UPDATE"; String updateSql = "UPDATE `user` SET age = ? WHERE id = ?"; try (Connection conn = DriverManager.getConnection(url, username, password)) { conn.setAutoCommit(false); try (PreparedStatement selectPs = conn.prepareStatement(selectSql)) { selectPs.setLong(1, userId); try (ResultSet rs = selectPs.executeQuery()) { if (rs.next()) { int oldAge = rs.getInt("age"); System.out.println("用户原年龄: " + oldAge); } } } try (PreparedStatement updatePs = conn.prepareStatement(updateSql)) { updatePs.setInt(1, newAge); updatePs.setLong(2, userId); int rows = updatePs.executeUpdate(); System.out.println("更新行数: " + rows); } conn.commit(); System.out.println("事务提交成功"); } catch (SQLException e) { System.err.println("事务执行失败,已回滚"); e.printStackTrace(); throw new RuntimeException("事务失败", e); } }手写事务代码时,必须记住几个关键点:
- 在
finally或关闭连接前处理回滚,否则异常会导致部分数据变更; - 使用
FOR UPDATE时要注意锁的粒度,避免长时间锁表; - 在实际生产环境中,事务应该尽量短,不要在事务里做耗时的外部调用;
- 如果连接获取失败,也要保证不会把未提交的数据留在数据库中。
这个例子已经展示出,手写代码的困境不只是“不会写 API”,更重要的是“理解执行流程和数据一致性”。
4.4 运行验证与结果
编写完 DAO 后,还需要一个入口来执行验证。下面是一个简单的Main类:
// 文件路径:user-demo/src/main/java/com/example/Main.java package com.example; import com.example.dao.UserDao; import com.example.model.User; import java.util.List; public class Main { public static void main(String[] args) { String url = "jdbc:mysql://127.0.0.1:3306/user_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; String username = "root"; String password = "your_password"; UserDao userDao = new UserDao(url, username, password); List<User> users = userDao.findUsersOlderThan(18, 10); if (users.isEmpty()) { System.out.println("没有符合条件的用户"); } else { for (User user : users) { System.out.println(user); } } } }预期的输出类似:
User{id=3, username='zhangsan', email='zhangsan@example.com', age=25, createTime=2025-01-02T10:00:00} User{id=2, username='lisi', email='lisi@example.com', age=22, createTime=2025-01-01T09:30:00}如果数据库中没有数据,或者minAge设置过大,就会输出“没有符合条件的用户”。这种结果验证过程,正是手写代码时最重要的反馈循环:写代码、运行、看结果、修正理解。
5. 有 AI 编程工具,为什么还要会手写代码
5.1 AI 补全的代码也需要评审
很多 AI 工具生成代码时,会参考海量代码模式,但它并不确切了解你要处理的数据、业务约束、安全要求。比如,AI 可能生成一段直接用Statement拼接 SQL 的代码,因为它模仿了某些旧代码的风格;也可能生成一个看似简洁但忽略空指针保护的 lambda 表达式。
手写代码的经验,让你在评审 AI 生成结果时能迅速发现问题。至少要检查以下几点:
- 是否符合项目的分层规范;
- 是否有资源泄漏风险;
- 是否有边界条件被忽略;
- 是否有更简单的实现方式。
如果完全没有手写经验,你面对 AI 的输出几乎无法判断优劣,只能“能跑就行”,这是很危险的。
5.2 手写代码培养的是“排错直觉”
排错直觉听起来像是玄学,但它本质上是一种模式识别能力。当你写过很多次NullPointerException之后,看到user.getAddress().getCity()这类链式调用时,会本能地警惕user、getAddress()返回值是否可能为 null。这种直觉完全来自于手写代码时的反复踩坑。
举个例子,一个常见的手写代码错误是:
String name = null; if (name.equals("admin")) { System.out.println("is admin"); }这种写法会抛NullPointerException。正确的做法是把常量放在前面:
String name = null; if ("admin".equals(name)) { System.out.println("is admin"); }AI 可能会直接给出第二种写法,但它不会告诉你为什么。等你真正在手写代码中遇到第一种写法并排查半天之后,才会理解“常量在前”这一习惯的价值。这就是手写带来的排错直觉。
5.3 手写代码的工程边界
在 AI 时代,仍然存在一些必须手写(或至少手工严格 review)的代码区域:
- 核心业务逻辑:涉及金额、库存、状态流转等强一致性的代码;
- 安全敏感代码:登录、授权、密钥管理、参数校验;
- 性能关键路径:高频调用的 SQL、缓存策略、批量处理逻辑;
- 底层协议或格式解析:自定义二进制协议、复杂文件解析。
这些区域不允许“生成后拿过来就用”。手工编写这些代码,能确保每一步都经过完整逻辑推演,而不是依赖模型预测。手写代码不是低效,而是在关键路径上的风险控制。
6. 手写代码常见问题与排查思路
6.1 常见编译错误与解决
手写代码时,编译错误是最先遇到的困境。以下是常见场景:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 找不到符号 | 方法名拼写错误、未导入包 | 检查类名和 import 列表 |
| 类型不兼容 | 把字符串传给数值参数 | 使用Integer.parseInt等进行转换 |
| 缺少返回语句 | 分支中没有覆盖所有路径 | 检查 if-else 是否都返回 |
| 无法访问的语句 | return 之后还有代码 | 删除无效代码 |
遇到编译错误时,不要急着问 AI,先看编译器给的行号和提示,这能培养最基础的排查能力。编译错误是反馈最快的,也是最容易自我纠正的。
6.2 运行时异常排查清单
运行时异常比编译错误更隐蔽。常见问题如下:
| 异常类型 | 触发场景 | 排查步骤 |
|---|---|---|
NullPointerException | 未初始化对象或方法返回 null | 从堆栈找到具体行,检查链式调用 |
ArrayIndexOutOfBoundsException | 下标越界 | 检查循环边界和数组长度 |
ClassCastException | 强制类型转换失败 | 使用instanceof判断 |
SQLException | 数据库连接失败、SQL 语法错误 | 查看 SQLState 和异常消息 |
一个比较实用的排查清单是:
- 完整阅读异常堆栈,找到第一行自己的代码;
- 打印关键变量的值,确认数据是否符合预期;
- 用最小样例复现,排除环境干扰;
- 考虑并发场景,检查是否有多线程修改共享数据;
- 分析日志,看是单一请求问题还是系统性问题。
手写代码时代,这个清单几乎每天都会用。现在有 AI 帮忙解释报错,但定位根因的路径仍然要自己走。
6.3 数据库连接与事务问题
数据库问题在手写代码时尤其常见。排查顺序通常是:
- 数据库服务是否启动,端口是否可访问;
- 驱动 JAR 是否引入,驱动类名是否正确;
- 用户名密码是否正确,账号是否有权限;
- SQL 语法是否正确,可以在数据库客户端直接执行验证;
- 事务是否提交,连接是否被意外关闭;
- 是否存在死锁,锁等待是否超时。
处理数据库操作时必须遵循安全底线:涉及UPDATE、DELETE时,要先在测试环境验证,带上精确的WHERE条件,做好数据备份,并采用最小权限原则分配数据库账号。手写代码阶段,这些习惯会直接影响线上安全。
7. 最佳实践:手写代码的基本功训练方法
7.1 刻意练习的日常化
手写代码的能力提升,靠的不是临阵磨枪,而是持续训练。推荐三种方式:
- 算法打卡:每天手写一道算法题,不依赖自动补全,重点训练逻辑设计和边界处理;
- 白纸写代码:在纸上或空白编辑器中写一个小功能,比如字符串去重、链表反转,不查资料;
- 读源码画流程:阅读 Spring、JDK 等开源项目源码,用文字或结构图理清调用链。
这些训练不是让你效率变低,而是让你在 AI 时代具备“没有 AI 也能完成”的底气。
7.2 代码规范与注释习惯
手写代码时,规范尤其重要,因为代码的可维护性会直接影响后续排错。建议关注:
- 命名清晰:方法名用动词开头,布尔变量用
is、has开头; - 函数长度:单个方法不要超过 50 行,过长的要拆分;
- 注释作用:注释说明“为什么”,而不是逐行翻译代码;
- 日志级别:业务关键路径用
info,异常场景用error,不要用printStackTrace代替日志框架。
一段相对规整的手写代码,应该具备“一眼扫过能明白逻辑”的特征。这也是 AI 生成代码后,你进行人工评审的基本标准。
7.3 构建自己的排错手册
手写代码的经历是宝贵的素材。建议在本地维护一个个人排错手册,记录你遇到过的异常、根因和解决过程。比如:
## 2025-06-10 NullPointerException 现象:调用 user.getAddress().getCity() 报空指针 根因:部分用户没有绑定地址,getAddress() 返回 null 解决:先用 address != null 判断,再获取 city 预防:接口返回 DTO 时统一做空值兜底这个文档的价值会随着时间增长。日后遇到相似问题时,你不用重新踩一遍坑,直接检索就能定位思路。这种积累方式,是 AI 无法直接“给到你”的。
8. 手写代码能力自测清单
回到开头的问题:假如 AI 从未诞生,手写代码的困境到底是什么?答案其实很朴素——它不是某一个语法不会写,而是从设计、编码、排错到交付,每一个环节都需要开发者拥有完整闭环的判断力。
如果你也想检验自己的手写基本功,可以参考下面这个自测清单:
- 能否不借助搜索引擎,写一个完整的 JDBC 查询方法并正确处理资源释放;
- 能否手写一个快速排序、链表反转,并解释时间复杂度;
- 能否解释为什么 SQL 要使用
PreparedStatement,而不是字符串拼接; - 能否只通过堆栈信息定位一个
NullPointerException并修复; - 能否在多表关联时设计合理的索引,并通过
EXPLAIN验证; - 能否在项目环境发生变化时,快速定位是配置问题还是代码问题。
如果以上大部分都能做到,那么即使 AI 工具失灵,你依然可以独立交付功能。如果有些还不熟练,也不用焦虑,手写能力是一点点攒出来的。日常开发时,可以有意识地关闭代码补全提示,亲手敲完整块逻辑,再借助 AI 做评审和优化。这样既能享受 AI 带来的效率提升,也不会丢掉属于程序员最核心的排错能力和工程判断力。
下一步,你可以继续深入学习 JDBC 连接池(HikariCP、Druid)、MyBatis 的底层执行流程、Spring 的事务传播机制,这些内容都会让“手写代码”和“AI 辅助”之间的配合更自然。欢迎收藏本文,也欢迎你在评论区分享自己手写代码时最难忘的一次排查经历。