Designing Immutable Classes and Applying the Flyweight Pattern

Immutable Object Design and Implementation

Thread Safety Through Immutability

Mutable objects like SimpleDateFormat are not thread-safe. When shared across threads without synchronization, they can produce inconsistent or erroneous results:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 20; ++i) {
    new Thread(() -> {
        try {
            log.warn("{}", sdf.parse("1951-04-21"));
        } catch (Exception e) {
            log.error("{}", e);
        }
    }).start();
}

This often leads to NumberFormatException due to internal state corruption.

A thread-safe alternative is DateTimeFormatter, introduced in Java 8:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 20; ++i) {
    new Thread(() -> {
        TemporalAccessor parsed = formatter.parse("1951-04-21");
        log.warn("{}", parsed);
    }).start();
}

DateTimeFormatter is both final and immutable, ensuring safe concurrent access.

Why Is String Final?

The String class is declared final for:

  • Enabling string interning (string pool)
  • Guaranteeing thread safety
  • Ensuring consistent hash code computation

Principles of Immutable Class Design

Key characteristics include:

  • All fields are private and final
  • The class itself is final to prevent subclassing
  • No methods modify internal state
  • Defensive copying is used when exposing internal mutable components

For example, String's constructor from a char[] performs a copy:

public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}

Similarly, substring() creates a new String with a copied segment of the original character array:

public String(char value[], int offset, int count) {
    // ...
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

Classes like BigInteger and BigDecimal follow the same pattern—operations return new instances rather than modifying existing ones.

Immutable Objects and the Flyweight Pattern

Flyweight Pattern Overview

The flyweight pattern reduces memory usage by sharing common object state among multiple objects. Only intrinsic (shared) state is stored in the flyweight; extrinsic (context-specific) state is passed in during operations.

Flyweight in JDK

Boxed Primitive Caches:

  • Integer, Byte, Short, and Long cache values in [-128, 127]
  • Character caches [0, 127]
  • Boolean caches TRUE and FALSE

These caches are initialized on first use and enable object reuse via autoboxing.

String Interning: The JVM maintains a string pool where literal strings and interned strings are reused, reducing duplicate String instances.

Custom Resource Pool Using Flyweight Concepts

A connection pool can leverage atomic operations and wait/notify mechanics to manage limited resources:

class ConnectionPool {
    private final int size;
    private final Connection[] pool;
    private final AtomicIntegerArray status; // 0 = free, 1 = in use

    ConnectionPool(int size) {
        this.size = size;
        this.pool = new Connection[size];
        this.status = new AtomicIntegerArray(size);
        for (int i = 0; i < size; i++) {
            pool[i] = new MockConnection();
        }
    }

    public Connection acquire() {
        while (true) {
            for (int i = 0; i < size; i++) {
                if (status.get(i) == 0 && status.compareAndSet(i, 0, 1)) {
                    return pool[i];
                }
            }
            synchronized (this) {
                try {
                    wait(); // Wait until a connection is freed
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    public void release(Connection conn) {
        for (int i = 0; i < size; i++) {
            if (pool[i] == conn) {
                status.set(i, 0);
                synchronized (this) {
                    notifyAll();
                }
                break;
            }
        }
    }
}

Threads wait when no connections are available to avoid busy-waiting and excessive CAS contention.

Potential Optimizations

  • Support timeout on acquisition
  • Validate connection liveness
  • Allow dynamic pool resizing
  • Use more efficient waiting mechanisms (e.g., Condition)

Production systems typically use mature libraries like HikariCP, Druid, or Apache Commons Pool.

Final Fields and Memory Visibility

Final Field Semantics

  • For primitives: value cannot change after initialization
  • For references: reference cannot change, but referenced object may be mutable
  • Must be initialized in declaration or all constructors

Memory Guarantees

The JVM inserts a write barrier after final field assignments in constructors. This ensures that once an object is visible to other threads, its final fields are properly initialized—no risk of seeing default values (e.g., 0 or null).

For static final constants within compile-time constant expression limits, the JVM inlines values directly into calling code, bypassing field reads entirely.

Stateless Classes Are Thread-Safe

A class with no instance fields (i.e., no state) is inherently thread-safe. This principle underlies recommendations to avoid member variables in servlets—since there’s no shared mutable state, no synchronization is needed.

Tags: java Concurrency Immutability Design Patterns flyweight pattern

Posted on Sun, 25 Oct 2026 16:33:18 +0000 by ponies3387