Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

8 Best Coding Practices for Creating and Destroying Objects in Java

Tech May 19 1

Use static factory methods instead of public constructors

Static factory methods offer multiple advantages over traditional constructors:

  1. They have descriptive names that make code more readable and self-documenting. For example, a method named getInstanceByCode makes its purpose immediately clear, unlike an overloaded constructor with the same parameters.
  2. 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;
}
  1. 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);
}
  1. They can return different implementations based on input parameters.
public static <T extends BaseStrategy> T getStrategy(String key) {
    return (T) strategyRegistry.get(key);
}
  1. 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();
    }
}
Tags: Java

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.