Skip to content

最佳实践

Java 开发最佳实践,涵盖命名、方法设计、异常处理、空值处理、集合使用、并发编程和日志记录等核心领域。


一、命名规范

1.1 使用有意义的名称

名称应该回答:它为什么存在、做什么事、怎么用。好的命名可以大幅减少注释的需求。

java
// ❌ 糟糕的命名
int d;                 // 经过多少天?
List<String> list;     // 什么类型的列表?
void process();        // 处理什么?

// ✅ 好的命名
int elapsedTimeInDays;
List<String> activeUserNames;
void sendEmailNotification();

1.2 避免缩写

缩写会降低可读性,增加理解成本。除非是公认的缩写(如 idurlhttp),否则应使用完整单词。

java
// ❌ 滥用缩写
String custAddr;       // customerAddress
String prodCat;        // productCategory
void calTax();         // calculateTax

// ✅ 完整单词
String customerAddress;
String productCategory;
void calculateTax();

1.3 命名风格速查

元素风格示例
类名大驼峰 (UpperCamelCase)OrderService, UserController
方法/变量小驼峰 (lowerCamelCase)getOrderById, userName
常量大写下划线 (UPPER_SNAKE)MAX_RETRY_COUNT, DEFAULT_TIMEOUT
包名全小写com.example.order.service
布尔变量is/has/can 开头isActive, hasPermission, canExecute

二、方法设计

2.1 单一职责原则

一个方法只做一件事,做好一件事。这有利于测试、维护和复用。

java
// ❌ 一个方法做了太多事
public void processOrder(Order order) {
    // 校验订单
    // 计算价格
    // 扣减库存
    // 发送通知
    // 记录日志
}

// ✅ 拆分职责
public void processOrder(Order order) {
    validateOrder(order);
    calculatePrice(order);
    deductInventory(order);
    notifyUser(order);
    logOrder(order);
}

2.2 控制方法长度

理想情况下,一个方法不超过 50 行。如果超过,说明职责过多,需要拆分。

2.3 避免布尔参数

布尔参数使方法调用含义模糊,且通常意味着方法做了两件事。

java
// ❌ 布尔参数 -- 调用方看不懂
orderService.process(order, true, false);

// ✅ 拆分为独立方法
orderService.processWithNotification(order);
orderService.processWithoutNotification(order);

// ✅ 或使用枚举
orderService.process(order, ProcessingMode.ASYNC);

2.4 参数数量控制

方法参数不宜超过 4 个。超过时考虑封装为参数对象。

java
// ❌ 参数过多
public User createUser(String name, String email, String phone, 
                       String address, int age, String department) { }

// ✅ 封装为对象
public User createUser(CreateUserRequest request) { }

@Data
class CreateUserRequest {
    private String name;
    private String email;
    private String phone;
    private String address;
    private int age;
    private String department;
}

三、异常处理最佳实践

3.1 永远不要吞掉异常

java
// ❌ 吞掉异常 -- 问题无法追踪
try {
    doSomething();
} catch (Exception e) {
    // 空 catch 块
}

// ❌ 只打印不处理
try {
    doSomething();
} catch (Exception e) {
    e.printStackTrace();
}

// ✅ 正确处理
try {
    doSomething();
} catch (Exception e) {
    log.error("Failed to do something, userId={}", userId, e);
    throw new ServiceException("操作失败", e);
}

3.2 使用具体的异常类型

java
// ❌ 捕获所有异常
try {
    readFile();
} catch (Exception e) { }

// ✅ 捕获具体异常
try {
    readFile();
} catch (FileNotFoundException e) {
    log.warn("文件不存在: {}", filePath);
} catch (IOException e) {
    log.error("读取文件异常", e);
    throw new UncheckedIOException(e);
}

3.3 try-with-resources 自动关闭资源

java
// ❌ 手动关闭 -- 繁琐且容易遗漏
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader(path));
    // 使用 reader
} catch (IOException e) {
    log.error("读取失败", e);
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            log.error("关闭失败", e);
        }
    }
}

// ✅ try-with-resources -- JDK 7+ 自动关闭
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
    // 使用 reader
} catch (IOException e) {
    log.error("读取失败", e);
}

3.4 异常分层处理

java
// Controller 层:统一异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ServiceException.class)
    public Result<?> handleServiceException(ServiceException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("系统繁忙,请稍后重试");
    }
}

3.5 异常处理核心原则

原则说明
不吞异常异常必须被记录或传播,不能静默忽略
具体异常捕获特定异常类型,不要用 catch(Exception)
早抛晚捕尽早抛出异常,在合适层级统一捕获处理
转换异常底层异常转换为业务异常,保护调用方
日志完整记录异常时带上关键上下文信息

