java-best-practices by clostaunau/holiday-card
npx skills add https://github.com/clostaunau/holiday-card --skill java-best-practices本技能为 Java 开发提供全面的最佳实践,可作为代码审查和架构决策时的参考指南。它涵盖了 SOLID 原则、DRY、整洁代码、Java 特有模式、测试策略和常见反模式。
何时使用此技能:
高质量的 Java 代码对于构建可维护、可扩展和健壮的应用程序至关重要。本技能记录了行业标准实践,强调:
此技能设计为由 uncle-duke-java 代理在代码审查期间以及开发人员在编写 Java 代码时参考。
所需知识:
所需工具:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
预期的项目结构:
project/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/
│ │ │ ├── model/
│ │ │ ├── service/
│ │ │ ├── repository/
│ │ │ ├── controller/
│ │ │ └── util/
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── java/
│ └── com/example/
├── pom.xml (or build.gradle)
└── README.md
规则: 一个类应该只有一个引起变化的原因。每个类应该有一个单一、明确定义的职责。
为何重要: 具有多个职责的类更难以理解、测试和维护。对一个职责的更改可能会影响其他职责。
❌ 不良实践 - 多个职责:
// 这个类违反了 SRP:它处理用户数据、验证、持久化和电子邮件
public class User {
private String email;
private String password;
// 职责 1:数据验证
public boolean isValid() {
return email != null && email.contains("@")
&& password != null && password.length() >= 8;
}
// 职责 2:数据库操作
public void save() {
Connection conn = DriverManager.getConnection("jdbc:...");
PreparedStatement ps = conn.prepareStatement("INSERT INTO users...");
ps.setString(1, email);
ps.setString(2, password);
ps.executeUpdate();
}
// 职责 3:电子邮件操作
public void sendWelcomeEmail() {
EmailService.send(email, "Welcome!", "Welcome to our app");
}
// 职责 4:密码加密
public void encryptPassword() {
this.password = BCrypt.hashpw(password, BCrypt.gensalt());
}
}
问题:
✅ 良好实践 - 单一职责:
// 职责:保存用户数据
public class User {
private final String email;
private final String passwordHash;
public User(String email, String passwordHash) {
this.email = email;
this.passwordHash = passwordHash;
}
public String getEmail() { return email; }
public String getPasswordHash() { return passwordHash; }
}
// 职责:验证用户数据
public class UserValidator {
public ValidationResult validate(String email, String password) {
List<String> errors = new ArrayList<>();
if (email == null || !email.contains("@")) {
errors.add("Invalid email format");
}
if (password == null || password.length() < 8) {
errors.add("Password must be at least 8 characters");
}
return new ValidationResult(errors.isEmpty(), errors);
}
}
// 职责:持久化用户数据
public class UserRepository {
private final DataSource dataSource;
public UserRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public void save(User user) {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO users (email, password_hash) VALUES (?, ?)")) {
ps.setString(1, user.getEmail());
ps.setString(2, user.getPasswordHash());
ps.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to save user", e);
}
}
}
// 职责:发送电子邮件
public class EmailService {
public void sendWelcomeEmail(User user) {
send(user.getEmail(), "Welcome!", "Welcome to our app");
}
private void send(String to, String subject, String body) {
// Email sending logic
}
}
// 职责:哈希密码
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.hashpw(rawPassword, BCrypt.gensalt());
}
public boolean matches(String rawPassword, String encodedPassword) {
return BCrypt.checkpw(rawPassword, encodedPassword);
}
}
好处:
规则: 软件实体(类、模块、函数)应该对扩展开放,对修改关闭。
为何重要: 你应该能够在不更改现有代码的情况下添加新功能,从而降低破坏现有功能的风险。
❌ 不良实践 - 违反 OCP:
public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
// Process credit card payment
System.out.println("Processing credit card payment: $" + amount);
} else if (paymentType.equals("PAYPAL")) {
// Process PayPal payment
System.out.println("Processing PayPal payment: $" + amount);
} else if (paymentType.equals("BITCOIN")) {
// Process Bitcoin payment
System.out.println("Processing Bitcoin payment: $" + amount);
}
// Adding new payment method requires modifying this class!
}
}
问题:
✅ 良好实践 - 遵循 OCP:
// 抽象支付接口
public interface PaymentMethod {
void process(double amount);
}
// 具体实现
public class CreditCardPayment implements PaymentMethod {
@Override
public void process(double amount) {
System.out.println("Processing credit card payment: $" + amount);
// Credit card specific logic
}
}
public class PayPalPayment implements PaymentMethod {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
// PayPal specific logic
}
}
public class BitcoinPayment implements PaymentMethod {
@Override
public void process(double amount) {
System.out.println("Processing Bitcoin payment: $" + amount);
// Bitcoin specific logic
}
}
// 处理器委托给支付方法
public class PaymentProcessor {
public void processPayment(PaymentMethod paymentMethod, double amount) {
paymentMethod.process(amount);
}
}
// 用法
PaymentProcessor processor = new PaymentProcessor();
processor.processPayment(new CreditCardPayment(), 100.0);
processor.processPayment(new PayPalPayment(), 50.0);
// 添加新的支付方法:只需创建新类,无需修改!
public class ApplePayPayment implements PaymentMethod {
@Override
public void process(double amount) {
System.out.println("Processing Apple Pay payment: $" + amount);
}
}
好处:
✅ 高级示例 - 折扣策略:
// 策略接口
public interface DiscountStrategy {
double applyDiscount(double price);
}
// 具体策略
public class NoDiscount implements DiscountStrategy {
@Override
public double applyDiscount(double price) {
return price;
}
}
public class PercentageDiscount implements DiscountStrategy {
private final double percentage;
public PercentageDiscount(double percentage) {
this.percentage = percentage;
}
@Override
public double applyDiscount(double price) {
return price * (1 - percentage / 100);
}
}
public class FixedAmountDiscount implements DiscountStrategy {
private final double amount;
public FixedAmountDiscount(double amount) {
this.amount = amount;
}
@Override
public double applyDiscount(double price) {
return Math.max(0, price - amount);
}
}
// 上下文使用策略
public class PriceCalculator {
private final DiscountStrategy discountStrategy;
public PriceCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public double calculateFinalPrice(double originalPrice) {
return discountStrategy.applyDiscount(originalPrice);
}
}
// 用法
PriceCalculator calc1 = new PriceCalculator(new PercentageDiscount(10));
double price1 = calc1.calculateFinalPrice(100); // 90.0
PriceCalculator calc2 = new PriceCalculator(new FixedAmountDiscount(15));
double price2 = calc2.calculateFinalPrice(100); // 85.0
规则: 超类的对象应该可以被子类的对象替换,而不会破坏应用程序。子类型必须可以替换其基类型。
为何重要: 违反 LSP 会导致意外行为并破坏多态性。
❌ 不良实践 - 违反 LSP:
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
// Square 违反 LSP,因为它改变了 setter 的行为
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 副作用!
}
@Override
public void setHeight(int height) {
this.width = height; // 副作用!
this.height = height;
}
}
// 这个测试对 Rectangle 有效,但对 Square 失败
public void testRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
assertEquals(20, rect.getArea()); // 对 Square 失败!(结果是 25 而不是 20)
}
问题:
✅ 良好实践 - 遵循 LSP:
// 形状的通用接口
public interface Shape {
int getArea();
}
// Rectangle 实现
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
public int getWidth() { return width; }
public int getHeight() { return height; }
}
// Square 实现(不继承自 Rectangle)
public class Square implements Shape {
private final int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
public int getSide() { return side; }
}
// 适用于任何 Shape
public int calculateTotalArea(List<Shape> shapes) {
return shapes.stream()
.mapToInt(Shape::getArea)
.sum();
}
好处:
✅ 良好实践 - 维护契约:
public interface BankAccount {
// 前置条件:amount > 0
// 后置条件:余额增加 amount
void deposit(double amount);
// 前置条件:amount > 0 且 amount <= balance
// 后置条件:余额减少 amount
void withdraw(double amount) throws InsufficientFundsException;
double getBalance();
}
public class SavingsAccount implements BankAccount {
private double balance;
@Override
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (amount > balance) {
throw new InsufficientFundsException();
}
balance -= amount;
}
@Override
public double getBalance() {
return balance;
}
}
// 子类维护契约(LSP)
public class CheckingAccount implements BankAccount {
private double balance;
private final double overdraftLimit;
public CheckingAccount(double overdraftLimit) {
this.overdraftLimit = overdraftLimit;
}
@Override
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount; // 相同的后置条件
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
// 可以弱化前置条件(允许透支)但不能强化
if (amount > balance + overdraftLimit) {
throw new InsufficientFundsException();
}
balance -= amount; // 相同的后置条件
}
@Override
public double getBalance() {
return balance;
}
}
规则: 客户端不应该被迫依赖它们不使用的接口。多个特定接口优于一个通用接口。
为何重要: 庞大的接口强制实现提供它们不需要的方法,导致空实现和紧耦合。
❌ 不良实践 - 臃肿的接口:
// 臃肿的接口强制所有实现提供所有方法
public interface Worker {
void work();
void eat();
void sleep();
void getSalary();
void attendMeeting();
}
// Robot 不吃不睡,但被迫实现这些方法
public class RobotWorker implements Worker {
@Override
public void work() {
System.out.println("Robot working");
}
@Override
public void eat() {
// 对机器人来说没有意义!
throw new UnsupportedOperationException("Robots don't eat");
}
@Override
public void sleep() {
// 对机器人来说没有意义!
throw new UnsupportedOperationException("Robots don't sleep");
}
@Override
public void getSalary() {
throw new UnsupportedOperationException("Robots don't get paid");
}
@Override
public void attendMeeting() {
System.out.println("Robot attending meeting");
}
}
问题:
✅ 良好实践 - 隔离的接口:
// 隔离的接口 - 客户端仅依赖它们需要的东西
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface Payable {
void getSalary();
}
public interface MeetingAttendee {
void attendMeeting();
}
// Human 实现相关接口
public class HumanWorker implements Workable, Eatable, Sleepable, Payable, MeetingAttendee {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
@Override
public void sleep() {
System.out.println("Human sleeping");
}
@Override
public void getSalary() {
System.out.println("Human receiving salary");
}
@Override
public void attendMeeting() {
System.out.println("Human attending meeting");
}
}
// Robot 仅实现相关接口
public class RobotWorker implements Workable, MeetingAttendee {
@Override
public void work() {
System.out.println("Robot working");
}
@Override
public void attendMeeting() {
System.out.println("Robot attending meeting");
}
}
好处:
规则: 高层模块不应该依赖低层模块。两者都应该依赖抽象。抽象不应该依赖细节。细节应该依赖抽象。
为何重要: DIP 解耦代码,使其更灵活、可测试和可维护。
❌ 不良实践 - 高层依赖低层:
// 低层模块
public class MySQLDatabase {
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
// 高层模块依赖于具体的低层模块
public class UserService {
private MySQLDatabase database; // 具体依赖!
public UserService() {
this.database = new MySQLDatabase(); // 紧耦合!
}
public void createUser(String userData) {
// 业务逻辑
database.save(userData);
}
}
问题:
✅ 良好实践 - 两者都依赖抽象:
// 抽象
public interface Database {
void save(String data);
}
// 低层模块依赖抽象
public class MySQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
public class PostgreSQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to PostgreSQL: " + data);
}
}
public class MongoDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to MongoDB: " + data);
}
}
// 高层模块依赖抽象
public class UserService {
private final Database database; // 抽象!
// 通过构造函数注入依赖
public UserService(Database database) {
this.database = database;
}
public void createUser(String userData) {
// 业务逻辑
database.save(userData);
}
}
// 用法 - 客户端选择实现
Database db = new MySQLDatabase();
UserService service = new UserService(db);
service.createUser("John Doe");
// 易于切换实现
Database postgresDb = new PostgreSQLDatabase();
UserService postgresService = new UserService(postgresDb);
// 易于使用模拟对象测试
Database mockDb = mock(Database.class);
UserService testService = new UserService(mockDb);
好处:
Spring Framework 建立在 SOLID 原则之上,特别是依赖倒置原则。
✅ Spring DI 示例:
// 抽象
public interface UserRepository {
User findById(Long id);
void save(User user);
}
// 实现
@Repository
public class JpaUserRepository implements UserRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public User findById(Long id) {
return entityManager.find(User.class, id);
}
@Override
public void save(User user) {
entityManager.persist(user);
}
}
// 服务依赖于抽象
@Service
public class UserService {
private final UserRepository userRepository;
// 构造函数注入(推荐)
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(Long id) {
return userRepository.findById(id);
}
}
// 控制器依赖于服务抽象
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.getUser(id);
return ResponseEntity.ok(user);
}
}
Spring DI 最佳实践:
@Qualifier规则: 系统中的每个知识片段都必须有一个单一、明确、权威的表示。
为何重要: 重复会导致不一致、更难维护和更多错误。
❌ 不良实践 - 明显的重复:
public class OrderService {
public void processOnlineOrder(Order order) {
// 验证
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getTotalAmount() <= 0) {
throw new IllegalArgumentException("Order total must be positive");
}
// 处理
System.out.println("Processing online order: " + order.getId());
order.setStatus(OrderStatus.PROCESSING);
saveOrder(order);
}
public void processPhoneOrder(Order order) {
// 相同的验证 - 重复!
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getTotalAmount() <= 0) {
throw new IllegalArgumentException("Order total must be positive");
}
// 处理
System.out.println("Processing phone order: " + order.getId());
order.setStatus(OrderStatus.PROCESSING);
saveOrder(order);
}
}
✅ 良好实践 - 提取公共逻辑:
public class OrderService {
public void processOnlineOrder(Order order) {
validateOrder(order);
processOrder(order, "online");
}
public void processPhoneOrder(Order order) {
validateOrder(order);
processOrder(order, "phone");
}
private void validateOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getTotalAmount() <= 0) {
throw new IllegalArgumentException("Order total must be positive");
}
}
private void processOrder(Order order, String type) {
System.out.println("Processing " + type + " order: " + order.getId());
order.setStatus(OrderStatus.PROCESSING);
saveOrder(order);
}
}
✅ 为可重用逻辑创建工具类:
public final class StringUtils {
private StringUtils() {
// 防止实例化
}
public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
public static String capitalize(String str) {
if (isBlank(str)) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
}
public static String truncate(String str, int maxLength) {
if (str == null || str.length() <= maxLength) {
return str;
}
return str.substring(0, maxLength) + "...";
}
}
// 用法
if (StringUtils.isBlank(username)) {
throw new ValidationException("Username is required");
}
String displayName = StringUtils.capitalize(name);
✅ 使用泛型避免重复:
// 而不是为不同类型创建单独的类
public class GenericRepository<T, ID> {
private final Class<T> entityClass;
@PersistenceContext
private EntityManager entityManager;
public GenericRepository(Class<T> entityClass) {
this.entityClass = entityClass;
}
public Optional<T> findById(ID id) {
T entity = entityManager.find(entityClass, id);
return Optional.ofNullable(entity);
}
public List<T> findAll() {
CriteriaQuery<T> query = entityManager.getCriteriaBuilder()
.createQuery(entityClass);
query.select(query.from(entityClass));
return entityManager.createQuery(query).getResultList();
}
public void save(T entity) {
entityManager.persist(entity);
}
public void delete(T entity) {
entityManager.remove(entity);
}
}
// 具体仓库扩展泛型仓库
@Repository
public class UserRepository extends GenericRepository<User, Long> {
public UserRepository() {
super(User.class);
}
// 添加特定于 User 的查询
public Optional<User> findByEmail(String email) {
// Custom query
}
}
规则: 名称应揭示意图、可发音且可搜索。
❌ 不良命名:
int d; // 经过的天数
String yyyymmdd;
List<int[]> list1;
public void getData() {
// 什么数据?
}
✅ 良好命名:
int elapsedTimeInDays;
String formattedDate;
List<Customer> activeCustomers;
public Customer getCustomerById(Long customerId) {
// 清楚这个方法做什么
}
// 类:PascalCase,名词
public class CustomerService { }
public class OrderRepository { }
// 接口:PascalCase,通常是形容词或名词
public interface Serializable { }
public interface UserRepository { }
// 方法:camelCase,动词
public void calculateTotal() { }
public Customer findCustomerById(Long id) { }
// 变量:camelCase,名词
String customerName;
int orderCount;
boolean isActive;
// 常量:UPPER_SNAKE_CASE
public static final int MAX_RETRY_COUNT = 3;
public static final String DEFAULT_ENCODING = "UTF-8";
// 包:小写,点分隔
package com.example.service;
package com.example.repository;
// 布尔方法/变量:is, has, can
boolean isValid();
boolean hasPermission();
boolean canExecute();
规则: 函数应该小而只做一件事。每个方法的目标是 5-20 行。
❌ 不良实践 - 庞大、复杂的方法:
public void processOrder(Order order) {
// 验证
if (order == null) throw new IllegalArgumentException();
if (order.getItems().isEmpty()) throw new IllegalArgumentException();
// 计算总计
double total = 0;
for (OrderItem item : order.getItems()) {
double itemPrice = item.getPrice();
int quantity = item.getQuantity();
double discount = item.getDiscount();
total += (itemPrice * quantity) * (1 - discount);
}
order.setTotal(total);
// 应用优惠券
if (order.getCoupon() != null) {
String couponCode = order.getCoupon().getCode();
if (couponCode.startsWith("SAVE")) {
total *= 0.9;
} else if (couponCode.startsWith("BIG")) {
total *= 0.8;
}
order.setTotal(total);
}
// 检查库存
for (OrderItem item : order.getItems()) {
int available = inventoryService.getAvailableQuantity(item.getProductId());
if (available < item.getQuantity()) {
throw new InsufficientInventoryException();
}
}
// 保存订单
orderRepository.save(order);
// 发送电子邮件
emailService.send(order.getCustomer().getEmail(), "Order Confirmation",
"Your order " + order.getId() + " has been confirmed");
// 更新库存
for (OrderItem item : order.getItems()) {
inventoryService.decrementQuantity(item.getProductId(), item.getQuantity());
}
}
✅ 良好实践 - 小而专注的方法:
public void processOrder(Order order) {
validateOrder(order);
calculateOrderTotal(order);
applyCouponDiscount(order);
checkInventoryAvailability(order);
saveOrder(order);
sendConfirmationEmail(order);
updateInventory(order);
}
private void validateOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}
}
private void calculateOrderTotal(Order order) {
double total = order.getItems().stream()
.mapToDouble(this::calculateItemTotal)
.sum();
order.setTotal(total);
}
private double calculateItemTotal(OrderItem item) {
return item.getPrice() * item.getQuantity() * (1 - item.getDiscount());
}
private void applyCouponDiscount(Order order) {
if (order.getCoupon() == null) {
return;
}
double discountMultiplier = getDiscountMultiplier(order.getCoupon());
order.setTotal(order.getTotal() * discountMultiplier);
}
private double getDiscountMultiplier(Coupon coupon) {
String code = coupon.getCode();
if (code.startsWith("SAVE")) return 0.9;
if (code.startsWith("BIG")) return 0.8;
return 1.0;
}
private void checkInventoryAvailability(Order order) {
for (OrderItem item : order.getItems()) {
int available = inventoryService.getAvailableQuantity(item.getProductId());
if (available < item.getQuantity()) {
throw new InsufficientInventoryException(
"Product " + item.getProductId() + " has insufficient inventory");
}
}
}
private void saveOrder(Order order) {
orderRepository.save(order);
}
private void sendConfirmationEmail(Order order) {
String email = order.getCustomer().getEmail();
String subject = "Order Confirmation";
String body = String.format("Your order %s has been confirmed", order.getId());
emailService.send(email, subject, body);
}
private void updateInventory(Order order) {
order.getItems().forEach(item ->
inventoryService.decrementQuantity(item.getProductId(), item.getQuantity())
);
}
好处:
规则: 代码应该自解释。注释应该解释 WHY,而不是 WHAT。
❌ 不良注释:
// 将标志设置为 true
isActive = true;
// 遍历用户
for (User user : users) {
// 检查用户是否活跃
if (user.isActive()) {
// 添加到列表
activeUsers.add(user);
}
}
// 这是 UserService 类
public class UserService {
}
✅ 良好注释:
// 不需要注释 - 代码是自解释的
isActive = true;
List<User> activeUsers = users.stream()
.filter(User::isActive)
.collect(Collectors.toList());
// 良好:解释 WHY,而不是 WHAT
// 我们使用指数退避来避免在多次失败后压垮外部 API
// (断路器模式)
private int calculateRetryDelay(int attemptNumber) {
return (int) Math.pow(2, attemptNumber) * 1000;
}
// 良好:公共 API 的 JavaDoc
/**
* 在账户之间原子地转移资金。
*
* @param fromAccount 源账户(必须有足够的余额)
* @param toAccount 目标账户
* @param amount 要转移的金额(必须为正数)
* @throws InsufficientFundsException 如果源账户资金不足
* @throws IllegalArgumentException 如果金额为负数或零
*/
public void transferFunds(Account fromAccount, Account toAccount, double amount)
throws InsufficientFundsException {
// 实现
}
// 良好:解释不明显的业务规则
// 根据 IRS 法规 2024-15,税费计算不包括运费但包括折扣调整
double taxableAmount = subtotal - discount;
何时注释:
何时不注释:
规则: 对异常情况使用异常。不要使用异常进行控制流。
❌ 不良错误处理:
// 使用异常进行控制流
public User findUser(Long id) {
try {
return userRepository.findById(id);
} catch (NotFoundException e) {
return null; // 吞掉异常
}
}
// 捕获通用 Exception
public void processData(String data) {
try {
// 复杂逻辑
} catch
This skill provides comprehensive best practices for Java development, serving as a reference guide during code reviews and architectural decisions. It covers SOLID principles, DRY, Clean Code, Java-specific patterns, testing strategies, and common anti-patterns.
When to use this skill:
High-quality Java code is essential for building maintainable, scalable, and robust applications. This skill documents industry-standard practices that emphasize:
This skill is designed to be referenced by the uncle-duke-java agent during code reviews and by developers when writing Java code.
Required Knowledge:
Required Tools:
Expected Project Structure:
project/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/
│ │ │ ├── model/
│ │ │ ├── service/
│ │ │ ├── repository/
│ │ │ ├── controller/
│ │ │ └── util/
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── java/
│ └── com/example/
├── pom.xml (or build.gradle)
└── README.md
Rule: A class should have only one reason to change. Each class should have a single, well-defined responsibility.
Why it matters: Classes with multiple responsibilities are harder to understand, test, and maintain. Changes to one responsibility can affect the others.
❌ Bad - Multiple Responsibilities:
// This class violates SRP: it handles user data, validation, persistence, and email
public class User {
private String email;
private String password;
// Responsibility 1: Data validation
public boolean isValid() {
return email != null && email.contains("@")
&& password != null && password.length() >= 8;
}
// Responsibility 2: Database operations
public void save() {
Connection conn = DriverManager.getConnection("jdbc:...");
PreparedStatement ps = conn.prepareStatement("INSERT INTO users...");
ps.setString(1, email);
ps.setString(2, password);
ps.executeUpdate();
}
// Responsibility 3: Email operations
public void sendWelcomeEmail() {
EmailService.send(email, "Welcome!", "Welcome to our app");
}
// Responsibility 4: Password encryption
public void encryptPassword() {
this.password = BCrypt.hashpw(password, BCrypt.gensalt());
}
}
Issues:
✅ Good - Single Responsibility:
// Responsibility: Hold user data
public class User {
private final String email;
private final String passwordHash;
public User(String email, String passwordHash) {
this.email = email;
this.passwordHash = passwordHash;
}
public String getEmail() { return email; }
public String getPasswordHash() { return passwordHash; }
}
// Responsibility: Validate user data
public class UserValidator {
public ValidationResult validate(String email, String password) {
List<String> errors = new ArrayList<>();
if (email == null || !email.contains("@")) {
errors.add("Invalid email format");
}
if (password == null || password.length() < 8) {
errors.add("Password must be at least 8 characters");
}
return new ValidationResult(errors.isEmpty(), errors);
}
}
// Responsibility: Persist user data
public class UserRepository {
private final DataSource dataSource;
public UserRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public void save(User user) {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO users (email, password_hash) VALUES (?, ?)")) {
ps.setString(1, user.getEmail());
ps.setString(2, user.getPasswordHash());
ps.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to save user", e);
}
}
}
// Responsibility: Send emails
public class EmailService {
public void sendWelcomeEmail(User user) {
send(user.getEmail(), "Welcome!", "Welcome to our app");
}
private void send(String to, String subject, String body) {
// Email sending logic
}
}
// Responsibility: Hash passwords
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.hashpw(rawPassword, BCrypt.gensalt());
}
public boolean matches(String rawPassword, String encodedPassword) {
return BCrypt.checkpw(rawPassword, encodedPassword);
}
}
Benefits:
Rule: Software entities (classes, modules, functions) should be open for extension but closed for modification.
Why it matters: You should be able to add new functionality without changing existing code, reducing the risk of breaking existing features.
❌ Bad - Violates OCP:
public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
// Process credit card payment
System.out.println("Processing credit card payment: $" + amount);
} else if (paymentType.equals("PAYPAL")) {
// Process PayPal payment
System.out.println("Processing PayPal payment: $" + amount);
} else if (paymentType.equals("BITCOIN")) {
// Process Bitcoin payment
System.out.println("Processing Bitcoin payment: $" + amount);
}
// Adding new payment method requires modifying this class!
}
}
Issues:
✅ Good - Follows OCP:
// Abstract payment interface
public interface PaymentMethod {
void process(double amount);
}
// Concrete implementations
public class CreditCardPayment implements PaymentMethod {
@Override
public void process(double amount) {
System.out.println("Processing credit card payment: $" + amount);
// Credit card specific logic
}
}
public class PayPalPayment implements PaymentMethod {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
// PayPal specific logic
}
}
public class BitcoinPayment implements PaymentMethod {
@Override
public void process(double amount) {
System.out.println("Processing Bitcoin payment: $" + amount);
// Bitcoin specific logic
}
}
// Processor delegates to payment method
public class PaymentProcessor {
public void processPayment(PaymentMethod paymentMethod, double amount) {
paymentMethod.process(amount);
}
}
// Usage
PaymentProcessor processor = new PaymentProcessor();
processor.processPayment(new CreditCardPayment(), 100.0);
processor.processPayment(new PayPalPayment(), 50.0);
// Adding new payment method: just create new class, no modification needed!
public class ApplePayPayment implements PaymentMethod {
@Override
public void process(double amount) {
System.out.println("Processing Apple Pay payment: $" + amount);
}
}
Benefits:
✅ Advanced Example - Discount Strategies:
// Strategy interface
public interface DiscountStrategy {
double applyDiscount(double price);
}
// Concrete strategies
public class NoDiscount implements DiscountStrategy {
@Override
public double applyDiscount(double price) {
return price;
}
}
public class PercentageDiscount implements DiscountStrategy {
private final double percentage;
public PercentageDiscount(double percentage) {
this.percentage = percentage;
}
@Override
public double applyDiscount(double price) {
return price * (1 - percentage / 100);
}
}
public class FixedAmountDiscount implements DiscountStrategy {
private final double amount;
public FixedAmountDiscount(double amount) {
this.amount = amount;
}
@Override
public double applyDiscount(double price) {
return Math.max(0, price - amount);
}
}
// Context uses strategy
public class PriceCalculator {
private final DiscountStrategy discountStrategy;
public PriceCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public double calculateFinalPrice(double originalPrice) {
return discountStrategy.applyDiscount(originalPrice);
}
}
// Usage
PriceCalculator calc1 = new PriceCalculator(new PercentageDiscount(10));
double price1 = calc1.calculateFinalPrice(100); // 90.0
PriceCalculator calc2 = new PriceCalculator(new FixedAmountDiscount(15));
double price2 = calc2.calculateFinalPrice(100); // 85.0
Rule: Objects of a superclass should be replaceable with objects of a subclass without breaking the application. Subtypes must be substitutable for their base types.
Why it matters: Violating LSP leads to unexpected behavior and breaks polymorphism.
❌ Bad - Violates LSP:
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
// Square violates LSP because it changes behavior of setters
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Side effect!
}
@Override
public void setHeight(int height) {
this.width = height; // Side effect!
this.height = height;
}
}
// This test works for Rectangle but fails for Square
public void testRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
assertEquals(20, rect.getArea()); // Fails for Square! (25 instead of 20)
}
Issues:
✅ Good - Follows LSP:
// Common interface for shapes
public interface Shape {
int getArea();
}
// Rectangle implementation
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
public int getWidth() { return width; }
public int getHeight() { return height; }
}
// Square implementation (no inheritance from Rectangle)
public class Square implements Shape {
private final int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
public int getSide() { return side; }
}
// Works for any Shape
public int calculateTotalArea(List<Shape> shapes) {
return shapes.stream()
.mapToInt(Shape::getArea)
.sum();
}
Benefits:
✅ Good - Maintains Contracts:
public interface BankAccount {
// Precondition: amount > 0
// Postcondition: balance increased by amount
void deposit(double amount);
// Precondition: amount > 0 and amount <= balance
// Postcondition: balance decreased by amount
void withdraw(double amount) throws InsufficientFundsException;
double getBalance();
}
public class SavingsAccount implements BankAccount {
private double balance;
@Override
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (amount > balance) {
throw new InsufficientFundsException();
}
balance -= amount;
}
@Override
public double getBalance() {
return balance;
}
}
// Subclass maintains contracts (LSP)
public class CheckingAccount implements BankAccount {
private double balance;
private final double overdraftLimit;
public CheckingAccount(double overdraftLimit) {
this.overdraftLimit = overdraftLimit;
}
@Override
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount; // Same postcondition
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
// Can weaken precondition (allow overdraft) but not strengthen
if (amount > balance + overdraftLimit) {
throw new InsufficientFundsException();
}
balance -= amount; // Same postcondition
}
@Override
public double getBalance() {
return balance;
}
}
Rule: Clients should not be forced to depend on interfaces they don't use. Many specific interfaces are better than one general-purpose interface.
Why it matters: Large interfaces force implementations to provide methods they don't need, leading to empty implementations and tight coupling.
❌ Bad - Fat Interface:
// Fat interface forces all implementations to provide all methods
public interface Worker {
void work();
void eat();
void sleep();
void getSalary();
void attendMeeting();
}
// Robot doesn't eat or sleep but is forced to implement these methods
public class RobotWorker implements Worker {
@Override
public void work() {
System.out.println("Robot working");
}
@Override
public void eat() {
// Doesn't make sense for robots!
throw new UnsupportedOperationException("Robots don't eat");
}
@Override
public void sleep() {
// Doesn't make sense for robots!
throw new UnsupportedOperationException("Robots don't sleep");
}
@Override
public void getSalary() {
throw new UnsupportedOperationException("Robots don't get paid");
}
@Override
public void attendMeeting() {
System.out.println("Robot attending meeting");
}
}
Issues:
✅ Good - Segregated Interfaces:
// Segregated interfaces - clients depend only on what they need
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface Payable {
void getSalary();
}
public interface MeetingAttendee {
void attendMeeting();
}
// Human implements relevant interfaces
public class HumanWorker implements Workable, Eatable, Sleepable, Payable, MeetingAttendee {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
@Override
public void sleep() {
System.out.println("Human sleeping");
}
@Override
public void getSalary() {
System.out.println("Human receiving salary");
}
@Override
public void attendMeeting() {
System.out.println("Human attending meeting");
}
}
// Robot only implements relevant interfaces
public class RobotWorker implements Workable, MeetingAttendee {
@Override
public void work() {
System.out.println("Robot working");
}
@Override
public void attendMeeting() {
System.out.println("Robot attending meeting");
}
}
Benefits:
Rule: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Why it matters: DIP decouples code, making it more flexible, testable, and maintainable.
❌ Bad - High-level depends on low-level:
// Low-level module
public class MySQLDatabase {
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
// High-level module depends on concrete low-level module
public class UserService {
private MySQLDatabase database; // Concrete dependency!
public UserService() {
this.database = new MySQLDatabase(); // Tight coupling!
}
public void createUser(String userData) {
// Business logic
database.save(userData);
}
}
Issues:
✅ Good - Both depend on abstraction:
// Abstraction
public interface Database {
void save(String data);
}
// Low-level modules depend on abstraction
public class MySQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
public class PostgreSQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to PostgreSQL: " + data);
}
}
public class MongoDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saving to MongoDB: " + data);
}
}
// High-level module depends on abstraction
public class UserService {
private final Database database; // Abstraction!
// Dependency injected through constructor
public UserService(Database database) {
this.database = database;
}
public void createUser(String userData) {
// Business logic
database.save(userData);
}
}
// Usage - client chooses implementation
Database db = new MySQLDatabase();
UserService service = new UserService(db);
service.createUser("John Doe");
// Easy to switch implementations
Database postgresDb = new PostgreSQLDatabase();
UserService postgresService = new UserService(postgresDb);
// Easy to test with mock
Database mockDb = mock(Database.class);
UserService testService = new UserService(mockDb);
Benefits:
Spring Framework is built on SOLID principles, particularly Dependency Inversion.
✅ Spring DI Example:
// Abstraction
public interface UserRepository {
User findById(Long id);
void save(User user);
}
// Implementation
@Repository
public class JpaUserRepository implements UserRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public User findById(Long id) {
return entityManager.find(User.class, id);
}
@Override
public void save(User user) {
entityManager.persist(user);
}
}
// Service depends on abstraction
@Service
public class UserService {
private final UserRepository userRepository;
// Constructor injection (recommended)
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(Long id) {
return userRepository.findById(id);
}
}
// Controller depends on service abstraction
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.getUser(id);
return ResponseEntity.ok(user);
}
}
Spring DI Best Practices:
@Qualifier when multiple implementations existRule: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Why it matters: Duplication leads to inconsistencies, harder maintenance, and more bugs.
❌ Bad - Obvious Duplication:
public class OrderService {
public void processOnlineOrder(Order order) {
// Validate
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getTotalAmount() <= 0) {
throw new IllegalArgumentException("Order total must be positive");
}
// Process
System.out.println("Processing online order: " + order.getId());
order.setStatus(OrderStatus.PROCESSING);
saveOrder(order);
}
public void processPhoneOrder(Order order) {
// Same validation - DUPLICATION!
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getTotalAmount() <= 0) {
throw new IllegalArgumentException("Order total must be positive");
}
// Process
System.out.println("Processing phone order: " + order.getId());
order.setStatus(OrderStatus.PROCESSING);
saveOrder(order);
}
}
✅ Good - Extract Common Logic:
public class OrderService {
public void processOnlineOrder(Order order) {
validateOrder(order);
processOrder(order, "online");
}
public void processPhoneOrder(Order order) {
validateOrder(order);
processOrder(order, "phone");
}
private void validateOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
if (order.getTotalAmount() <= 0) {
throw new IllegalArgumentException("Order total must be positive");
}
}
private void processOrder(Order order, String type) {
System.out.println("Processing " + type + " order: " + order.getId());
order.setStatus(OrderStatus.PROCESSING);
saveOrder(order);
}
}
✅ Create Utility Classes for Reusable Logic:
public final class StringUtils {
private StringUtils() {
// Prevent instantiation
}
public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
public static String capitalize(String str) {
if (isBlank(str)) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
}
public static String truncate(String str, int maxLength) {
if (str == null || str.length() <= maxLength) {
return str;
}
return str.substring(0, maxLength) + "...";
}
}
// Usage
if (StringUtils.isBlank(username)) {
throw new ValidationException("Username is required");
}
String displayName = StringUtils.capitalize(name);
✅ Use Generics to Avoid Duplication:
// Instead of creating separate classes for different types
public class GenericRepository<T, ID> {
private final Class<T> entityClass;
@PersistenceContext
private EntityManager entityManager;
public GenericRepository(Class<T> entityClass) {
this.entityClass = entityClass;
}
public Optional<T> findById(ID id) {
T entity = entityManager.find(entityClass, id);
return Optional.ofNullable(entity);
}
public List<T> findAll() {
CriteriaQuery<T> query = entityManager.getCriteriaBuilder()
.createQuery(entityClass);
query.select(query.from(entityClass));
return entityManager.createQuery(query).getResultList();
}
public void save(T entity) {
entityManager.persist(entity);
}
public void delete(T entity) {
entityManager.remove(entity);
}
}
// Concrete repositories extend generic repository
@Repository
public class UserRepository extends GenericRepository<User, Long> {
public UserRepository() {
super(User.class);
}
// Add User-specific queries
public Optional<User> findByEmail(String email) {
// Custom query
}
}
Rule: Names should reveal intent, be pronounceable, and be searchable.
❌ Bad Names:
int d; // elapsed time in days
String yyyymmdd;
List<int[]> list1;
public void getData() {
// What data?
}
✅ Good Names:
int elapsedTimeInDays;
String formattedDate;
List<Customer> activeCustomers;
public Customer getCustomerById(Long customerId) {
// Clear what this method does
}
// Classes: PascalCase, nouns
public class CustomerService { }
public class OrderRepository { }
// Interfaces: PascalCase, often adjectives or nouns
public interface Serializable { }
public interface UserRepository { }
// Methods: camelCase, verbs
public void calculateTotal() { }
public Customer findCustomerById(Long id) { }
// Variables: camelCase, nouns
String customerName;
int orderCount;
boolean isActive;
// Constants: UPPER_SNAKE_CASE
public static final int MAX_RETRY_COUNT = 3;
public static final String DEFAULT_ENCODING = "UTF-8";
// Packages: lowercase, periods
package com.example.service;
package com.example.repository;
// Boolean methods/variables: is, has, can
boolean isValid();
boolean hasPermission();
boolean canExecute();
Rule: Functions should be small and do one thing. Aim for 5-20 lines per method.
❌ Bad - Large, Complex Method:
public void processOrder(Order order) {
// Validation
if (order == null) throw new IllegalArgumentException();
if (order.getItems().isEmpty()) throw new IllegalArgumentException();
// Calculate total
double total = 0;
for (OrderItem item : order.getItems()) {
double itemPrice = item.getPrice();
int quantity = item.getQuantity();
double discount = item.getDiscount();
total += (itemPrice * quantity) * (1 - discount);
}
order.setTotal(total);
// Apply coupon
if (order.getCoupon() != null) {
String couponCode = order.getCoupon().getCode();
if (couponCode.startsWith("SAVE")) {
total *= 0.9;
} else if (couponCode.startsWith("BIG")) {
total *= 0.8;
}
order.setTotal(total);
}
// Check inventory
for (OrderItem item : order.getItems()) {
int available = inventoryService.getAvailableQuantity(item.getProductId());
if (available < item.getQuantity()) {
throw new InsufficientInventoryException();
}
}
// Save order
orderRepository.save(order);
// Send email
emailService.send(order.getCustomer().getEmail(), "Order Confirmation",
"Your order " + order.getId() + " has been confirmed");
// Update inventory
for (OrderItem item : order.getItems()) {
inventoryService.decrementQuantity(item.getProductId(), item.getQuantity());
}
}
✅ Good - Small, Focused Methods:
public void processOrder(Order order) {
validateOrder(order);
calculateOrderTotal(order);
applyCouponDiscount(order);
checkInventoryAvailability(order);
saveOrder(order);
sendConfirmationEmail(order);
updateInventory(order);
}
private void validateOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}
}
private void calculateOrderTotal(Order order) {
double total = order.getItems().stream()
.mapToDouble(this::calculateItemTotal)
.sum();
order.setTotal(total);
}
private double calculateItemTotal(OrderItem item) {
return item.getPrice() * item.getQuantity() * (1 - item.getDiscount());
}
private void applyCouponDiscount(Order order) {
if (order.getCoupon() == null) {
return;
}
double discountMultiplier = getDiscountMultiplier(order.getCoupon());
order.setTotal(order.getTotal() * discountMultiplier);
}
private double getDiscountMultiplier(Coupon coupon) {
String code = coupon.getCode();
if (code.startsWith("SAVE")) return 0.9;
if (code.startsWith("BIG")) return 0.8;
return 1.0;
}
private void checkInventoryAvailability(Order order) {
for (OrderItem item : order.getItems()) {
int available = inventoryService.getAvailableQuantity(item.getProductId());
if (available < item.getQuantity()) {
throw new InsufficientInventoryException(
"Product " + item.getProductId() + " has insufficient inventory");
}
}
}
private void saveOrder(Order order) {
orderRepository.save(order);
}
private void sendConfirmationEmail(Order order) {
String email = order.getCustomer().getEmail();
String subject = "Order Confirmation";
String body = String.format("Your order %s has been confirmed", order.getId());
emailService.send(email, subject, body);
}
private void updateInventory(Order order) {
order.getItems().forEach(item ->
inventoryService.decrementQuantity(item.getProductId(), item.getQuantity())
);
}
Benefits:
Rule: Code should be self-explanatory. Comments should explain WHY, not WHAT.
❌ Bad Comments:
// Set the flag to true
isActive = true;
// Loop through users
for (User user : users) {
// Check if user is active
if (user.isActive()) {
// Add to list
activeUsers.add(user);
}
}
// This is the UserService class
public class UserService {
}
✅ Good Comments:
// No comment needed - code is self-explanatory
isActive = true;
List<User> activeUsers = users.stream()
.filter(User::isActive)
.collect(Collectors.toList());
// Good: Explains WHY, not WHAT
// We use exponential backoff to avoid overwhelming the external API
// after multiple failures (circuit breaker pattern)
private int calculateRetryDelay(int attemptNumber) {
return (int) Math.pow(2, attemptNumber) * 1000;
}
// Good: JavaDoc for public API
/**
* Transfers funds between accounts atomically.
*
* @param fromAccount source account (must have sufficient balance)
* @param toAccount destination account
* @param amount amount to transfer (must be positive)
* @throws InsufficientFundsException if source account lacks funds
* @throws IllegalArgumentException if amount is negative or zero
*/
public void transferFunds(Account fromAccount, Account toAccount, double amount)
throws InsufficientFundsException {
// Implementation
}
// Good: Explains non-obvious business rule
// Tax calculation excludes shipping but includes discount adjustments
// per IRS regulation 2024-15
double taxableAmount = subtotal - discount;
When to Comment:
When NOT to Comment:
Rule: Use exceptions for exceptional cases. Don't use exceptions for control flow.
❌ Bad Error Handling:
// Using exceptions for control flow
public User findUser(Long id) {
try {
return userRepository.findById(id);
} catch (NotFoundException e) {
return null; // Swallowing exception
}
}
// Catching generic Exception
public void processData(String data) {
try {
// Complex logic
} catch (Exception e) {
// Too broad!
}
}
// Empty catch block
try {
riskyOperation();
} catch (IOException e) {
// Ignored - NEVER DO THIS
}
✅ Good Error Handling:
// Use Optional for "not found" scenarios
public Optional<User> findUser(Long id) {
return userRepository.findById(id);
}
// Catch specific exceptions
public void processData(String data) {
try {
parseAndValidate(data);
saveToDatabase(data);
} catch (JsonParseException e) {
log.error("Failed to parse JSON data: {}", data, e);
throw new DataProcessingException("Invalid JSON format", e);
} catch (DataAccessException e) {
log.error("Database error while saving data", e);
throw new DataProcessingException("Failed to save data", e);
}
}
// Always handle or rethrow exceptions
try {
riskyOperation();
} catch (IOException e) {
log.error("Operation failed", e);
throw new ApplicationException("Failed to perform operation", e);
}
// Use try-with-resources for auto-closeable resources
public String readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.lines().collect(Collectors.joining("\n"));
}
// Reader automatically closed, even if exception occurs
}
Rule: Organize code logically within classes. Related methods should be close together.
✅ Good Class Organization:
public class UserService {
// 1. Constants
private static final int MAX_LOGIN_ATTEMPTS = 3;
private static final long LOCKOUT_DURATION_MINUTES = 30;
// 2. Static fields
private static final Logger log = LoggerFactory.getLogger(UserService.class);
// 3. Instance fields
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
// 4. Constructors
public UserService(UserRepository userRepository,
PasswordEncoder passwordEncoder,
EmailService emailService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.emailService = emailService;
}
// 5. Public methods (grouped by functionality)
// User creation methods
public User registerUser(UserRegistrationDto dto) {
validateRegistration(dto);
User user = createUser(dto);
sendWelcomeEmail(user);
return user;
}
// User authentication methods
public AuthToken login(String email, String password) {
User user = findUserByEmail(email);
validatePassword(user, password);
return generateAuthToken(user);
}
// 6. Private helper methods (near methods that use them)
private void validateRegistration(UserRegistrationDto dto) {
// Validation logic
}
private User createUser(UserRegistrationDto dto) {
// Creation logic
}
private void sendWelcomeEmail(User user) {
emailService.send(user.getEmail(), "Welcome!", getWelcomeEmailBody());
}
private User findUserByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UserNotFoundException(email));
}
private void validatePassword(User user, String password) {
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
throw new AuthenticationException("Invalid password");
}
}
private AuthToken generateAuthToken(User user) {
// Token generation logic
}
private String getWelcomeEmailBody() {
return "Welcome to our application!";
}
}
Rule: Use Optional<T> to represent values that may be absent. Never return null for collections.
❌ Bad - Returning Null:
public User findUser(Long id) {
User user = database.find(id);
return user; // May return null!
}
// Caller must remember to check null
User user = findUser(123L);
if (user != null) {
// Use user
}
✅ Good - Using Optional:
public Optional<User> findUser(Long id) {
User user = database.find(id);
return Optional.ofNullable(user);
}
// Caller forced to handle absence
Optional<User> userOpt = findUser(123L);
// Method 1: ifPresent
userOpt.ifPresent(user -> System.out.println(user.getName()));
// Method 2: orElse
User user = userOpt.orElse(createDefaultUser());
// Method 3: orElseThrow
User user = userOpt.orElseThrow(() ->
new UserNotFoundException("User 123 not found"));
// Method 4: map/flatMap
String email = userOpt
.map(User::getEmail)
.orElse("unknown@example.com");
Optional Best Practices:
Optional<T> from methods that may not find a valueOptional for fieldsOptional as method parametersOptional-returning methodsOptional.empty() instead of Optional.ofNullable(null)❌ Bad Optional Usage:
// Don't use Optional as field
public class User {
private Optional<String> middleName; // BAD!
}
// Don't use Optional as parameter
public void setEmail(Optional<String> email) { // BAD!
}
// Don't call get() without checking
Optional<User> userOpt = findUser(id);
User user = userOpt.get(); // May throw NoSuchElementException!
✅ Good Optional Usage:
// Use null for optional fields (or use proper null handling)
public class User {
private String middleName; // Can be null
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
}
// Use regular parameter with @Nullable annotation
public void setEmail(@Nullable String email) {
this.email = email;
}
// Always check before get(), or use other methods
Optional<User> userOpt = findUser(id);
if (userOpt.isPresent()) {
User user = userOpt.get();
// Use user
}
// Or use orElse/orElseThrow/ifPresent
User user = userOpt.orElseThrow(() -> new NotFoundException());
Rule: Favor composition (has-a) over inheritance (is-a) unless there's a true is-a relationship.
❌ Bad - Inheritance Abuse:
// Inheritance used just to reuse code (wrong!)
public class Stack extends ArrayList<Object> {
public void push(Object item) {
add(item);
}
public Object pop() {
return remove(size() - 1);
}
}
// Problems:
// 1. Stack exposes all ArrayList methods (add, remove, clear, etc.)
// 2. Stack IS-NOT-A ArrayList semantically
// 3. Breaks encapsulation
✅ Good - Composition:
public class Stack<T> {
private final List<T> elements = new ArrayList<>();
public void push(T item) {
elements.add(item);
}
public T pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
public T peek() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.get(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
public int size() {
return elements.size();
}
}
When to Use Inheritance:
When to Use Composition:
Rule: Make classes and variables immutable when possible. Use final extensively.
✅ Immutable Class:
public final class Money {
private final double amount;
private final String currency;
public Money(double amount, String currency) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
this.amount = amount;
this.currency = currency;
}
public double getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
// Return new instance instead of modifying
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount + other.amount, this.currency);
}
public Money multiply(double multiplier) {
return new Money(this.amount * multiplier, this.currency);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money money = (Money) o;
return Double.compare(money.amount, amount) == 0
&& currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}
Benefits of Immutability:
✅ Using Final for Variables:
public void processOrder(Order order) {
final double total = order.getTotal();
final List<OrderItem> items = order.getItems();
// Compiler prevents reassignment
// total = 100; // Compilation error
// items = new ArrayList<>(); // Compilation error
// Note: final prevents reassignment, not mutation
items.add(new OrderItem()); // This is allowed!
// For true immutability, use Collections.unmodifiableList
final List<OrderItem> immutableItems =
Collections.unmodifiableList(new ArrayList<>(items));
}
Rule: Use Stream API for collection operations. It's more readable, functional, and can be parallelized.
❌ Bad - Imperative Style:
List<User> activeUsers = new ArrayList<>();
for (User user : users) {
if (user.isActive()) {
activeUsers.add(user);
}
}
List<String> emails = new ArrayList<>();
for (User user : activeUsers) {
emails.add(user.getEmail());
}
Collections.sort(emails);
✅ Good - Declarative with Streams:
List<String> emails = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.sorted()
.collect(Collectors.toList());
✅ Common Stream Patterns:
// Filtering
List<Order> largeOrders = orders.stream()
.filter(order -> order.getTotal() > 1000)
.collect(Collectors.toList());
// Mapping
List<String> customerNames = orders.stream()
.map(order -> order.getCustomer().getName())
.collect(Collectors.toList());
// FlatMap (flatten nested collections)
List<OrderItem> allItems = orders.stream()
.flatMap(order -> order.getItems().stream())
.collect(Collectors.toList());
// Reduce (sum, average, etc.)
double totalRevenue = orders.stream()
.mapToDouble(Order::getTotal)
.sum();
Optional<Order> maxOrder = orders.stream()
.max(Comparator.comparing(Order::getTotal));
// Grouping
Map<String, List<Order>> ordersByStatus = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus));
// Partitioning (special case of grouping - boolean)
Map<Boolean, List<Order>> ordersByShipped = orders.stream()
.collect(Collectors.partitioningBy(Order::isShipped));
// Find first/any
Optional<User> firstAdmin = users.stream()
.filter(User::isAdmin)
.findFirst();
// Distinct
List<String> uniqueCities = users.stream()
.map(User::getCity)
.distinct()
.collect(Collectors.toList());
// Limit and skip
List<User> firstTenUsers = users.stream()
.limit(10)
.collect(Collectors.toList());
// Combining operations
double averageOrderValueForActiveCustomers = orders.stream()
.filter(order -> order.getCustomer().isActive())
.mapToDouble(Order::getTotal)
.average()
.orElse(0.0);
Stream Best Practices:
❌ Bad Stream Usage:
// Don't modify external state in streams
List<String> results = new ArrayList<>();
users.stream()
.forEach(user -> results.add(user.getName())); // Side effect!
// Use collect instead
List<String> results = users.stream()
.map(User::getName)
.collect(Collectors.toList());
// Don't reuse streams
Stream<User> userStream = users.stream();
long count = userStream.count(); // OK
List<User> list = userStream.collect(Collectors.toList()); // IllegalStateException!
Rule: Use lambda expressions for functional interfaces. Prefer method references when applicable.
✅ Lambda Best Practices:
// Lambda expression
List<User> sorted = users.stream()
.sorted((u1, u2) -> u1.getName().compareTo(u2.getName()))
.collect(Collectors.toList());
// Better: Method reference
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getName))
.collect(Collectors.toList());
// Lambda with multiple statements
users.forEach(user -> {
user.setLastLoginTime(LocalDateTime.now());
user.incrementLoginCount();
userRepository.save(user);
});
// Custom functional interface
@FunctionalInterface
public interface OrderProcessor {
void process(Order order);
}
OrderProcessor processor = order -> {
validateOrder(order);
calculateTotal(order);
saveOrder(order);
};
Rule: Use method references instead of lambda expressions when possible. They're more concise.
// Lambda vs Method Reference
// Lambda: user -> user.getName()
// Method reference: User::getName
users.stream().map(User::getName);
// Lambda: user -> System.out.println(user)
// Method reference: System.out::println
users.forEach(System.out::println);
// Lambda: () -> new ArrayList<>()
// Method reference: ArrayList::new
Supplier<List<String>> supplier = ArrayList::new;
// Lambda: str -> str.length()
// Method reference: String::length
strings.stream().map(String::length);
Types of Method References:
ClassName::staticMethodobject::instanceMethodClassName::instanceMethodClassName::newRule: Always use try-with-resources for AutoCloseable resources.
❌ Bad - Manual Resource Management:
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
String line = reader.readLine();
// Process line
} catch (IOException e) {
log.error("Error reading file", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error("Error closing reader", e);
}
}
}
✅ Good - Try-With-Resources:
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine();
// Process line
} catch (IOException e) {
log.error("Error reading file", e);
}
// Reader automatically closed, even if exception occurs
// Multiple resources
try (FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")) {
// Use streams
} catch (IOException e) {
log.error("Error processing files", e);
}
// Both streams automatically closed in reverse order
Rule: Use StringBuilder for multiple string concatenations in loops. Use + for simple concatenations.
❌ Bad - String Concatenation in Loop:
String result = "";
for (int i = 0; i < 1000; i++) {
result += i + ","; // Creates 1000 new String objects!
}
✅ Good - StringBuilder:
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
builder.append(i).append(",");
}
String result = builder.toString();
// Or use String.join for collections
List<String> items = Arrays.asList("apple", "banana", "cherry");
String result = String.join(", ", items);
// Or use Streams
String result = IntStream.range(0, 1000)
.mapToObj(String::valueOf)
.collect(Collectors.joining(","));
✅ Simple Concatenation - Use + is Fine:
// OK for simple cases (compiler optimizes)
String fullName = firstName + " " + lastName;
String message = "Hello, " + name + "!";
// Not OK in loops
for (String item : items) {
result = result + item; // BAD!
}
Rule: Use enums for fixed sets of constants. Enums can have fields, methods, and constructors.
✅ Basic Enum:
public enum OrderStatus {
PENDING,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED
}
// Usage
Order order = new Order();
order.setStatus(OrderStatus.PENDING);
if (order.getStatus() == OrderStatus.DELIVERED) {
// Process delivery
}
✅ Enum with Fields and Methods:
public enum PaymentMethod {
CREDIT_CARD("Credit Card", 2.9),
DEBIT_CARD("Debit Card", 1.5),
PAYPAL("PayPal", 3.5),
BITCOIN("Bitcoin", 1.0);
private final String displayName;
private final double transactionFeePercent;
PaymentMethod(String displayName, double transactionFeePercent) {
this.displayName = displayName;
this.transactionFeePercent = transactionFeePercent;
}
public String getDisplayName() {
return displayName;
}
public double calculateFee(double amount) {
return amount * (transactionFeePercent / 100);
}
public double calculateTotal(double amount) {
return amount + calculateFee(amount);
}
}
// Usage
PaymentMethod method = PaymentMethod.CREDIT_CARD;
double total = method.calculateTotal(100.0); // 102.90
System.out.println("Paying with: " + method.getDisplayName());
✅ Enum with Abstract Methods (Strategy Pattern):
public enum Operation {
PLUS {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS {
@Override
public double apply(double x, double y) {
return x - y;
}
},
MULTIPLY {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override
public double apply(double x, double y) {
if (y == 0) throw new ArithmeticException("Division by zero");
return x / y;
}
};
public abstract double apply(double x, double y);
}
// Usage
double result = Operation.PLUS.apply(5, 3); // 8.0
Rule: Use checked exceptions for recoverable conditions, unchecked for programming errors.
Checked Exceptions:
Exception (not RuntimeException)Unchecked Exceptions:
RuntimeException✅ When to Use Each:
// Checked exception - caller can handle
public User findUserById(Long id) throws UserNotFoundException {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
// Unchecked exception - programming error
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.age = age;
}
// Checked exception - I/O operation
public String readFile(String path) throws IOException {
return Files.readString(Paths.get(path));
}
// Unchecked exception - null argument (programming error)
public void processOrder(Order order) {
Objects.requireNonNull(order, "Order cannot be null");
// Process order
}
Rule: Catch exceptions only if you can handle them meaningfully. Otherwise, let them propagate.
❌ Bad - Catching and Rethrowing:
public void processData(String data) throws DataProcessingException {
try {
// Process data
} catch (JsonParseException e) {
throw new DataProcessingException(e); // Unnecessary try-catch
}
}
// Better: Let it propagate
public void processData(String data) throws JsonParseException {
// Process data
}
✅ Good - Catch When You Can Handle:
public void processDataWithRetry(String data) {
int maxAttempts = 3;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
processData(data);
return; // Success
} catch (TransientException e) {
if (attempt == maxAttempts) {
log.error("Failed after {} attempts", maxAttempts, e);
throw new DataProcessingException("Processing failed", e);
}
log.warn("Attempt {} failed, retrying...", attempt);
sleep(1000 * attempt); // Exponential backoff
}
}
}
✅ Well-Designed Custom Exception:
public class InsufficientFundsException extends Exception {
private final double requestedAmount;
private final double availableBalance;
public InsufficientFundsException(double requestedAmount, double availableBalance) {
super(String.format("Insufficient funds: requested %.2f, available %.2f",
requestedAmount, availableBalance));
this.requestedAmount = requestedAmount;
this.availableBalance = availableBalance;
}
public double getRequestedAmount() {
return requestedAmount;
}
public double getAvailableBalance() {
return availableBalance;
}
public double getShortfall() {
return requestedAmount - availableBalance;
}
}
// Usage
try {
account.withdraw(500);
} catch (InsufficientFundsException e) {
log.error("Withdrawal failed: {}", e.getMessage());
notifyUser(String.format("You need $%.2f more", e.getShortfall()));
}
✅ Exception Logging Best Practices:
public void processOrder(Order order) {
try {
validateOrder(order);
saveOrder(order);
sendConfirmation(order);
} catch (ValidationException e) {
// Log with context
log.error("Order validation failed for order {}: {}",
order.getId(), e.getMessage(), e);
throw e;
} catch (DataAccessException e) {
// Log with different level based on recoverability
log.error("Failed to save order {}", order.getId(), e);
throw new OrderProcessingException("Failed to save order", e);
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
122,000 周安装