Design PatternsStructural
Flyweight Pattern
Share common, immutable state between many fine-grained objects to drastically cut memory use.
Design Patterns
Intent
Flyweight lets you fit far more objects into memory by sharing the parts of their state that are identical. Each object’s state is split in two:
- Intrinsic state — immutable and shared (a tree species’ texture, a glyph’s font outline)
- Extrinsic state — unique per use and passed in or stored separately (a tree’s x/y position, a glyph’s position on the page)
A factory caches flyweights so that identical intrinsic state is stored only once.
Structure
classDiagram
direction LR
class TreeTypeFactory {
-Map cache
+get(name, color, texture) TreeType
}
class TreeType {
-String name
-String color
-byte[] texture
+draw(x, y)
}
class Tree {
-int x
-int y
-TreeType type
}
Forest --> "many" Tree
Tree --> TreeType : shares
TreeTypeFactory --> TreeType : caches
Example: a forest of a million trees
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
// Intrinsic, shared, immutable state — the flyweight
record TreeType(String species, String color, byte[] texture) {
void draw(int x, int y) { /* render texture at (x, y) */ }
}
// Factory guarantees one TreeType per distinct key
final class TreeTypes {
private static final Map<String, TreeType> CACHE = new ConcurrentHashMap<>();
static TreeType of(String species, String color) {
return CACHE.computeIfAbsent(species + "|" + color,
k -> new TreeType(species, color, new byte[50_000])); // pretend 50 KB texture
}
static int count() { return CACHE.size(); }
}
// Extrinsic state — small and unique per tree
record Tree(int x, int y, TreeType type) {}
class Main {
public static void main(String[] args) {
String[][] kinds = { {"Oak", "green"}, {"Pine", "dark-green"}, {"Birch", "white"}, {"Maple", "red"} };
var rnd = new Random(42);
List<Tree> forest = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
String[] k = kinds[rnd.nextInt(kinds.length)];
forest.add(new Tree(rnd.nextInt(10_000), rnd.nextInt(10_000), TreeTypes.of(k[0], k[1])));
}
long sharedBytes = TreeTypes.count() * 50_000L;
long naiveBytes = forest.size() * 50_000L;
System.out.printf("Trees: %,d distinct TreeTypes: %d%n", forest.size(), TreeTypes.count());
System.out.printf("Texture memory with flyweights: %,d KB%n", sharedBytes / 1024);
System.out.printf("Texture memory without: %,d MB%n", naiveBytes / 1024 / 1024);
System.out.println("Same instance shared? " + (forest.get(0).type() == TreeTypes.of(forest.get(0).type().species(), forest.get(0).type().color())));
}
}Output
Without flyweights, a million trees with their own 50 KB textures would need ~47 GB. With four shared TreeTypes, the textures take 195 KB.
Flyweights in the JDK
You use flyweights every day:
Integer a = 127, b = 127;
System.out.println(a == b); // true — cached by Integer.valueOf (range -128..127)
Integer c = 128, d = 128;
System.out.println(c == d); // false — outside the cache, two objects
String s1 = "hello", s2 = "hello";
System.out.println(s1 == s2); // true — string literals are interned in the string pool
Integer.valueOf,Long.valueOf,Short.valueOf,Byte.valueOf,Character.valueOfandBoolean.valueOfcaches- The String pool and
String.intern() Enumconstants andBigDecimal.ZERO/ONE/TENCollections.emptyList()/List.of()returning shared empty instances
Trade-offs
| Pros | Cons |
|---|---|
| Huge memory savings for many similar objects | Code is more complex: state is split in two |
| Fewer allocations, less GC pressure | Flyweights must be immutable |
| Factory cache gives identity-based equality for free | Extrinsic state must be computed or passed on every call |
Finished this lesson?
Mark it complete to track your progress through Design Patterns.