四、空值处理

4.1 Optional 的使用

java
// ❌ 防御式 null 检查
public String getCity(User user) {
    if (user != null) {
        Address address = user.getAddress();
        if (address != null) {
            return address.getCity();
        }
    }
    return "未知";
}

// ✅ Optional 链式处理
public String getCity(User user) {
    return Optional.ofNullable(user)
        .map(User::getAddress)
        .map(Address::getCity)
        .orElse("未知");
}

4.2 Optional 使用原则

java
// ❌ 不要将 Optional 作为字段或参数
public class User {
    private Optional<String> email;  // 错误
}
public void process(Optional<String> param) { }  // 错误

// ✅ Optional 仅用于返回值
public Optional<User> findById(Long id) {
    return Optional.ofNullable(userMap.get(id));
}

4.3 Objects.requireNonNull

java
// ✅ 在构造函数和方法中对参数进行非空校验
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = Objects.requireNonNull(userRepository, 
            "userRepository must not be null");
    }

    public User findById(Long id) {
        Objects.requireNonNull(id, "id must not be null");
        return userRepository.findById(id);
    }
}

4.4 @Nullable 和 @NonNull 注解

java
// ✅ 使用注解明确空值语义
public interface UserRepository {
    @Nullable
    User findByName(String name);

    @NonNull
    User findById(@NonNull Long id);
}

五、集合最佳实践

5.1 初始化时指定容量

java
// ❌ 不指定容量 -- 可能多次扩容
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    list.add("item" + i);
}

// ✅ 指定容量 -- 避免扩容开销
List<String> list = new ArrayList<>(10000);

5.2 使用 isEmpty() 而非 size() == 0

java
// ❌ size() == 0
if (list.size() == 0) { }

// ✅ isEmpty() -- 更语义化,某些集合实现可能更高效
if (list.isEmpty()) { }

5.3 返回空集合而非 null

java
// ❌ 返回 null -- 调用方必须做 null 检查
public List<Order> getOrders() {
    if (noOrders) {
        return null;
    }
    return orders;
}

// ✅ 返回空集合
public List<Order> getOrders() {
    return orders != null ? orders : Collections.emptyList();
}

5.4 选择合适的集合类型

场景推荐集合原因
频繁随机访问ArrayListO(1) 索引访问
频繁增删LinkedListO(1) 头尾操作
去重HashSetO(1) 查找
排序去重TreeSet有序
键值对HashMapO(1) 查找
排序键值对TreeMap有序
线程安全ConcurrentHashMap分段锁

5.5 使用增强 for 和 Stream API

java
// ❌ 传统 for 循环
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

// ✅ 增强 for 循环
for (String item : list) {
    System.out.println(item);
}

// ✅ Stream API 批量操作
List<String> upperNames = names.stream()
    .filter(Objects::nonNull)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

六、并发编程最佳实践

6.1 使用并发集合

java
// ❌ 非线程安全的集合
Map<String, String> map = new HashMap<>();      // 并发问题
List<String> list = new ArrayList<>();           // 并发问题

// ✅ 并发集合
Map<String, String> map = new ConcurrentHashMap<>();
List<String> list = new CopyOnWriteArrayList<>();

6.2 使用 ExecutorService 而非手动创建线程

java
// ❌ 手动创建线程 -- 不可控
new Thread(() -> {
    doWork();
}).start();

// ✅ 使用线程池 -- 可管理、可复用
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100),
    new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

executor.submit(() -> doWork());

6.3 始终设置线程名称

java
// ✅ 使用 ThreadFactory 统一命名
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
    .setNameFormat("async-task-%d")
    .setDaemon(true)
    .build();

// ✅ 或使用线程池自定义工厂
new ThreadPoolExecutor(
    corePoolSize, maxPoolSize, keepAliveTime, unit,
    workQueue,
    r -> new Thread(r, "biz-thread-" + threadNumber.getAndIncrement())
);

6.4 使用 volatile 保证可见性

java
public class TaskRunner {
    // ✅ volatile 保证多线程可见性
    private volatile boolean running = true;

    public void stop() {
        running = false;
    }

    public void run() {
        while (running) {
            // 执行任务
        }
    }
}

6.5 锁的粒度要细

java
// ❌ 方法级别的锁 -- 粒度太粗
public synchronized void processOrder(Order order) {
    // 大量非临界区代码
    validateOrder(order);
    // 只有这里有竞态条件
    updateInventory(order);
    sendNotification(order);
}

// ✅ 细粒度锁
public void processOrder(Order order) {
    validateOrder(order);
    synchronized (inventoryLock) {
        updateInventory(order);
    }
    sendNotification(order);
}

