- Learn when to use different access modifiers
- Understand encapsulation patterns and anti-patterns
- Master best practices for class API design
Access Modifiers Best Practices in Java
Access modifiers give you the tools for encapsulation. Using them well means knowing when to be restrictive and when to open up access.
Choosing the Right Access Modifier
Start with Private
Always default to private for fields and methods unless you have a good reason to be more permissive.
public class BankAccount {
private String accountNumber; // Private by default
private double balance;
// Only expose what's necessary
public double getBalance() { return balance; }
public void deposit(double amount) { /* implementation */ }
}
When to Use Protected
Use protected sparingly, mainly for inheritance hierarchies where subclasses need access to parent implementation details.
public class Shape {
protected double area; // Subclasses need access for calculations
protected void calculateArea() {
// Common area calculation logic
}
}
public class Circle extends Shape {
private double radius;
@Override
protected void calculateArea() {
this.area = Math.PI * radius * radius;
}
}
When to Use Public
Make methods and constructors public when they are part of the class's public API.
public class Calculator {
// Public API methods
public int add(int a, int b) { return a + b; }
public int subtract(int a, int b) { return a - b; }
// Constructor should be public for instantiation
public Calculator() { }
}
Encapsulation Patterns
Data Transfer Objects (DTOs)
For simple data containers, consider public final fields (immutable DTOs):
public class UserDTO {
public final String username;
public final String email;
public UserDTO(String username, String email) {
this.username = username;
this.email = email;
}
}
Builder Pattern
For objects with many optional parameters:
public class Computer {
private final String cpu;
private final int ram;
private final int storage;
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
}
public static class Builder {
// All fields have default access within the same package
String cpu = "Intel i5";
int ram = 8;
int storage = 256;
public Builder cpu(String cpu) { this.cpu = cpu; return this; }
public Builder ram(int ram) { this.ram = ram; return this; }
public Builder storage(int storage) { this.storage = storage; return this; }
public Computer build() { return new Computer(this); }
}
}
Common Anti-Patterns to Avoid
1. Making Everything Public
// Anti-pattern: Breaks encapsulation
public class BadExample {
public String internalData;
public void internalLogic() { }
}
2. Getters and Setters for Everything
// Sometimes unnecessary if no additional logic is needed
public class SimpleDTO {
private String name;
public String getName() { return name; } // Unnecessary if just storing data
public void setName(String name) { this.name = name; }
}
3. Inconsistent Access Levels
public class Inconsistent {
public String publicField; // Should be private
private void privateMethod() {
// Implementation
}
public void publicMethod() {
privateMethod(); // Inconsistent - public method calls private
}
}
Package Organization
Package-Private Classes
Use package-private classes for internal implementation:
// In com.example.util
class HelperUtils { // Package-private
static boolean isValidEmail(String email) {
return email.contains("@");
}
}
// In com.example.service
public class UserService {
public void createUser(String email) {
if (HelperUtils.isValidEmail(email)) {
// Create user
}
}
}
Inheritance Considerations
Protected vs Package-Private
Choose based on whether subclasses in other packages need access:
public class Parent {
// Use protected if external subclasses need access
protected void customizableMethod() { }
// Use package-private if only same-package subclasses need access
void packageMethod() { }
}
Testing and Access Modifiers
Package-Private for Testing
Make methods package-private when they need to be tested but shouldn't be public:
public class Calculator {
// Package-private for unit testing
double calculateTax(double amount) {
return amount * 0.08;
}
public double getTotalWithTax(double amount) {
return amount + calculateTax(amount);
}
}
Refactoring Access Modifiers
Start Restrictive, Loosen as Needed
- Begin with
private - Change to
package-privateif same-package classes need access - Change to
protectedif subclasses need access - Change to
publiconly for true public API
IDE Support
Modern IDEs help identify when access modifiers can be more restrictive:
- IntelliJ IDEA: "Make method/package-private" inspections
- Eclipse: Access modifier warnings
Good access modifier use means balancing encapsulation with practicality. Start with private and only loosen when necessary. Think about the class's responsibilities, inheritance needs, and how you'll test it. Well-chosen access modifiers make your code more maintainable and secure.
