platform-context-exporter / rules
alkoleft/platform-context-exporter/.cursor/rules/111-java-object-oriented-design.mdc
Java Object-Oriented Design Guidelines
Cursor rule48 starsChanged 16 months ago
---
description: Java Object-Oriented Design Guidelines
globs: **/*.java
alwaysApply: false
---
# Java Object-Oriented Design Guidelines
This document provides comprehensive guidelines for robust Java object-oriented design and refactoring. It emphasizes core principles like SOLID, DRY, and YAGNI, best practices for class and interface design including favoring composition over inheritance and designing for immutability. The rules also cover mastering encapsulation, inheritance, and polymorphism, and finally, identifying and refactoring common object-oriented design code smells such as God Classes, Feature Envy, and Data Clumps to promote maintainable, flexible, and understandable code.
## Implementing These Principles
These guidelines are built upon the following core principles:
1. **Adherence to Fundamental Design Principles**: Embrace foundational principles like SOLID, DRY, and YAGNI. These principles are key to building systems that are robust, maintainable, flexible, and easy to understand.
2. **Effective Class and Interface Design**: Employ best practices for designing classes and interfaces. This includes favoring composition over inheritance to achieve flexibility, programming to an interface rather than an implementation to promote loose coupling, keeping classes small and focused on a single responsibility, and designing for immutability where appropriate to enhance simplicity and thread-safety.
3. **Mastery of Core OOP Concepts**: Thoroughly understand and correctly apply the pillars of object-oriented programming:
* **Encapsulation**: Protect internal state and expose behavior through well-defined interfaces.
* **Inheritance**: Model true "is-a" relationships, ensuring subclasses are substitutable for their base types (Liskov Substitution Principle).
* **Polymorphism**: Allow objects of different types to respond to the same message in their own way, simplifying client code.
4. **Proactive Code Smell Management**: Develop the ability to identify common object-oriented design "code smells" (e.g., God Class, Feature Envy, Data Clumps, Refused Bequest). Recognizing and refactoring these smells is crucial for improving the long-term health, maintainability, and clarity of the codebase.
## Table of contents
- Rule 1: Adhere to Core Design Principles (SOLID, DRY, YAGNI)
- Rule 2: Follow Best Practices for Class and Interface Design
- Rule 3: Master Encapsulation, Inheritance, and Polymorphism
- Rule 4: Identify and Refactor Object-Oriented Design Code Smells
- Rule 5: Creating and Destroying Objects
- Rule 6: Classes and Interfaces Best Practices
- Rule 7: Enums and Annotations
- Rule 8: Method Design
- Rule 9: Exception Handling
## Rule 1: Adhere to Core Design Principles (SOLID, DRY, YAGNI)
Title: Apply Fundamental Software Design Principles
Description: Core principles like SOLID, DRY, and YAGNI are foundational to good object-oriented design, leading to more robust, maintainable, and understandable systems.
### Sub-Rule 1.1: Single Responsibility Principle (SRP)
Title: A class should have one, and only one, reason to change.
Description: This means a class should only have one job or primary responsibility. If a class handles multiple responsibilities, changes to one responsibility might inadvertently affect others.
**Good example:**
```java
// Good: Separate responsibilities
class UserData {
private String name;
private String email;
// constructor, getters
public UserData(String name, String email) { this.name = name; this.email = email; }
public String getName() { return name; }
public String getEmail() { return email; }
}
class UserPersistence {
public void saveUser(UserData user) {
System.out.println("Saving user " + user.getName() + " to database.");
// Database saving logic
}
}
class UserEmailer {
public void sendWelcomeEmail(UserData user) {
System.out.println("Sending welcome email to " + user.getEmail());
// Email sending logic
}
}
```
**Bad Example:**
```java
// Bad: User class with multiple responsibilities
class User {
private String name;
private String email;
public User(String name, String email) { this.name = name; this.email = email; }
public String getName() { return name; }
public String getEmail() { return email; }
public void saveToDatabase() {
System.out.println("Saving user " + name + " to database.");
// Database logic mixed in
}
public void sendWelcomeEmail() {
System.out.println("Sending welcome email to " + email);
// Email logic mixed in
}
// If email sending changes, or DB logic changes, this class needs to change.
}
```
### Sub-Rule 1.2: Open/Closed Principle (OCP)
Title: Software entities should be open for extension but closed for modification.
Description: You should be able to add new functionality without changing existing, tested code. This is often achieved using interfaces, abstract classes, and polymorphism.
**Good example:**
```java
interface Shape {
double calculateArea();
}
class Rectangle implements Shape {
private double width, height;
public Rectangle(double w, double h) { width=w; height=h; }
@Override public double calculateArea() { return width * height; }
}
class Circle implements Shape {
private double radius;
public Circle(double r) { radius=r; }
@Override public double calculateArea() { return Math.PI * radius * radius; }
}
// New shapes (e.g., Triangle) can be added by implementing Shape
// without modifying existing Shape, Rectangle, Circle, or AreaCalculator.
class AreaCalculator {
public double getTotalArea(List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::calculateArea).sum();
}
}
```
**Bad Example:**
```java
// Bad: AreaCalculator needs modification for new shapes
class AreaCalculatorBad {
public double calculateRectangleArea(Rectangle rect) { return rect.width * rect.height; }
public double calculateCircleArea(Circle circ) { return Math.PI * circ.radius * circ.radius; }
// If a Triangle class is added, this class must be modified to add calculateTriangleArea().
}
class Rectangle { public double width, height; /* ... */ }
class Circle { public double radius; /* ... */ }
```
### Sub-Rule 1.3: Liskov Substitution Principle (LSP)
Title: Subtypes must be substitutable for their base types.
Description: Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program or causing unexpected behavior.
**Good example:**
```java
interface Bird {
void move();
}
class FlyingBird implements Bird {
public void fly() { System.out.println("Flying high!"); }
@Override public void move() { fly(); }
}
class Sparrow extends FlyingBird { /* Can fly */ }
class Ostrich implements Bird { // Ostrich is a Bird but doesn't fly in the typical sense
public void runFast() { System.out.println("Running fast on the ground!"); }
@Override public void move() { runFast(); }
}
public class BirdLSPExample {
public static void makeBirdMove(Bird bird) {
bird.move(); // Works correctly for Sparrow (flies) and Ostrich (runs)
}
public static void main(String args) {
makeBirdMove(new Sparrow());
makeBirdMove(new Ostrich());
}
}
```
**Bad Example:**
```java
// Bad: Violating LSP
class Bird {
public void fly() { System.out.println("Bird is flying."); }
}
class Penguin extends Bird {
@Override
public void fly() {
// Penguins can't fly, so this method might do nothing or throw an exception.
// This violates LSP because a Penguin can't simply replace a generic Bird where fly() is expected.
throw new UnsupportedOperationException("Penguins can't fly.");
}
public void swim() { System.out.println("Penguin is swimming."); }
}
public class BirdLSPViolation {
public static void letTheBirdFly(Bird bird) {
bird.fly(); // This will crash if bird is a Penguin
}
public static void main(String args) {
try {
letTheBirdFly(new Penguin());
} catch (UnsupportedOperationException e) {
System.err.println(e.getMessage());
}
}
}
```
### Sub-Rule 1.4: Interface Segregation Principle (ISP)
Title: Clients should not be forced to depend on interfaces they do not use.
Description: It's better to have many small, specific interfaces (role interfaces) than one large, general-purpose interface. This prevents classes from having to implement methods they don't need.
**Good example:**
```java
// Good: Segregated interfaces
interface Worker {
void work();
}
interface Eater {
void eat();
}
class HumanWorker implements Worker, Eater {
@Override public void work() { System.out.println("Human working."); }
@Override public void eat() { System.out.println("Human eating."); }
}
class RobotWorker implements Worker {
@Override public void work() { System.out.println("Robot working efficiently."); }
// RobotWorker does not need to implement eat()
}
```
**Bad Example:**
```java
// Bad: Fat interface
interface IWorkerAndEater {
void work();
void eat(); // All implementers must provide eat(), even if they don't eat.
}
class Human implements IWorkerAndEater {
@Override public void work() { /* ... */ }
@Override public void eat() { /* ... */ }
}
class Robot implements IWorkerAndEater {
@Override public void work() { System.out.println("Robot working."); }
@Override public void eat() {
// Robots don't eat. This method is forced and likely empty or throws exception.
throw new UnsupportedOperationException("Robots don't eat.");
}
}
```
### Sub-Rule 1.5: Dependency Inversion Principle (DIP)
Title: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Description: Abstractions (e.g., interfaces) should not depend on details. Details (concrete implementations) should depend on abstractions. This promotes loose coupling.
**Good example:**
```java
// Abstraction
interface MessageSender {
void sendMessage(String message);
}
// Low-level module (detail)
class EmailSender implements MessageSender {
@Override public void sendMessage(String message) { System.out.println("Email sent: " + message); }
}
// Low-level module (detail)
class SMSSender implements MessageSender {
@Override public void sendMessage(String message) { System.out.println("SMS sent: " + message); }
}
// High-level module
class NotificationService {
private final MessageSender sender; // Depends on abstraction
public NotificationService(MessageSender sender) { // Dependency injected
this.sender = sender;
}
public void notify(String message) {
sender.sendMessage(message);
}
}
public class DIPExample {
public static void main(String args) {
NotificationService emailNotifier = new NotificationService(new EmailSender());
emailNotifier.notify("Hello via Email!");
NotificationService smsNotifier = new NotificationService(new SMSSender());
smsNotifier.notify("Hello via SMS!");
}
}
```
**Bad Example:**
```java
// Bad: High-level module depends directly on low-level module
class EmailerBad {
public void sendEmail(String message) { System.out.println("Email sent: " + message); }
}
class NotificationServiceBad {
private EmailerBad emailer; // Direct dependency on concrete EmailerBad
public NotificationServiceBad() {
this.emailer = new EmailerBad(); // Instantiates concrete class
}
public void sendNotification(String message) {
emailer.sendEmail(message); // Tightly coupled
}
// If we want to use SMSSender, NotificationServiceBad needs to be changed.
}
```
### Sub-Rule 1.6: DRY (Don't Repeat Yourself)
Title: Avoid duplication of code.
Description: Every piece of knowledge or logic must have a single, unambiguous, authoritative representation within a system. Use methods, classes, inheritance, or composition to centralize and reuse code.
**Good example:**
```java
class CalculationUtils {
// Centralized validation logic
public static void validatePositive(double value, String name) {
if (value <= 0) {
throw new IllegalArgumentException(name + " must be positive.");
}
}
}
class RectangleArea {
public double calculate(double width, double height) {
CalculationUtils.validatePositive(width, "Width");
CalculationUtils.validatePositive(height, "Height");
return width * height;
}
}
class CircleVolume {
public double calculate(double radius, double height) {
CalculationUtils.validatePositive(radius, "Radius");
CalculationUtils.validatePositive(height, "Height");
return Math.PI * radius * radius * height;
}
}
```
**Bad Example:**
```java
// Bad: Duplicated validation logic
class RectangleAreaBad {
public double calculate(double width, double height) {
if (width <= 0) throw new IllegalArgumentException("Width must be positive."); // Duplicated
if (height <= 0) throw new IllegalArgumentException("Height must be positive."); // Duplicated
return width * height;
}
}
class CircleVolumeBad {
public double calculate(double radius, double height) {
if (radius <= 0) throw new IllegalArgumentException("Radius must be positive."); // Duplicated
if (height <= 0) throw new IllegalArgumentException("Height must be positive."); // Duplicated
return Math.PI * radius * radius * height;
}
}
```
### Sub-Rule 1.7: YAGNI (You Ain't Gonna Need It)
Title: Implement features only when you actually need them.
Description: Avoid implementing functionality based on speculation that it might be needed in the future. This helps prevent over-engineering and keeps the codebase simpler and more focused on current requirements.
**Good example:**
```java
// Good: Simple class meeting current needs
class ReportGenerator {
public String generateSimpleReport(List<String> data) {
System.out.println("Generating simple report.");
return "Report: " + String.join(", ", data);
}
// If PDF export is needed later, it can be added then.
// No need to implement generatePdfReport, generateExcelReport etc. upfront.
}
```
**Bad Example:**
```java
// Bad: Over-engineered with features not currently needed
class ReportGeneratorOverkill {
public String generateHtmlReport(List<String> data) { /* ... */ return "html";}
public byte generatePdfReport(List<String> data) {
System.out.println("Generating PDF report (not actually used yet).");
return new byte0;
}
public byte generateExcelReport(List<String> data) {
System.out.println("Generating Excel report (not actually used yet).");
return new byte0;
}
// Current requirement is only for HTML, but PDF and Excel are added "just in case".
}
```
## Rule 2: Follow Best Practices for Class and Interface Design
Title: Design Well-Structured and Maintainable Classes and Interfaces
Description: Good class and interface design is crucial for building flexible and understandable OOD systems.
- **Favor Composition over Inheritance:** Where possible, use composition (has-a relationship) to reuse code and build complex objects by assembling smaller, focused objects. Inheritance (is-a relationship) can lead to tight coupling and fragile class hierarchies if overused or misused.
- **Program to an Interface, Not an Implementation:** Depend on abstractions (interfaces or abstract classes) rather than concrete implementations. This promotes loose coupling and allows different implementations to be swapped easily.
- **Keep Classes Small and Focused:** Similar to SRP, ensure classes are not trying to do too much. Smaller classes are easier to understand, test, and maintain.
- **Design for Immutability:** Immutable objects (whose state cannot change after creation) are simpler to reason about, inherently thread-safe, and can be freely shared without risk of unintended modification.
- **Clear Naming:** Use clear, descriptive, and unambiguous names for classes, interfaces, methods, and variables that accurately reveal their purpose and intent.
**Good example:**
(Illustrating composition and programming to an interface)
```java
// Interface (Abstraction)
interface Engine {
void start();
void stop();
}
// Concrete Implementations
class PetrolEngine implements Engine {
@Override public void start() { System.out.println("Petrol engine started."); }
@Override public void stop() { System.out.println("Petrol engine stopped."); }
}
class ElectricEngine implements Engine {
@Override public void start() { System.out.println("Electric engine silently started."); }
@Override public void stop() { System.out.println("Electric engine silently stopped."); }
}
// Class using Composition and Programming to an Interface
class Car {
private final Engine engine; // Depends on Engine interface (abstraction)
private final String modelName;
// Engine is injected (composition)
public Car(String modelName, Engine engine) {
this.modelName = modelName;
this.engine = engine;
}
public void startCar() {
System.out.print(modelName + ": ");
engine.start();
}
public void stopCar() {
System.out.print(modelName + ": ");
engine.stop();
}
public String getModelName(){ return modelName; }
}
public class ClassDesignExample {
public static void main(String args) {
Car petrolCar = new Car("SedanX", new PetrolEngine());
Car electricCar = new Car("EVMax", new ElectricEngine());
petrolCar.startCar();
electricCar.startCar();
petrolCar.stopCar();
electricCar.stopCar();
}
}
```
**Bad Example:**
(Illustrating tight coupling through concrete implementation and potentially problematic inheritance)
```java
// Bad: Tight coupling, not programming to an interface
class BadCar {
private final BadPetrolEngine engine; // Direct dependency on concrete BadPetrolEngine
public BadCar() {
this.engine = new BadPetrolEngine(); // Instantiates concrete class
}
public void start() { engine.startPetrol(); }
// If we want an electric car, this class needs significant changes or a new similar class.
}
class BadPetrolEngine { public void startPetrol() { System.out.println("Bad petrol engine starts."); } }
// Bad: Potentially misusing inheritance (Vehicle IS-A PetrolEngine? Not really)
/*
abstract class Vehicle {
// ... common vehicle properties ...
}
class CarExtendsPetrolEngine extends BadPetrolEngine { // Car IS-A PetrolEngine? Incorrect modeling.
public void drive() { System.out.println("Driving car that IS-A PetrolEngine."); }
}
*/
```
## Rule 3: Master Encapsulation, Inheritance, and Polymorphism
Title: Effectively Utilize Core Object-Oriented Concepts
Description: Encapsulation, Inheritance, and Polymorphism are the three pillars of object-oriented programming.
### Sub-Rule 3.1: Encapsulation
Title: Protect Internal State and Implementation Details
Description:
- Hide the internal state (fields) and implementation details of an object from the outside world.
- Expose a well-defined public interface (methods) for interacting with the object.
- Use access modifiers (`private`, `protected`, `default/package-private`, `public`) effectively to control visibility and protect invariants.
**Good example:**
```java
class BankAccount {
private double balance; // Encapsulated: internal state is private
private final String accountNumber;
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
if (initialBalance < 0) throw new IllegalArgumentException("Initial balance cannot be negative.");
this.balance = initialBalance;
}
// Public interface to interact with the balance
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit amount must be positive.");
this.balance += amount;
System.out.println("Deposited: " + amount + ", New Balance: " + this.balance);
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Withdrawal amount must be positive.");
if (amount > this.balance) throw new IllegalArgumentException("Insufficient funds.");
this.balance -= amount;
System.out.println("Withdrew: " + amount + ", New Balance: " + this.balance);
}
public double getBalance() { // Controlled access to balance
return this.balance;
}
public String getAccountNumber() { return this.accountNumber; }
}
```
**Bad Example:**
```java
// Bad: Poor encapsulation, exposing internal state
class UnsafeBankAccount {
public double balance; // Public field: internal state exposed and can be freely modified
public String accountNumber;
public UnsafeBankAccount(String accNum, double initial) { this.accountNumber = accNum; this.balance = initial; }
// No methods to control how balance is changed, invariants can be broken.
}
public class BadEncapsulationExample {
public static void main(String args) {
UnsafeBankAccount account = new UnsafeBankAccount("123", 100.0);
account.balance = -500.0; // Direct modification, potentially breaking business rules
System.out.println("Unsafe balance: " + account.balance);
}
}
```
### Sub-Rule 3.2: Inheritance
Title: Model "is-a" Relationships and Ensure LSP
Description:
- Use inheritance to model true "is-a" relationships, where a subclass is a more specific type of its superclass.
- Ensure that the Liskov Substitution Principle (LSP) is followed: subclasses must be substitutable for their base types without altering the correctness of the program.
- Be cautious of deep or wide inheritance hierarchies, as they can become complex, hard to maintain, and may indicate a need for composition or different abstractions.
**Good example:**
(See LSP good example under Rule 1.3, or consider this Animal example)
```java
abstract class Animal {
private String name;
public Animal(String name) { this.name = name; }
public String getName() { return name; }
public abstract void makeSound(); // Abstract method for polymorphism
}
class Dog extends Animal { // Dog IS-A Animal
public Dog(String name) { super(name); }
@Override public void makeSound() { System.out.println(getName() + " says: Woof!"); }
public void fetch() { System.out.println(getName() + " is fetching."); }
}
class Cat extends Animal { // Cat IS-A Animal
public Cat(String name) { super(name); }
@Override public void makeSound() { System.out.println(getName() + " says: Meow!"); }
public void purr() { System.out.println(getName() + " is purring."); }
}
public class InheritanceExample {
public static void main(String args) {
Animal myDog = new Dog("Buddy");
Animal myCat = new Cat("Whiskers");
myDog.makeSound();
myCat.makeSound();
// ((Dog)myDog).fetch(); // Can cast if sure of type to access specific methods
}
}
```
**Bad Example:**
(See LSP bad example under Rule 1.3, or a fragile base class example)
```java
// Bad: Fragile base class or incorrect "is-a" relationship
class Window {
public void open() { System.out.println("Window opened."); }
public void close() { System.out.println("Window closed."); }
}
// class CarDoor extends Window { /* A CarDoor IS-A Window? Not really. It has a window, but isn't one itself.
// This leads to inheriting methods that might not make sense (e.g. a CarDoor might have a window that opens/closes,
// but the door itself doesn't open/close in the same way a house window does).
// This is better modeled with composition: CarDoor HAS-A WindowComponent. */
// }
class BetterCarDoor {
private WindowComponent window = new WindowComponent();
public void openDoor() { System.out.println("Car door opened."); }
public void closeDoor() { System.out.println("Car door closed."); }
public void openWindow() { window.open(); }
public void closeWindow() { window.close(); }
static class WindowComponent { /* Similar to Window */
public void open() {System.out.println("Car window rolling down.");}
public void close() {System.out.println("Car window rolling up.");}
}
}
```
### Sub-Rule 3.3: Polymorphism
Title: Enable Objects to Respond to the Same Message Differently
Description:
- Polymorphism allows objects of different classes (that share a common superclass or interface) to respond to the same message (method call) in their own specific ways.
- It is primarily leveraged through inheritance (method overriding) and interfaces (implementing interface methods).
- Polymorphism simplifies client code, as it can interact with different types of objects through a common interface without needing to know their concrete types.
**Good example:**
```java
interface Drawable {
void draw();
}
class CircleShape implements Drawable {
@Override public void draw() { System.out.println("Drawing a Circle: O"); }
}
class SquareShape implements Drawable {
@Override public void draw() { System.out.println("Drawing a Square: "); }
}
class TriangleShape implements Drawable {
@Override public void draw() { System.out.println("Drawing a Triangle: /\\"); }
}
public class PolymorphismExample {
public static void drawShapes(List<Drawable> shapes) {
for (Drawable shape : shapes) {
shape.draw(); // Polymorphic call: actual method executed depends on shape's concrete type
}
}
public static void main(String args) {
List<Drawable> myShapes = List.of(
new CircleShape(),
new SquareShape(),
new TriangleShape()
);
drawShapes(myShapes);
}
}
```
**Bad Example:**
```java
// Bad: Lacking polymorphism, using type checking and casting
class ShapeDrawer {
public void drawSpecificShape(Object shape) {
if (shape instanceof CircleShapeBad) {
((CircleShapeBad) shape).drawCircle();
} else if (shape instanceof SquareShapeBad) {
((SquareShapeBad) shape).drawSquare();
} else if (shape instanceof TriangleShapeBad) {
((TriangleShapeBad) shape).drawTriangle();
} else {
System.out.println("Unknown shape type.");
}
// This is not polymorphic. Adding new shapes requires modifying this method.
}
}
class CircleShapeBad { public void drawCircle() { System.out.println("Drawing Circle (Bad)."); } }
class SquareShapeBad { public void drawSquare() { System.out.println("Drawing Square (Bad)."); } }
class TriangleShapeBad { public void drawTriangle() { System.out.println("Drawing Triangle (Bad)."); } }
```
## Rule 4: Identify and Refactor Object-Oriented Design Code Smells
Title: Recognize and Address Common OOD Code Smells
Description: Code smells are symptoms of potential underlying problems in the design. Recognizing and refactoring them can significantly improve code quality.
### Sub-Rule 4.1: Large Class / God Class
Title: A class that knows or does too much.
Description: Such classes violate SRP and are hard to understand, maintain, and test. Consider breaking them down into smaller, more focused classes.
**Good example:** (Separated responsibilities - see SRP Good Example)
**Bad Example:** (A single class doing parsing, validation, persistence, notification - see SRP Bad Example)
### Sub-Rule 4.2: Feature Envy
Title: A method that seems more interested in a class other than the one it actually is in.
Description: This often means the method is using data from another class more than its own. Consider moving the method to the class it's "envious" of, or introduce a new class to mediate.
**Good example:**
```java
class Customer {
private String name;
private Address address;
public Customer(String name, Address address) { this.name = name; this.address = address; }
public String getFullAddressDetails() { // Method operates on its own Address object
return address.getStreet() + ", " + address.getCity() + ", " + address.getZipCode();
}
}
class Address {
private String street, city, zipCode;
public Address(String s, String c, String z) { street=s; city=c; zipCode=z; }
public String getStreet() { return street; }
public String getCity() { return city; }
public String getZipCode() { return zipCode; }
}
```
**Bad Example:**
```java
class Order {
private double amount;
private Customer customer; // Has a Customer
public Order(double amount, Customer customer) { this.amount = amount; this.customer = customer; }
// Bad: This method is more interested in Customer's Address than Order itself
public String getCustomerShippingLabel() {
Address addr = customer.getAddress(); // Assuming Customer has getAddress()
return customer.getName() + "\n" + addr.getStreet() +
"\n" + addr.getCity() + ", " + addr.getZipCode();
// Better: Move this logic to Customer class as getShippingLabel() or similar.
}
}
// Assume Customer and Address classes from previous example
```
### Sub-Rule 4.3: Inappropriate Intimacy
Title: Classes that spend too much time delving into each other's private parts.
Description: This indicates tight coupling and poor encapsulation. Classes should interact through well-defined public interfaces, not by accessing internal implementation details of others.
**Good example:** (Classes interact via public methods - see Encapsulation Good Example)
**Bad Example:**
```java
class ServiceA {
public int internalCounter = 0; // Public field, bad
public void doSomething() { internalCounter++; }
}
class ServiceB {
public void manipulateServiceA(ServiceA serviceA) {
// Bad: Directly accessing and modifying internal state of ServiceA
serviceA.internalCounter = 100;
System.out.println("ServiceA counter directly set to: " + serviceA.internalCounter);
// Better: ServiceA should have a method like resetCounter(int value) if this is valid behavior.
}
}
```
### Sub-Rule 4.4: Refused Bequest
Title: A subclass uses only some of the methods and properties inherited from its parents, or overrides them to do nothing or throw exceptions.
Description: This might indicate a violation of LSP or an incorrect inheritance hierarchy. The subclass might not truly be a substitutable type of the superclass.
**Good example:** (Subclass meaningfully uses/extends inherited features - see Inheritance Good Example or LSP Good Example for Ostrich)
**Bad Example:** (See LSP Bad Example with Penguin not being able to fly)
### Sub-Rule 4.5: Shotgun Surgery
Title: When a single conceptual change requires modifications in many different classes.
Description: This often indicates that a single responsibility has been spread too thinly across multiple classes, leading to high coupling and difficulty in making changes.
**Good example:** (A change in tax calculation logic only requires modification in a `TaxCalculator` class, not in `Order`, `Product`, `Invoice` classes that use it.)
**Bad Example:** (If changing a discount rule requires updates in `ProductPage`, `ShoppingCart`, `CheckoutService`, and `OrderConfirmationEmail` classes, it's shotgun surgery.)
### Sub-Rule 4.6: Data Clumps
Title: Bunches of data items that regularly appear together in multiple places (e.g., parameters in multiple methods, fields in several classes).
Description: These data clumps often represent a missing concept that should be encapsulated into its own object or record.
**Good example:**
```java
// Good: Encapsulating related data into a Range object
record DateRange(LocalDate start, LocalDate end) {
public DateRange {
if (start.isAfter(end)) throw new IllegalArgumentException("Start date must be before end date.");
}
}
class EventScheduler {
public void scheduleEvent(String eventName, DateRange range) {
System.out.println("Scheduling " + eventName + " from " + range.start() + " to " + range.end());
}
public boolean isDateInRange(LocalDate date, DateRange range) {
return !date.isBefore(range.start()) && !date.isAfter(range.end());
}
}
```
**Bad Example:**
```java
// Bad: Data clump (startDay, startMonth, startYear, endDay, endMonth, endYear) passed around
class EventSchedulerBad {
public void scheduleEvent(String eventName,
int startDay, int startMonth, int startYear,
int endDay, int endMonth, int endYear) {
// ... logic using these separate date parts ...
System.out.println("Scheduling event with many date parameters.");
}
public boolean checkOverlap(int sDay1, int sMon1, int sYr1, int eDay1, int eMon1, int eYr1,
int sDay2, int sMon2, int sYr2, int eDay2, int eMon2, int eYr2) {
// ... complex logic with many parameters ...
return false;
}
// This pattern of passing around many related date parts is a data clump.
}
```
## Rule 5: Creating and Destroying Objects
Title: Best Practices for Object Creation and Destruction
Description: Effective object creation and destruction patterns improve code clarity, performance, and maintainability. These practices help avoid common pitfalls and leverage Java's capabilities effectively.
### Sub-Rule 5.1: Consider Static Factory Methods Instead of Constructors
Title: Use static factory methods to provide more flexibility than constructors
Description: Static factory methods offer advantages like descriptive names, ability to return existing instances, and flexibility in return types.
**Good example:**
```java
public class BigInteger {
// Static factory method with descriptive name
public static BigInteger valueOf(long val) {
if (val == 0) return ZERO; // Return cached instance
if (val > 0 && val <= MAX_CONSTANT) return posConst[(int) val];
return new BigInteger(val);
}
// Private constructor
private BigInteger(long val) { /* implementation */ }
private static final BigInteger ZERO = new BigInteger(0);
private static final BigInteger[] posConst = new BigInteger[MAX_CONSTANT + 1];
}
// Usage with clear intent
BigInteger zero = BigInteger.valueOf(0); // Clear what we're creating
BigInteger hundred = BigInteger.valueOf(100);
```
**Bad Example:**
```java
public class BigInteger {
// Only constructor available - less flexible
public BigInteger(long val) { /* implementation */ }
// Client code is less clear
BigInteger zero = new BigInteger(0); // Not clear this could be cached
BigInteger hundred = new BigInteger(100); // Creates new instance every time
}
```
### Sub-Rule 5.2: Consider a Builder When Faced with Many Constructor Parameters
Title: Use the Builder pattern for classes with multiple optional parameters
Description: The Builder pattern provides a readable alternative to telescoping constructors and is safer than JavaBeans pattern.
**Good example:**
```java
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) { calories = val; return this; }
public Builder fat(int val) { fat = val; return this; }
public Builder sodium(int val) { sodium = val; return this; }
public Builder carbohydrate(int val) { carbohydrate = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
// Usage - readable and flexible
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
```
**Bad Example:**
```java
// Telescoping constructor pattern - hard to read and error-prone
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
// Usage - confusing parameter order, easy to make mistakes
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27); // What do these numbers mean?
```
### Sub-Rule 5.3: Enforce the Singleton Property with a Private Constructor or an Enum Type
Title: Use enum or private constructor with static field for singletons
Description: Enum-based singletons are the best way to implement singletons, providing serialization and reflection safety.
**Good example:**
```java
// Enum singleton - preferred approach
public enum DatabaseConnection {
INSTANCE;
public void connect() {
System.out.println("Connecting to database...");
}
public void executeQuery(String query) {
System.out.println("Executing: " + query);
}
}
// Alternative: Static field with private constructor
public class Logger {
private static final Logger INSTANCE = new Logger();
private Logger() { /* private constructor */ }
public static Logger getInstance() {
return INSTANCE;
}
public void log(String message) {
System.out.println("LOG: " + message);
}
}
// Usage
DatabaseConnection.INSTANCE.connect();
Logger.getInstance().log("Application started");
```
**Bad Example:**
```java
// Not thread-safe singleton
public class BadSingleton {
private static BadSingleton instance;
private BadSingleton() {}
public static BadSingleton getInstance() {
if (instance == null) { // Race condition possible
instance = new BadSingleton();
}
return instance;
}
}
```
### Sub-Rule 5.4: Prefer Dependency Injection to Hardwiring Resources
Title: Use dependency injection instead of hardcoded dependencies
Description: Classes should not create their dependencies directly but receive them from external sources, improving testability and flexibility.
**Good example:**
```java
public class SpellChecker {
private final Lexicon dictionary;
// Dependency injected through constructor
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) {
return dictionary.contains(word);
}
}
interface Lexicon {
boolean contains(String word);
}
class EnglishLexicon implements Lexicon {
public boolean contains(String word) {
// English dictionary lookup
return true;
}
}
// Usage - flexible and testable
Lexicon englishDict = new EnglishLexicon();
SpellChecker checker = new SpellChecker(englishDict);
```
**Bad Example:**
```java
// Hardwired dependency - inflexible and hard to test
public class SpellChecker {
private static final Lexicon dictionary = new EnglishLexicon(); // Hardcoded
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) {
return dictionary.contains(word);
}
}
```
### Sub-Rule 5.5: Avoid Creating Unnecessary Objects
Title: Reuse objects when possible to improve performance
Description: Object creation can be expensive. Reuse immutable objects and avoid creating objects in loops when possible.
**Good example:**
```java
public class DateUtils {
// Reuse expensive objects
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
public String formatDate(LocalDate date) {
return FORMATTER.format(date); // Reuse formatter
}
// Use primitives when possible
public boolean isEven(int number) {
return number % 2 == 0; // No object creation
}
}
```
**Bad Example:**
```java
public class DateUtils {
public String formatDate(LocalDate date) {
// Creates new formatter every time - expensive
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
return formatter.format(date);
}
// Unnecessary autoboxing
public boolean isEven(Integer number) {
return number % 2 == 0; // Creates Integer objects
}
}
```
## Rule 6: Classes and Interfaces Best Practices
Title: Design Classes and Interfaces for Maximum Effectiveness
Description: Well-designed classes and interfaces are the foundation of maintainable and robust Java applications. These practices ensure proper encapsulation, inheritance, and interface design.
### Sub-Rule 6.1: Minimize the Accessibility of Classes and Members
Title: Use the most restrictive access level that makes sense
Description: Proper encapsulation hides implementation details and allows for easier maintenance and evolution of code.
**Good example:**
```java
public class BankAccount {
private final String accountNumber; // Private - implementation detail
private double balance; // Private - internal state
// Package-private for testing
static final double MINIMUM_BALANCE = 0.0;
public BankAccount(String accountNumber, double initialBalance) { // Public - part of API
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public double getBalance() { // Public - part of API
return balance;
}
public void deposit(double amount) { // Public - part of API
validateAmount(amount);
balance += amount;
}
private void validateAmount(double amount) { // Private - implementation detail
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
}
```
**Bad Example:**
```java
public class BankAccount {
public String accountNumber; // Should be private
public double balance; // Should be private
public static final double MINIMUM_BALANCE = 0.0; // Unnecessarily public
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public void validateAmount(double amount) { // Should be private
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
}
```
### Sub-Rule 6.2: In Public Classes, Use Accessor Methods, Not Public Fields
Title: Provide getter and setter methods instead of exposing fields directly
Description: Accessor methods provide flexibility to add validation, logging, or other logic without breaking clients.
**Good example:**
```java
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) {
// Can add validation or other logic
if (Double.isNaN(x)) {
throw new IllegalArgumentException("x cannot be NaN");
}
this.x = x;
}
public void setY(double y) {
if (Double.isNaN(y)) {
throw new IllegalArgumentException("y cannot be NaN");
}
this.y = y;
}
}
```
**Bad Example:**
```java
public class Point {
public double x; // Direct field access - no validation possible
public double y; // Cannot add logic later without breaking clients
public Point(double x, double y) {
this.x = x;
this.y = y;
}
}
```
### Sub-Rule 6.3: Minimize Mutability
Title: Make classes immutable when possible
Description: Immutable classes are simpler, safer, and can be freely shared. They are inherently thread-safe and have no temporal coupling.
**Good example:**
```java
public final class Complex {
private final double real;
private final double imaginary;
public Complex(double real, double imaginary) {
this.real = real;
this.imaginary = imaginary;
}
public double realPart() { return real; }
public double imaginaryPart() { return imaginary; }
// Operations return new instances instead of modifying
public Complex plus(Complex c) {
return new Complex(real + c.real, imaginary + c.imaginary);
}
public Complex minus(Complex c) {
return new Complex(real - c.real, imaginary - c.imaginary);
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Complex)) return false;
Complex c = (Complex) o;
return Double.compare(c.real, real) == 0 &&
Double.compare(c.imaginary, imaginary) == 0;
}
@Override
public int hashCode() {
return Objects.hash(real, imaginary);
}
}
```
**Bad Example:**
```java
public class Complex {
private double real; // Mutable fields
private double imaginary; // Mutable fields
public Complex(double real, double imaginary) {
this.real = real;
this.imaginary = imaginary;
}
public double getRealPart() { return real; }
public double getImaginaryPart() { return imaginary; }
// Mutating operations - not thread-safe, harder to reason about
public void plus(Complex c) {
this.real += c.real;
this.imaginary += c.imaginary;
}
public void setReal(double real) { this.real = real; }
public void setImaginary(double imaginary) { this.imaginary = imaginary; }
}
```
### Sub-Rule 6.4: Favor Composition Over Inheritance
Title: Use composition instead of inheritance when you want to reuse code
Description: Composition is more flexible than inheritance and avoids the fragility of inheritance hierarchies.
**Good example:**
```java
// Using composition
public class InstrumentedSet<E> {
private final Set<E> s;
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
this.s = s;
}
public boolean add(E e) {
addCount++;
return s.add(e);
}
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public int getAddCount() {
return addCount;
}
// Delegate other methods to the wrapped set
public int size() { return s.size(); }
public boolean isEmpty() { return s.isEmpty(); }
public boolean contains(Object o) { return s.contains(o); }
// ... other delegating methods
}
```
**Bad Example:**
```java
// Using inheritance - fragile and error-prone
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c); // This calls add() internally, double-counting!
}
public int getAddCount() {
return addCount;
}
}
```
### Sub-Rule 6.5: Design and Document for Inheritance or Else Prohibit It
Title: Either design classes specifically for inheritance or make them final
Description: Classes not designed for inheritance can break when subclassed. Document self-use patterns or prohibit inheritance.
**Good example:**
```java
// Designed for inheritance with proper documentation
public abstract class AbstractProcessor {
/**
* Processes the given data. This implementation calls {@link #validate(String)}
* followed by {@link #transform(String)}. Subclasses may override this method
* to provide different processing logic.
*
* @param data the data to process
* @return the processed result
* @throws IllegalArgumentException if data is invalid
*/
public String process(String data) {
validate(data);
return transform(data);
}
/**
* Validates the input data. The default implementation checks for null.
* Subclasses may override to provide additional validation.
*/
protected void validate(String data) {
if (data == null) {
throw new IllegalArgumentException("Data cannot be null");
}
}
/**
* Transforms the validated data. Subclasses must implement this method.
*/
protected abstract String transform(String data);
}
// Or prohibit inheritance
public final class UtilityClass {
private UtilityClass() { /* prevent instantiation */ }
public static String formatName(String firstName, String lastName) {
return firstName + " " + lastName;
}
}
```
**Bad Example:**
```java
// Not designed for inheritance but not prohibited
public class DataProcessor {
public String process(String data) {
// Complex logic that might break if overridden
String validated = validate(data);
String transformed = transform(validated);
return finalize(transformed);
}
private String validate(String data) { /* ... */ return data; }
private String transform(String data) { /* ... */ return data; }
private String finalize(String data) { /* ... */ return data; }
}
```
## Rule 7: Enums and Annotations
Title: Effective Use of Enums and Annotations
Description: Enums and annotations are powerful Java features that, when used correctly, can make code more readable, type-safe, and maintainable.
### Sub-Rule 7.1: Use Enums Instead of Int Constants
Title: Replace int constants with type-safe enums
Description: Enums provide type safety, namespace protection, and additional functionality that int constants cannot offer.
**Good example:**
```java
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
// Usage
double earthWeight = 175;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values()) {
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
```
**Bad Example:**
```java
// Int constants - not type-safe, no namespace
public class Planet {
public static final int MERCURY = 0;
public static final int VENUS = 1;
public static final int EARTH = 2;
public static final int MARS = 3;
// Separate arrays for data - error-prone
private static final double[] MASS = {3.302e+23, 4.869e+24, 5.975e+24, 6.419e+23};
private static final double[] RADIUS = {2.439e6, 6.052e6, 6.378e6, 3.393e6};
public static double surfaceWeight(int planet, double mass) {
// No compile-time checking - could pass any int
if (planet < 0 || planet >= MASS.length) {
throw new IllegalArgumentException("Invalid planet: " + planet);
}
// Complex calculations with array indexing
return mass * (6.67300E-11 * MASS[planet] / (RADIUS[planet] * RADIUS[planet]));
}
}
```
### Sub-Rule 7.2: Use Instance Fields Instead of Ordinals
Title: Don't derive values from enum ordinals; use instance fields
Description: Ordinal values can change when enum constants are reordered, making code fragile.
**Good example:**
```java
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
```
**Bad Example:**
```java
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal() + 1; // Fragile - breaks if order changes
}
}
```
### Sub-Rule 7.3: Use EnumSet Instead of Bit Fields
Title: Replace bit field enums with EnumSet for better type safety and performance
Description: EnumSet provides all the benefits of bit fields with better readability and type safety.
**Good example:**
```java
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// EnumSet - type-safe and efficient
public void applyStyles(Set<Style> styles) {
System.out.printf("Applying styles %s to text%n", styles);
// Implementation here
}
}
// Usage
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
```
**Bad Example:**
```java
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// Bit field - not type-safe
public void applyStyles(int styles) {
System.out.printf("Applying styles %s to text%n", styles);
// Implementation here
}
}
// Usage - error-prone, no type safety
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
```
### Sub-Rule 7.4: Use EnumMap Instead of Ordinal Indexing
Title: Use EnumMap for enum-keyed data instead of ordinal indexing
Description: EnumMap is specifically designed for enum keys and provides better performance and type safety.
**Good example:**
```java
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase, Transition>> m =
Stream.of(values()).collect(groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
toMap(t -> t.to, t -> t, (x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
```
**Bad Example:**
```java
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// Ordinal-based array - fragile and error-prone
private static final Transition[][] TRANSITIONS = {
{ null, MELT, SUBLIME }, // SOLID
{ FREEZE, null, BOIL }, // LIQUID
{ DEPOSIT, CONDENSE, null } // GAS
};
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
```
### Sub-Rule 7.5: Consistently Use the Override Annotation
Title: Always use @Override when overriding methods
Description: The @Override annotation catches errors at compile time and makes code more readable.
**Good example:**
```java
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
@Override
public boolean equals(Object o) { // Correct signature
if (!(o instanceof Bigram)) return false;
Bigram b = (Bigram) o;
return b.first == first && b.second == second;
}
@Override
public int hashCode() {
return 31 * first + second;
}
@Override
public String toString() {
return String.format("(%c, %c)", first, second);
}
}
```
**Bad Example:**
```java
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
// Missing @Override - typo in method signature won't be caught
public boolean equals(Bigram b) { // Wrong signature! Should be equals(Object)
return b.first == first && b.second == second;
}
public int hashCode() { // Missing @Override
return 31 * first + second;
}
}
```
## Rule 8: Method Design
Title: Design Methods for Clarity, Safety, and Usability
Description: Well-designed methods are the building blocks of maintainable code. These practices ensure methods are robust, clear, and easy to use correctly.
### Sub-Rule 8.1: Check Parameters for Validity
Title: Validate method parameters early and clearly
Description: Fail fast by checking parameters at the beginning of methods. This makes debugging easier and prevents corruption of object state.
**Good example:**
```java
public class MathUtils {
/**
* Returns a BigInteger whose value is (this mod m).
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m <= 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0) {
throw new ArithmeticException("Modulus <= 0: " + m);
}
// ... do the computation
return this;
}
/**
* Returns the index of the first occurrence of needle in haystack,
* or -1 if needle is not contained in haystack.
* @param haystack the string to search in
* @param needle the string to search for
* @throws NullPointerException if haystack or needle is null
*/
public static int indexOf(String haystack, String needle) {
Objects.requireNonNull(haystack, "haystack");
Objects.requireNonNull(needle, "needle");
// ... do the search
return haystack.indexOf(needle);
}
}
```
**Bad Example:**
```java
public class MathUtils {
public BigInteger mod(BigInteger m) {
// No parameter validation - could cause confusing errors later
// ... do the computation
return this;
}
public static int indexOf(String haystack, String needle) {
// No null checks - will throw NullPointerException at some random point
return haystack.indexOf(needle);
}
}
```
### Sub-Rule 8.2: Make Defensive Copies When Needed
Title: Protect against malicious or accidental modification of mutable parameters
Description: When accepting mutable objects as parameters or returning them, make defensive copies to maintain class invariants.
**Good example:**
```java
public final class Period {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime()); // Defensive copy
this.end = new Date(end.getTime()); // Defensive copy
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(this.start + " after " + this.end);
}
}
public Date start() {
return new Date(start.getTime()); // Defensive copy on return
}
public Date end() {
return new Date(end.getTime()); // Defensive copy on return
}
}
```
**Bad Example:**
```java
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
this.start = start; // No defensive copy - client can modify after construction
this.end = end; // No defensive copy - client can modify after construction
}
public Date start() {
return start; // No defensive copy - client can modify internal state
}
public Date end() {
return end; // No defensive copy - client can modify internal state
}
}
```
### Sub-Rule 8.3: Design Method Signatures Carefully
Title: Choose method names carefully and avoid long parameter lists
Description: Good method signatures are self-documenting and hard to use incorrectly.
**Good example:**
```java
public class UserService {
// Clear, descriptive method names
public User createUser(String username, String email, LocalDate birthDate) {
// Implementation
return new User(username, email, birthDate);
}
// Use builder pattern for many parameters
public static class UserBuilder {
private String username;
private String email;
private LocalDate birthDate;
private String firstName;
private String lastName;
private Address address;
public UserBuilder username(String username) { this.username = username; return this; }
public UserBuilder email(String email) { this.email = email; return this; }
public UserBuilder birthDate(LocalDate birthDate) { this.birthDate = birthDate; return this; }
public UserBuilder firstName(String firstName) { this.firstName = firstName; return this; }
public UserBuilder lastName(String lastName) { this.lastName = lastName; return this; }
public UserBuilder address(Address address) { this.address = address; return this; }
public User build() {
return new User(this);
}
}
// Use helper classes to group related parameters
public void updateUserProfile(User user, ProfileUpdate update) {
// Implementation
}
}
class ProfileUpdate {
private final String firstName;
private final String lastName;
private final Address address;
// Constructor and getters
}
```
**Bad Example:**
```java
public class UserService {
// Unclear method name and too many parameters
public User doUserStuff(String s1, String s2, int d, int m, int y,
String s3, String s4, String s5, String s6, String s7) {
// What do these parameters mean?
return new User(s1, s2, LocalDate.of(y, m, d));
}
// Ambiguous parameter types
public void updateUser(String username, String data) {
// What kind of data? How is it formatted?
}
}
```
### Sub-Rule 8.4: Return Empty Collections or Arrays, Not Nulls
Title: Never return null from methods that return collections or arrays
Description: Returning null forces clients to handle null checks and is a common source of bugs.
**Good example:**
```java
public class ShoppingCart {
private final List<Item> items = new ArrayList<>();
/**
* Returns a list of items in the cart.
* @return the items in the cart (never null, but may be empty)
*/
public List<Item> getItems() {
return new ArrayList<>(items); // Return copy of list, never null
}
/**
* Returns items matching the given category.
* @param category the category to filter by
* @return matching items (never null, but may be empty)
*/
public List<Item> getItemsByCategory(String category) {
return items.stream()
.filter(item -> category.equals(item.getCategory()))
.collect(Collectors.toList()); // Returns empty list if no matches
}
/**
* Returns an array of item names.
* @return array of item names (never null, but may be empty)
*/
public String[] getItemNames() {
return items.stream()
.map(Item::getName)
.toArray(String[]::new); // Returns empty array if no items
}
}
```
**Bad Example:**
```java
public class ShoppingCart {
private final List<Item> items = new ArrayList<>();
public List<Item> getItems() {
return items.isEmpty() ? null : new ArrayList<>(items); // Bad: returns null
}
public List<Item> getItemsByCategory(String category) {
List<Item> result = items.stream()
.filter(item -> category.equals(item.getCategory()))
.collect(Collectors.toList());
return result.isEmpty() ? null : result; // Bad: returns null
}
public String[] getItemNames() {
if (items.isEmpty()) {
return null; // Bad: returns null instead of empty array
}
return items.stream()
.map(Item::getName)
.toArray(String[]::new);
}
}
```
### Sub-Rule 8.5: Return Optionals Judiciously
Title: Use Optional for methods that may not return a value, but use it carefully
Description: Optional is intended for return types where there might legitimately be no result and the client needs to perform special processing.
**Good example:**
```java
public class UserRepository {
private final Map<String, User> users = new HashMap<>();
/**
* Finds a user by username.
* @param username the username to search for
* @return an Optional containing the user if found, empty otherwise
*/
public Optional<User> findByUsername(String username) {
return Optional.ofNullable(users.get(username));
}
/**
* Gets the maximum age among all users.
* @return an Optional containing the max age if users exist, empty otherwise
*/
public OptionalInt getMaxAge() {
return users.values().stream()
.mapToInt(User::getAge)
.max();
}
}
// Usage
Optional<User> user = repository.findByUsername("john");
if (user.isPresent()) {
System.out.println("Found user: " + user.get().getName());
} else {
System.out.println("User not found");
}
// Or with functional style
repository.findByUsername("john")
.ifPresentOrElse(
u -> System.out.println("Found: " + u.getName()),
() -> System.out.println("User not found")
);
```
**Bad Example:**
```java
public class UserRepository {
private final Map<String, User> users = new HashMap<>();
// Bad: Using Optional for fields
private Optional<String> defaultUsername = Optional.empty();
// Bad: Using Optional for parameters
public void updateUser(Optional<String> username, Optional<String> email) {
// This makes the API harder to use
}
// Bad: Using Optional for collections
public Optional<List<User>> getAllUsers() {
return users.isEmpty() ? Optional.empty() : Optional.of(new ArrayList<>(users.values()));
// Should just return empty list instead
}
// Bad: Optional in performance-critical code where null would be fine
public Optional<User> findByUsernameInLoop(String username) {
// If this is called in a tight loop, the Optional allocation overhead matters
return Optional.ofNullable(users.get(username));
}
}
```
## Rule 9: Exception Handling
Title: Handle Exceptions Effectively and Appropriately
Description: Proper exception handling makes code more robust and easier to debug. These practices ensure exceptions are used correctly and provide meaningful information.
### Sub-Rule 9.1: Use Exceptions Only for Exceptional Conditions
Title: Don't use exceptions for ordinary control flow
Description: Exceptions should be used for exceptional conditions, not for normal program flow. They are expensive and make code harder to understand.
**Good example:**
```java
public class NumberProcessor {
public void processNumbers(int[] numbers) {
for (int number : numbers) { // Normal iteration
if (isValid(number)) { // Normal condition checking
process(number);
} else {
System.out.println("Skipping invalid number: " + number);
}
}
}
private boolean isValid(int number) {
return number >= 0 && number <= 1000;
}
private void process(int number) {
// Process the number
System.out.println("Processing: " + number);
}
}
```
**Bad Example:**
```java
public class NumberProcessor {
public void processNumbers(int[] numbers) {
try {
int i = 0;
while (true) { // Using exception for loop termination
process(numbers[i++]);
}
} catch (ArrayIndexOutOfBoundsException e) {
// Using exception for normal control flow - bad!
}
}
private void process(int number) {
System.out.println("Processing: " + number);
}
}
```
### Sub-Rule 9.2: Use Checked Exceptions for Recoverable Conditions and Runtime Exceptions for Programming Errors
Title: Choose the right type of exception for the situation
Description: Checked exceptions force the caller to handle recoverable conditions, while runtime exceptions indicate programming errors.
**Good example:**
```java
public class FileProcessor {
/**
* Processes a file. Throws checked exception for recoverable I/O issues.
*/
public void processFile(String filename) throws FileProcessingException {
try {
// File operations that might fail due to external factors
Files.readAllLines(Paths.get(filename));
} catch (IOException e) {
// Wrap in domain-specific checked exception
throw new FileProcessingException("Failed to process file: " + filename, e);
}
}
/**
* Validates input parameters. Throws runtime exception for programming errors.
*/
public void validateInput(String input) {
if (input == null) {
// Programming error - should never happen in correct code
throw new IllegalArgumentException("Input cannot be null");
}
if (input.trim().isEmpty()) {
// Programming error - caller should validate before calling
throw new IllegalArgumentException("Input cannot be empty");
}
}
}
// Custom checked exception for recoverable conditions
class FileProcessingException extends Exception {
public FileProcessingException(String message, Throwable cause) {
super(message, cause);
}
}
```
**Bad Example:**
```java
public class FileProcessor {
// Bad: Using checked exception for programming error
public void validateInput(String input) throws ValidationException {
if (input == null) {
throw new ValidationException("Input cannot be null"); // Should be RuntimeException
}
}
// Bad: Using runtime exception for recoverable condition
public void processFile(String filename) {
try {
Files.readAllLines(Paths.get(filename));
} catch (IOException e) {
// Should be checked exception so caller can handle
throw new RuntimeException("File processing failed", e);
}
}
}
```
### Sub-Rule 9.3: Favor the Use of Standard Exceptions
Title: Use standard Java exceptions when appropriate
Description: Standard exceptions are familiar to developers and have clear semantics. Don't reinvent the wheel.
**Good example:**
```java
public class Calculator {
public double divide(double dividend, double divisor) {
if (divisor == 0.0) {
throw new ArithmeticException("Division by zero"); // Standard exception
}
return dividend / divisor;
}
public int getElement(List<Integer> list, int index) {
Objects.requireNonNull(list, "list"); // Standard NullPointerException
if (index < 0 || index >= list.size()) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + list.size());
}
return list.get(index);
}
public void processPositiveNumber(int number) {
if (number <= 0) {
throw new IllegalArgumentException("Number must be positive: " + number);
}
// Process the number
}
}
```
**Bad Example:**
```java
public class Calculator {
public double divide(double dividend, double divisor) {
if (divisor == 0.0) {
throw new DivisionByZeroException("Cannot divide by zero"); // Custom exception when standard would do
}
return dividend / divisor;
}
public int getElement(List<Integer> list, int index) {
if (list == null) {
throw new ListIsNullException("List cannot be null"); // Custom exception when standard would do
}
if (index < 0 || index >= list.size()) {
throw new InvalidIndexException("Bad index: " + index); // Custom exception when standard would do
}
return list.get(index);
}
}
// Unnecessary custom exceptions
class DivisionByZeroException extends RuntimeException {
public DivisionByZeroException(String message) {
super(message);
}
}
class ListIsNullException extends RuntimeException {
public ListIsNullException(String message) {
super(message);
}
}
class InvalidIndexException extends RuntimeException {
public InvalidIndexException(String message) {
super(message);
}
}
```
### Sub-Rule 9.4: Include Failure-Capture Information in Detail Messages
Title: Provide detailed, actionable information in exception messages
Description: Exception messages should contain all information needed to diagnose the failure.
**Good example:**
```java
public class BankAccount {
private double balance;
private final String accountNumber;
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
String.format("Withdrawal amount must be positive. Account: %s, Amount: %.2f",
accountNumber, amount));
}
if (amount > balance) {
throw new InsufficientFundsException(
String.format("Insufficient funds. Account: %s, Balance: %.2f, Requested: %.2f",
accountNumber, balance, amount));
}
balance -= amount;
}
public void transfer(BankAccount toAccount, double amount) {
if (toAccount == null) {
throw new IllegalArgumentException(
String.format("Destination account cannot be null. Source account: %s, Amount: %.2f",
accountNumber, amount));
}
if (toAccount.accountNumber.equals(this.accountNumber)) {
throw new IllegalArgumentException(
String.format("Cannot transfer to same account. Account: %s, Amount: %.2f",
accountNumber, amount));
}
withdraw(amount);
toAccount.deposit(amount);
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
String.format("Deposit amount must be positive. Account: %s, Amount: %.2f",
accountNumber, amount));
}
balance += amount;
}
}
class InsufficientFundsException extends RuntimeException {
public InsufficientFundsException(String message) {
super(message);
}
}
```
**Bad Example:**
```java
public class BankAccount {
private double balance;
private final String accountNumber;
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Invalid amount"); // Too vague
}
if (amount > balance) {
throw new InsufficientFundsException("Not enough money"); // No specific details
}
balance -= amount;
}
public void transfer(BankAccount toAccount, double amount) {
if (toAccount == null) {
throw new IllegalArgumentException("Bad account"); // No context
}
if (toAccount.accountNumber.equals(this.accountNumber)) {
throw new IllegalArgumentException("Error"); // Completely unhelpful
}
withdraw(amount);
toAccount.deposit(amount);
}
}
```
### Sub-Rule 9.5: Don't Ignore Exceptions
Title: Always handle exceptions appropriately, never ignore them silently
Description: Ignoring exceptions can hide bugs and make debugging extremely difficult.
**Good example:**
```java
public class FileManager {
private static final Logger logger = LoggerFactory.getLogger(FileManager.class);
public Optional<String> readFileContent(String filename) {
try {
return Optional.of(Files.readString(Paths.get(filename)));
} catch (IOException e) {
// Log the exception with context
logger.warn("Failed to read file: {}", filename, e);
return Optional.empty(); // Return meaningful result
}
}
public void saveToFile(String filename, String content) throws FileOperationException {
try {
Files.writeString(Paths.get(filename), content);
logger.info("Successfully saved content to file: {}", filename);
} catch (IOException e) {
// Re-throw as domain-specific exception with context
throw new FileOperationException("Failed to save content to file: " + filename, e);
}
}
public void cleanupTempFiles(List<String> tempFiles) {
for (String tempFile : tempFiles) {
try {
Files.deleteIfExists(Paths.get(tempFile));
} catch (IOException e) {
// Log but continue with other files
logger.warn("Failed to delete temporary file: {}", tempFile, e);
}
}
}
}
class FileOperationException extends Exception {
public FileOperationException(String message, Throwable cause) {
super(message, cause);
}
}
```
**Bad Example:**
```java
public class FileManager {
public String readFileContent(String filename) {
try {
return Files.readString(Paths.get(filename));
} catch (IOException e) {
// Silently ignoring exception - very bad!
}
return null; // Caller has no idea what went wrong
}
public void saveToFile(String filename, String content) {
try {
Files.writeString(Paths.get(filename), content);
} catch (IOException e) {
// Empty catch block - hiding the problem
}
}
public void processFiles(List<String> files) {
for (String file : files) {
try {
// Some file processing
Files.readString(Paths.get(file));
} catch (Exception e) {
// Catching Exception is too broad, and then ignoring it
}
}
}
}
```
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
Posts are public.Sign in to post
No one has posted yet. Be the first.