七、日志记录最佳实践

7.1 使用参数化日志

java
// ❌ 字符串拼接 -- 即使日志级别不满足,也会执行拼接
log.debug("User " + user.getName() + " logged in at " + now);

// ✅ 参数化日志 -- 只有需要时才进行字符串拼接
log.debug("User {} logged in at {}", user.getName(), now);

7.2 合适的日志级别

级别使用场景示例
ERROR系统错误,需要人工介入数据库连接失败、第三方接口不可用
WARN潜在问题,可以继续运行配置缺失使用默认值、重试成功
INFO重要业务流程节点订单创建、用户登录、定时任务开始/结束
DEBUG调试信息,生产环境默认关闭方法入参出参、中间变量值
TRACE非常详细的追踪信息逐行执行路径、SQL 参数绑定

7.3 不要记录敏感数据

java
// ❌ 记录敏感信息
log.info("用户登录成功,用户名={},密码={}", username, password);

// ✅ 脱敏处理
log.info("用户登录成功,用户名={},密码={}", username, maskPassword(password));

// ✅ 或直接不记录
log.info("用户登录成功,用户名={}", username);

7.4 异常日志必须记录堆栈

java
// ❌ 只记录了异常信息
catch (Exception e) {
    log.error("处理失败: {}", e.getMessage());  // 没有堆栈
}

// ✅ 记录完整堆栈
catch (Exception e) {
    log.error("处理失败,orderId={}", orderId, e);  // 最后一个参数是异常对象
}

八、可读性最佳实践

8.1 使用枚举替代魔法数字

java
// ❌ 魔法数字
if (order.getStatus() == 1) {  // 1 是什么?
    cancelOrder(order);
}

// ✅ 枚举
public enum OrderStatus {
    CREATED(1), PAID(2), SHIPPED(3), CANCELLED(4);

    private final int code;
    // ...
}

if (order.getStatus() == OrderStatus.PAID) {
    cancelOrder(order);
}

8.2 避免深层嵌套 -- 使用卫语句

java
// ❌ 深层嵌套
public void process(User user) {
    if (user != null) {
        if (user.isActive()) {
            if (user.hasPermission("admin")) {
                // 核心逻辑
            }
        }
    }
}

// ✅ 卫语句 -- 提前返回
public void process(User user) {
    if (user == null) {
        return;
    }
    if (!user.isActive()) {
        return;
    }
    if (!user.hasPermission("admin")) {
        return;
    }
    // 核心逻辑
}

8.3 使用 Lombok 减少样板代码

java
// ❌ 大量样板代码
public class User {
    private String name;
    private int age;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    // equals, hashCode, toString...
}

// ✅ Lombok 注解
@Data
@AllArgsConstructor
@Builder
public class User {
    private String name;
    private int age;
}

面试常见问题

Q1: 为什么推荐使用 try-with-resources?

A: try-with-resources 是 Java 7 引入的语法糖,实现了 AutoCloseable 接口的资源会在 try 块结束后自动调用 close() 方法。它避免了手动在 finally 块中关闭资源可能导致的异常覆盖问题,代码更简洁且不易遗漏资源关闭。

Q2: Optional 可以替代 null 检查吗?使用时有什么注意事项?

A: Optional 可以优雅地处理可能为 null 的返回值,但不能完全替代所有 null 检查。注意事项:(1) 不要将 Optional 作为类的字段或方法参数;(2) 不要调用 Optional.get() 而不先检查 isPresent();(3) 优先使用 orElse()orElseGet()orElseThrow() 等安全方法;(4) 序列化场景(如 RPC、JSON)中 Optional 可能不兼容。

Q3: 为什么不推荐在循环中拼接字符串?

A: String 是不可变对象,每次拼接都会创建新的 String 对象,导致大量临时对象和 GC 压力。循环中应使用 StringBuilder(线程不安全,性能好)或 StringBuffer(线程安全)。

Q4: ExecutorService 相比直接 new Thread 有什么优势?

A: (1) 线程复用,减少创建/销毁开销;(2) 可控制最大并发数,防止资源耗尽;(3) 提供任务队列、拒绝策略等管理能力;(4) 支持定时任务和有返回值的任务(Future/Callable);(5) 便于统一监控和管理线程生命周期。

Q5: 日志输出为什么推荐使用参数化占位符?

A: 参数化占位符(如 {})可以实现延迟求值——只有当对应日志级别被启用时,才会执行字符串拼接。如果直接使用 + 拼接,无论日志是否输出,拼接操作都会执行,造成不必要的性能开销,尤其在循环或高频调用场景中。