Java 最佳实践
Java 最佳实践:写出专业、可维护的高质量代码
Java 作为企业级开发的主流语言,仅仅是“代码能跑”是远远不够的。本教程将带你系统性地掌握从编码规范到性能优化的全套 Java 最佳实践,教你写出同事愿意维护、测试友好、生产稳定的代码。本文内容由浅入深,特别适合已掌握 Java 基础,希望提升工程素养的开发者。
一、编码规范与可读性
代码是写给人看的,只是顺便让机器执行。在团队协作和长期维护中,统一、清晰的编码风格比精妙的算法更重要。
1.1 命名约定
- 类与接口:使用大驼峰命名法(
PascalCase),名词或名词短语。如CustomerService,OrderRepository。 - 方法与变量:使用小驼峰命名法(
camelCase),动词或动词短语。如calculateTotalPrice(),isAvailable。 - 常量:全大写,单词间用下划线分隔。如
MAX_RETRY_COUNT。 - 避免缩写:除非是业内通用缩写(如
Id、Url),否则使用完整单词。calculateAverage()永远比calcAvg()更易理解。
1.2 代码格式
- 缩进统一使用 4 个空格,避免 Tab 混用。
- 单行长度建议不超过 120 字符,IDE 配置参考线。
- 操作符两侧加空格,方法参数逗号后加空格。
- 大括号采用 K&R 风格(左大括号不换行),与 Java 官方风格保持一致。
1.3 注释的正确姿势
- 注释解释“为什么”,而不是“做什么”。代码本身应能自描述做什么。
- 及时更新注释,过期的注释比没有注释危害更大。
- 使用 Javadoc 为公开 API 编写文档,包括
@param,@return,@throws等标签。 - 避免大段废弃代码被注释掉,应直接删除(版本管理工具会记住它)。
二、防御性编程与异常处理
健壮的代码永远假设输入可能非法、外部资源可能失效、依赖的模块可能崩溃。
2.1 参数校验
在每个方法的入口处对关键参数进行非空、合法范围等判断。善用 java.util.Objects 工具类。
public void processOrder(Order order) {
Objects.requireNonNull(order, "order must not be null");
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must contain at least one item");
}
// 业务逻辑
}
尽早抛出异常,并附带上下文信息,方便定位问题。
2.2 异常的正确使用
- 只捕获你能处理的异常。不要为了“不让程序崩溃”而使用空的
catch块。 - 如果当前层无法处理,将原始异常包装后重新抛出,保留异常链:
catch (IOException e) { throw new UncheckedIOException("Failed to read config file", e); } - 别用异常控制正常业务流程。先检查条件(如文件是否存在),而不是依赖捕获
FileNotFoundException。 - 在顶层框架或统一的异常处理器中记录并转换异常,避免向用户暴露堆栈信息。
2.3 资源管理
所有实现了 AutoCloseable 接口的资源(流、数据库连接、文件句柄等)必须使用 try-with-resources 语句确保关闭:
try (FileInputStream fis = new FileInputStream("data.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理
}
}
这比在 finally 块中手动关闭更简洁且安全,避免资源泄漏。
三、集合与泛型的最佳使用
Java 集合框架使用频率极高,不恰当的选择和用法会引发性能瓶颈和隐蔽的 bug。
3.1 选择合适的集合类型
- 需要快速随机访问:
ArrayList(O(1))优于LinkedList。 - 频繁在头部插入删除:
LinkedList或ArrayDeque。 - 需要去重或集合运算:
HashSet(无序)或TreeSet(排序)或LinkedHashSet(保留插入顺序)。 - 键值对映射:
HashMap(通用)、TreeMap(排序)、LinkedHashMap(保留插入顺序),并发场景用ConcurrentHashMap。
3.2 使用泛型提升类型安全
永远不要使用原始类型(raw type),使用具体类型参数:
List<String> names = new ArrayList<>(); // 正确
List list = new ArrayList(); // 危险,丧失类型检查
对于泛型方法,利用 PECS 原则(Producer Extends, Consumer Super)来定义通配符边界,使 API 更加灵活:
// 能读取 Number 及其子类的集合
public void readNumbers(List<? extends Number> list) { ... }
// 能存放 Integer 及其父类的集合
public void addIntegers(List<? super Integer> list) { ... }
3.3 不可变集合
当集合不应被修改时,使用 Collections.unmodifiableList() 或 Java 9+ 的 List.of(), Set.of(), Map.of()。这能防止意外修改并提升线程安全性。
3.4 避免常见的性能坑
- 在循环中拼接字符串使用
StringBuilder,不要用+操作符。 - 遍历
Map需要键和值时,使用entrySet()而不是先遍历keySet()再get()。 - 使用
Collections.emptyList()等返回空集合而不是null,省去调用方的判空负担。
四、并发与多线程实践
写出正确的并发代码难度很高,遵守一些原则可以有效减少错误。
4.1 优先使用高级并发工具
不要直接操作 Thread 和原始的 wait/notify。应该使用 java.util.concurrent 包下的:
ExecutorService管理线程生命周期。CompletableFuture组合异步任务。CountDownLatch,CyclicBarrier等同步辅助类。- 线程安全集合如
ConcurrentHashMap,CopyOnWriteArrayList。
4.2 确保共享变量的可见性
对多个线程间共享的变量,必须使用 volatile 关键字保证可见性,或使用 synchronized/Lock 保证原子性和可见性。绝不要依赖“感觉上”运行正常而忽视 happens-before 原则。
4.3 锁的使用原则
- 仅在有可变状态共享时才使用锁。
- 同步块要尽可能小,避免持有锁时进行耗时操作(I/O、网络调用)。
- 使用
ReentrantLock可实现更灵活的锁控制,如超时尝试、可中断获取锁。 - 避免嵌套锁,如无法避免,必须保持锁顺序一致,防止死锁。
4.4 线程安全的编码模式
- 尽量设计不可变对象(final 类、所有字段 final、无修改方法),天然线程安全。
- 使用线程封闭(ThreadLocal)将对象限制在单个线程内。
- 使用
volatile的布尔标志来安全停止线程,而不是过时的Thread.stop()。
五、性能与内存优化
不要过早优化,但应该有意识地写出高性能的代码,避免明显低效的实现。
5.1 字符串与正则优化
- 编译一次可复用的正则对象:
Pattern.compile()后复用Pattern实例。 - 大批量格式化操作考虑使用
MessageFormat或String.format(),但注意其开销,对极致性能场景可用手动拼接。
5.2 缓存与懒加载
- 对于计算成本高的数据,使用本地缓存(如
HashMap作为简单缓存,或用 Caffeine 等成熟库)。 - 注意缓存失效策略和内存占用,避免缓存泄漏。
- 懒加载(如单例的延迟初始化)可加快启动速度,但要确保线程安全(双检锁或静态内部类)。
5.3 避免创建不必要的对象
- 优先使用
Long.valueOf()等静态工厂方法,可能利用缓存。 - 对于频繁使用的同一逻辑,重用对象(如
Boolean.TRUE而不是new Boolean(true))。 - 注意自动装箱带来的性能开销,尤其是在循环中。
5.4 IO 优化
- 使用缓冲流(
BufferedInputStream/BufferedOutputStream)包装文件流以减少系统调用。 - 处理文本时指定字符集,如
StandardCharsets.UTF_8,避免依赖平台默认字符集。 - 大文件操作使用 NIO (
FileChannel,MappedByteBuffer) 获得更好的性能。
六、测试与代码质量保障
高质量代码必须可测试,并且被自动化测试覆盖。
6.1 单元测试最佳实践
- 使用 JUnit 5 编写测试,测试类名以 Test 结尾,方法名清晰表达测试意图(如
shouldThrowExceptionWhenInputInvalid)。 - 遵循 AAA 模式:Arrange(准备数据)、Act(执行被测方法)、Assert(验证结果)。
- 使用 Mockito 等框架隔离外部依赖,只测试当前单元逻辑。
- 测试覆盖率追求有意义的分支覆盖,而不是 100% 数字目标。
6.2 编写可测试代码
- 依赖注入:通过构造函数或 setter 注入依赖,而不是在方法内部
new具体类,方便替换为 mock。 - 避免过度使用静态方法和静态状态,它们难以 mock 和隔离。
- 让类尽量小且职责单一,便于编写针对性的测试。
6.3 静态代码分析与代码审查
- 集成静态分析工具,如 SonarQube、Checkstyle、SpotBugs,在 CI 阶段自动检查代码规范、潜在 bug 和安全漏洞。
- 坚持代码审查(Code Review),关注逻辑正确性、可读性、性能和安全问题。
七、安全编程实践
Java 应用经常面临安全威胁,基础的安全习惯能堵住大部分漏洞。
7.1 防范 SQL 注入
- 永远使用
PreparedStatement参数化查询,绝不用字符串拼接 SQL 语句。 - 对于动态表名或列名,使用白名单校验,不能直接将用户输入拼入。
7.2 跨站脚本(XSS)防范
- 在 Web 应用中对输出到页面的内容进行 HTML 实体编码。
- 使用成熟的模板引擎(如 Thymeleaf)的自动转义功能。
7.3 敏感信息处理
- 不在日志中打印密码、Token、身份证号等敏感数据。
- 内存中的敏感数据使用
char[]而不是String,因为字符串不可变无法主动清除。 - 使用专业的加密库和算法(如 AES),切忌自己实现加密。
7.4 输入验证与输出编码
所有来自外部的输入都需要验证:长度、格式、范围。输出到不同上下文(HTML、JavaScript、SQL)时分别编码。这是安全开发的核心原则。
遵循这些最佳实践并非一日之功,但每掌握一条,你的 Java 代码质量就会提升一个台阶。将它们融入日常开发习惯,长期坚持,便能自然写出健壮、可维护且优雅的 Java 程序。