8 Best Coding Practices for Creating and Destroying Objects in Java
Use static factory methods instead of public constructors
Static factory methods offer multiple advantages over traditional constructors:
- They have descriptive names that make code more readable and self-documenting. For example, a method named
getInstanceByCodemakes its purpose immediately clear, unlike an overloaded constructor with the same parameters. - They allow reusing existing instances instead of creating new ones, enabling patterns like singleton and flyweight.
Boolean cachedFlag = Boolean.valueOf(true);
// Implementation inside Boolean class
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
- They can return a subtype of the declared return type, giving more flexibility for future changes.
List<String> singleItemList = Collections.singletonList("example-item");
// Implementation returns a private inner subclass of List
public static <E> List<E> singletonList(E element) {
return new SingletonList<>(element);
}
- They can return different implementations based on input parameters.
public static <T extends BaseStrategy> T getStrategy(String key) {
return (T) strategyRegistry.get(key);
}
- The class of the returned object does not need to exist at compile time when the factory is written, enabling dynamic loading via reflection. The JDBC API is a classic example of this:
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass);
Common use cases include Spring's getBean() method and strategy pattern factories, which leverage all 5 benefits listed above.
Use the builder pattern when you have many constructor parameters
When a class requires multiple optional parametesr, telescoping constructors lead to confusing hard-to-read code that easily causes parameter order mistakes. The builder pattern solves this problem by letting you set parameters with clearly named method calls.
// Telescoping constructor error-prone for many parameters
Student alice = new Student("Alice", 21, "Female", "New York", "1234567890", "alice@example.com", "123456");
// Builder pattern, clear and readable
Student bob = Student.builder()
.name("Bob")
.age(22)
.sex("Male")
.address("Boston")
.phone("9876543210")
.email("bob@example.com")
.qq("987654")
.build();
The builder pattern improves readability significantly, and prevents object escape during the construction process.
Enforce singleton behavior with private constructors or enums
To enforce singleton (one and only one instance of a class), first make the constructor private to block accidental instantiation. Common implementations use either a public static final field, a static factory method, or a single-element enum.
// Public field singleton
SingletonField instance = SingletonField.INSTANCE;
// Static factory singleton
SingletonFactory instance = SingletonFactory.getInstance();
// Enum singleton
SingletonEnum instance = SingletonEnum.INSTANCE;
Traditional singleton implementations can be broken by reflection attacks and serialization. The enum approach avoids this issue, as the reflection API specifically blocks instantiating enums via reflection:
if ((clazz.getModifiers() & Modifier.ENUM) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
}
Prefer dependency injection for variable dependencies
If a class depends on an external resource or service that can have multiple implementations, do not hardcode the instantiation of the dependency inside your class. Instead, inject the dependency through the constructor, making you're code flexible, testable, and reusable.
// Bad: hardcoded dependency, cannot swap implementations for testing
class OrderService {
private PaymentProcessor processor = new CreditCardProcessor();
}
// Good: constructor injection, flexible to any implementation
class OrderService {
private final PaymentProcessor processor;
public OrderService(PaymentProcessor processor) {
this.processor = processor;
}
}
For large applications with many dependencies, use a dependency injection framework like Spring IoC to manage dependencies automatically.
Avoid creating unnecessary objects
Unnecessary objects waste memory and increase garbage collection overhead, so you should always prefer reusing existing objects when possible.
// Bad: creates a redundant new String object, unnecessary
String badName = new String("My Application");
// Good: reuses the interned string literal
String goodName = "My Application";
A common source of unnecessary objects is unintentional auto-boxing of primitive types:
// Bad: sum is a Long wrapper, auto-boxing creates thousands of unnecessary objects
Long total = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
total += i;
}
// Good: use primitive long to avoid unnecessary object creation
long total = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
total += i;
}
Use patterns like singleton and flyweight to reuse immutable objects such as strings and boxed primitives.
Eliminate obsolete object references to prevent memory leaks
Even though Jaava has automatic garbage collection, memory leaks can still occur when you retain references to objects that are no longer needed. A common example is a custom stack implementation:
public <T> T pop() {
if (currentSize == 0) {
throw new EmptyStackException();
}
T result = backingArray[--currentSize];
// Null out the obsolete reference to allow garbage collection
backingArray[currentSize] = null;
return result;
}
Other common sources of memory leaks include long-lived caches and event listeners. Use weak references for cache entries or explicitly remove unused listeners to avoid this issue.
Do not use the finalize() method for resource cleanup
The finalize() method is executed by a daemon thread, and there is no guarantee on when (or even if) it will run. It is unpredictable and unsuitable for cleaning up critical resources. Always use explicit cleanup methods like close() with try-finally or try-with-resources instead.
Prefer try-with-resources over try-finally for resource management
For resources that need to be closed after use (like files, network connections, or database connections), Java 7+'s try-with-resources statement is far better than the traditional try-finally approach. It automatically closes resources when you are done with them, eliminating boilerplate and preventing forgotten closes.
// try-finally approach, verbose and error-prone
public String readFirstLine(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
// try-with-resources approach, concise and safe
public String readFirstLine(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}