skip to content

How do static factory methods enable instance control and returning subtypes, and what concrete JDK examples illustrate this?

level: middleimportance: should knowfreq 55%

answer

  1. Factory need not allocate → instance control
  2. Integer.valueOf caches -128..127; Boolean.valueOf 2 constants
  3. Return type = interface → hide concrete class
  4. EnumSet.of → RegularEnumSet vs JumboEnumSet by size
  5. Hidden impl → library can change it across releases

basics

~20 s

Because a factory doesn't have to call new every time, it can hand back a cached object (instance control), like Integer.valueOf reusing small ints. And because its return type can be an interface, it can return any hidden subclass, like List.of returning some private list implementation.

solid answer

~50 s

Two of the strongest reasons to use a static factory are instance control and subtype hiding. Instance control means the class decides whether each call allocates: a factory can return a cached or pooled object instead of always running new. Integer.valueOf caches boxes for -128..127; Boolean.valueOf always returns one of two constants. This supports singletons and lets value classes guarantee a.equals(b) implies a == b. Subtype hiding means the factory declares a supertype — usually an interface — as its return type and returns any implementing class the caller never names. List.of, Collections.unmodifiableList, and EnumSet.of all return concrete classes that are not part of the public API; EnumSet.of even picks RegularEnumSet or JumboEnumSet based on the number of constants. The payoff is that the library can change or add implementations across releases without breaking callers, because callers only depend on the interface.

code

java · 12 lines
java
// Instance control: valueOf may reuse a cached object; new always allocates.
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println(a == b);   // true  -> same cached instance (-128..127)

Integer c = Integer.valueOf(128);
Integer d = Integer.valueOf(128);
System.out.println(c == d);   // false -> outside cache, distinct objects
System.out.println(c.equals(d)); // true -> compare values with equals

// Subtype hiding: factory returns the List interface, concrete class is hidden.
List<String> xs = List.of("a", "b");   // some private immutable List impl

go deeper

for a junior

Knows a factory can return a reused object rather than always calling new, and recognizes Integer.valueOf caching as an example.

for a middle

Explains instance control and subtype hiding with concrete JDK examples (valueOf caching, List.of/Collections.unmodifiableList hiding the class), and the == boxing pitfall.

for a senior

Connects subtype-hiding to API evolution and EnumSet's per-size implementation switch, and articulates the equals/== guarantee instance control can provide for value classes.

for a principal

Frames this as a library-design lever — small interface surface + hidden implementations + service-provider plug-in points — and reasons about binary/source compatibility implications when changing returned implementations.

This question unpacks advantages #2, #3 and #4 of static factories. ## Instance control (advantage #2): not required to create a new object A **constructor** invoked with `new` *always* allocates a fresh object on the heap. A **static factory method** has no such obligation — its body can `return` a pre-existing object. This gives the class **instance control**: complete authority over which instances exist and when. Classic JDK examples: - **`Boolean.valueOf(boolean)`** returns one of two pre-built constants, `Boolean.TRUE` / `Boolean.FALSE`. It *never* allocates. (`new Boolean(true)` does — which is why the constructor is deprecated.) - **`Integer.valueOf(int)`** caches the boxed `Integer` objects for the range **-128..127** (the *Flyweight* cache). `Integer.valueOf(100) == Integer.valueOf(100)` is `true`; outside the cache it's `false`. Autoboxing uses `valueOf` under the hood, which is the source of the famous `==` pitfall on boxed values. Why instance control matters: - **Singletons & non-instantiable classes** — a factory can always return the one instance. - **Value classes** — an *instance-controlled* immutable class can guarantee that **`a.equals(b)` if and only if `a == b`**, letting clients use the faster `==` and enabling interning/canonicalization. - **Reduced allocation** — fewer objects, less GC pressure for frequently used immutable values. ## Returning a subtype (advantage #3) — interface-hiding A constructor can only produce an instance of **its own class**. A static factory can declare a **supertype** — typically an **interface** — as its return type and return *any* class that implements it. Callers program to the interface and never learn the concrete class. ```java List<String> xs = List.of("a", "b"); // concrete class is hidden (e.g. ListN) List<String> ro = Collections.unmodifiableList(xs); // returns a private wrapper class ``` Neither `List.of` nor `Collections.unmodifiableList` names its return class in the public API. This is the engine behind **interface-based frameworks**: a small public surface of interfaces + factories, with the implementations package-private. ## Varying the class by input (advantage #4) Because the concrete type is hidden, the factory can **choose a different implementation per call**, transparently: - **`EnumSet.of(...)`** returns a **`RegularEnumSet`** (a single `long` bit-vector) when the enum has **≤ 64 constants**, and a **`JumboEnumSet`** (a `long[]`) otherwise. Callers only ever see `EnumSet`. If a future JDK added a third optimized variant, no client code would change. ## Why this is good for API evolution The combined effect: callers depend only on the **interface / declared return type**, not the implementation. The library author can swap the concrete class in a new release, add specialized fast-path implementations, or merge implementations — all **without breaking binary or source compatibility** — impossible if a public constructor had pinned a specific class. *Effective Java* extends this further into **service provider frameworks** (e.g. JDBC's `DriverManager.getConnection`), where the returned class may not even exist when the factory is written; the provider plugs it in at runtime. ## Pitfall to remember Instance control via caching interacts with `==`: `Integer.valueOf(127) == Integer.valueOf(127)` is `true` but `Integer.valueOf(128) == Integer.valueOf(128)` is `false`. Always compare boxed values with `.equals` (or use primitives). This is a direct, testable consequence of the factory caching strategy.

  • Why does Integer.valueOf(128) == Integer.valueOf(128) evaluate to false while the same expression with 127 is true?
    Integer.valueOf caches boxed instances only for -128..127. Within that range it returns the same cached object, so == is true. 128 is outside the cache, so each call allocates a distinct Integer, and == compares references, which differ. Use .equals or primitive int to compare values.
  • How does returning an interface type from a factory help API evolution?
    Callers depend only on the interface, not the concrete class, which the factory hides. The library can swap, split, or optimize implementations in future releases (as EnumSet does internally) without changing the public type, so client code keeps compiling and running unchanged.

saying these in an interview costs you the question

  • Believing Integer.valueOf caches all integers — the cache is only -128..127 by default.
  • Saying a constructor can return a cached instance — new always allocates; only a factory can reuse.
  • Assuming callers can rely on the concrete class returned by List.of or EnumSet.of — it's intentionally unspecified and may change.
  • Confusing instance control with thread safety — they're separate concerns (though immutable shared instances are inherently thread-safe).

context