Generics in Java provide type safety, reusability, and maintainability, making them ideal for implementing design patterns like Abstract Factory and Builder. These patterns are commonly used to create complex objects, but without generics, they often rely on casting or repetitive code.
By parameterizing factories and builders with generics, we can achieve compile-time safety, eliminate redundant code, and design fluent APIs that adapt to different domains. Think of generics as blueprint molds: instead of writing a new mold for every object, you define one flexible mold that produces many shapes safely.
In this case study, we’ll implement Abstract Factories and Builders using Generics, demonstrate real-world use cases, and highlight best practices.
Core Concepts of Java Generics
Type Parameters
<T>
→ Type<K, V>
→ Key, Value<E>
→ Element
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Generic Methods
public static <T> T getFirst(List<T> list) {
return list.get(0);
}
Abstract Factory Without Generics (Legacy)
interface ShapeFactory {
Object create(String type);
}
Problem: Requires casting and risks runtime errors.
ShapeFactory factory = ...;
Circle circle = (Circle) factory.create("circle"); // Unsafe
Designing an Abstract Factory with Generics
Step 1: Generic Factory Interface
interface Factory<T> {
T create();
}
Step 2: Concrete Implementations
class CircleFactory implements Factory<Circle> {
@Override
public Circle create() {
return new Circle();
}
}
class SquareFactory implements Factory<Square> {
@Override
public Square create() {
return new Square();
}
}
Usage:
Factory<Circle> circleFactory = new CircleFactory();
Circle circle = circleFactory.create(); // No cast needed
Abstract Factory with Multiple Type Parameters
Sometimes factories must accept both a product type and a key:
interface AbstractFactory<T, K> {
T create(K key);
}
class VehicleFactory implements AbstractFactory<Vehicle, String> {
public Vehicle create(String type) {
return switch (type) {
case "car" -> new Car();
case "bike" -> new Bike();
default -> throw new IllegalArgumentException("Unknown type");
};
}
}
Builder Pattern Without Generics (Legacy)
class UserBuilder {
private String name;
private int age;
public UserBuilder setName(String name) { this.name = name; return this; }
public UserBuilder setAge(int age) { this.age = age; return this; }
public User build() { return new User(name, age); }
}
Problem: Needs separate builders for every entity.
Designing a Generic Builder
Step 1: Generic Builder Interface
interface Builder<T> {
T build();
}
Step 2: Fluent Builder with Generics
class UserBuilder implements Builder<User> {
private String name;
private int age;
public UserBuilder setName(String name) { this.name = name; return this; }
public UserBuilder setAge(int age) { this.age = age; return this; }
@Override
public User build() { return new User(name, age); }
}
Step 3: Self-Referential Generics (for Fluent APIs)
class BaseBuilder<T, B extends BaseBuilder<T, B>> {
protected String common;
public B setCommon(String common) {
this.common = common;
return (B) this;
}
public T build() { throw new UnsupportedOperationException(); }
}
Concrete builder:
class ProductBuilder extends BaseBuilder<Product, ProductBuilder> {
private String name;
public ProductBuilder setName(String name) {
this.name = name;
return this;
}
@Override
public Product build() { return new Product(name, common); }
}
Using Wildcards for Flexible Builders
void processBuilders(List<? extends Builder<?>> builders) {
for (Builder<?> b : builders) {
Object obj = b.build();
System.out.println("Built: " + obj);
}
}
Type Erasure in Factories and Builders
Generics are erased at runtime:
Factory<Car> carFactory = new CircleFactory(); // Compile error
At runtime, both Factory<Car>
and Factory<Bike>
are just Factory
. Compile-time safety prevents errors.
Case Study: Putting It Together
public class FactoryBuilderDemo {
public static void main(String[] args) {
Factory<Circle> circleFactory = Circle::new;
Circle circle = circleFactory.create();
User user = new UserBuilder().setName("Alice").setAge(25).build();
Product product = new ProductBuilder().setName("Laptop").setCommon("Electronics").build();
System.out.println(circle);
System.out.println(user);
System.out.println(product);
}
}
Best Practices
- Use factories to abstract object creation.
- Use builders for complex objects with many optional parameters.
- Apply self-referential generics for fluent APIs.
- Avoid raw types and unnecessary wildcards.
- Keep APIs simple and consistent.
Common Anti-Patterns
- Exposing raw factories or builders (
Factory factory
). - Overloading with too many type parameters.
- Ignoring type erasure when reflecting over factories.
Performance Considerations
- Generics impose zero runtime cost due to type erasure.
- Performance is determined by underlying object creation logic, not generics.
📌 What's New in Java for Generics?
- Java 5: Introduction of Generics
- Java 7: Diamond operator (
<>
) for type inference - Java 8: Lambdas and method references simplify factories/builders
- Java 10:
var
keyword reduces verbosity in builders - Java 17+: Sealed classes integrate with generics in factory hierarchies
- Java 21: Virtual threads enhance concurrency with generic builders
Conclusion and Key Takeaways
Generics make Abstract Factories and Builders safer, more flexible, and reusable. By parameterizing creation patterns, developers avoid casting, reduce duplication, and create maintainable APIs.
Key Takeaways:
- Use generics in factories to eliminate casting.
- Apply builders for complex object creation.
- Self-referential generics enable fluent, type-safe APIs.
- Generics add compile-time safety with no runtime penalty.
FAQ
1. Why can’t I use new T()
in factories?
Because of type erasure, use Class<T>
with reflection or suppliers.
2. Do generics slow down object creation?
No, generics add no runtime overhead.
3. Should I use wildcards in builders?
Only when processing multiple builder types generically.
4. What’s the advantage of self-referential generics?
They allow fluent APIs without losing type safety.
5. Can I combine factories and builders?
Yes, factories can produce builders, or builders can use factories internally.
6. How does type erasure affect factories?
Different parameterized factories compile to the same runtime class.
7. Can enums be used with factories?
Yes, enums often represent factory keys.
8. Is it better to use lambdas for factories?
Yes, lambdas (Circle::new
) make factories concise.
9. Are raw builders ever acceptable?
No, always prefer parameterized builders for safety.
10. What’s the biggest pitfall with factories and builders?
Overcomplicating APIs with excessive type parameters and wildcards.