Fundamentals 24 min read

Master Java API Design: 25 Essential Best‑Practice Rules from Effective Java

This article distills key guidelines from Effective Java, covering static factory methods, builder patterns, singleton protection, utility class design, memory‑leak avoidance, equals/hashCode contracts, composition over inheritance, generics, enums, exception handling, and many other best‑practice rules to help Java developers write cleaner, safer, and more maintainable code.

Architect's Guide
Architect's Guide
Architect's Guide
Master Java API Design: 25 Essential Best‑Practice Rules from Effective Java

Effective Java is a classic must‑read; strictly following its principles dramatically improves API quality and coding standards.

1. Consider static factory methods instead of constructors

Example

Integer.valueOf("1"), Boolean.valueOf("true") and similar.

Advantages

High readability (method name conveys intent)

Performance (may avoid creating a new object)

High flexibility (can return subclasses)

These advantages are explained below.

High readability

new Point(x, y) vs Point.at(x, y) or Point.origin(); the latter are self‑describing.

Performance

Objects like Boolean can be pre‑instantiated and reused, avoiding repeated allocation.

<code>public final class Boolean implements Serializable, Comparable&lt;Boolean&gt; {
    // pre‑created instances
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    private Boolean(boolean value) { this.value = value; }
    private Boolean(String s) { this(parseBoolean(s)); }

    public static Boolean valueOf(boolean b) { return b ? TRUE : FALSE; }
    public static Boolean valueOf(String s) { return parseBoolean(s) ? TRUE : FALSE; }
    // ... other code
}</code>

High flexibility

Factory methods can return specific subclasses, enabling more powerful APIs such as Collections.

<code>public class Collections {
    private Collections() {}
    public static final List EMPTY_LIST = new EmptyList<>();
    public static final <T> List<T> emptyList() { return (List<T>) EMPTY_LIST; }
    private static class EmptyList<E> extends AbstractList<E> implements RandomAccess, Serializable { /* ... */ }
    public static <E> List<E> checkedList(List<E> list, Class<E> type) {
        return (list instanceof RandomAccess)
            ? new CheckedRandomAccessList<>(list, type)
            : new CheckedList<>(list, type);
    }
    static class CheckedRandomAccessList<E> extends CheckedList<E> implements RandomAccess { /* ... */ }
    static class CheckedList<E> extends CheckedCollection<E> implements List<E> { /* ... */ }
}</code>

2. When a class needs many constructors, use a Builder

Multiple constructors make client code error‑prone, especially in Android UI components.

<code>public class AlertDialog {
    private int width;
    private int height;
    private String title;
    private String confirmText;
    private String denyText;

    private AlertDialog() {}
    public AlertDialog(int w, int h) { this(w, h, null); }
    public AlertDialog(int w, int h, String t) { this(w, h, t, "OK"); }
    public AlertDialog(int w, int h, String t, String c) { this(w, h, t, c, null); }
    public AlertDialog(int w, int h, String t, String c, String d) { /* set fields */ }
}
</code>

Using a Builder isolates parameter setting and enables validation at construction time.

<code>public class AlertDialog {
    private AlertDialog(Builder b) {
        width = b.width;
        height = b.height;
        if (width == 0 || height == 0) throw new IllegalArgumentException("size must be set");
        // ... other assignments
    }
    public static class Builder {
        private int width;
        private int height;
        private String title;
        private String confirmText;
        private String denyText;
        public Builder setTitle(String t) { this.title = t; return this; }
        // other setters …
        public AlertDialog build() { return new AlertDialog(this); }
    }
}
</code>

3. Strengthen Singleton with private constructor or enum

Reflection can break a classic singleton; using a private constructor or a single‑element enum prevents this.

<code>public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() {}
}
</code>
<code>public enum Elvis { INSTANCE; /* methods */ }
</code>

4. Prevent instantiation of pure utility classes

Declare a private constructor that throws an exception.

<code>public class Util {
    private Util() { throw new AssertionError(); }
}
</code>

Note: this also blocks inheritance.

5. Avoid creating unnecessary objects

Reuse objects when possible.

Use object pools for expensive objects.

Do not pool cheap objects; modern JVMs handle them efficiently.

6. Eliminate obsolete object references

Memory leaks can arise from self‑managed memory, caches, and listeners.

Self‑managed memory: clear references after pop operations.

<code>public class Stack {
    private Object[] elements = new Object[DEFAULT_INITIAL_CAPACITY];
    private int size;
    public Object pop() {
        if (size == 0) throw new EmptyStackException();
        Object obj = elements[--size];
        elements[size] = null; // allow GC
        return obj;
    }
    // ... other methods
}
</code>

Cache: use WeakHashMap or periodic cleanup.

Listeners/Callbacks: unregister them or store as weak references.

7. Do not invoke GC explicitly

Let the JVM manage garbage collection; explicit finalizer calls degrade performance.

8. Override equals following the contract

Reflexive

Symmetric

Transitive

Consistent

Non‑null

9. When overriding equals , also override hashCode

This ensures correct behavior in hash‑based collections.

10. Always override toString

Provide a clear, descriptive representation of the object.

11. Be cautious when overriding clone

Consider alternative copying strategies.

12. Implement Comparable when natural ordering makes sense

13. Minimize accessibility of classes and members

Prefer private > protected > package‑private > public . Avoid widening visibility without justification.

14. Use accessor methods instead of public fields

15. Minimize mutability

16. Prefer composition over inheritance

Composition reduces coupling and avoids inheritance‑related fragility.

17. Design for inheritance or forbid it

18. Prefer interfaces to abstract classes

19. Interfaces should only define types

20. Class hierarchies are preferred over marker interfaces

21. Represent strategies with function objects

Pass listener‑like objects; reuse them instead of creating new anonymous instances.

22. Prefer static class members when appropriate

Nested static classes avoid implicit references to the outer instance.

23. Specify generic types instead of raw types

Use List&lt;String&gt; rather than raw List .

24. Eliminate non‑first‑check warnings

Address IDE warnings; avoid unnecessary @SuppressWarnings annotations.

25. Prefer List over arrays

Lists provide type safety and avoid runtime ArrayStoreException .

26‑28. Prefer generics and bounded wildcards (PECS)

<code>public void pushAll(Iterator<? extends E> src) { for (E e : src) push(e); }
public void popAll(Collection<? super E> dst) { while (!isEmpty()) dst.add(pop()); }
</code>

29‑35. Use enums, EnumSet, EnumMap, annotations, and @Override wherever applicable

<code>public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
Set&lt;Style&gt; styles = EnumSet.of(Style.BOLD, Style.ITALIC);
</code>

38. Validate method parameters

Public methods should throw exceptions for illegal arguments; private methods may use assertions.

39. Perform defensive copying

<code>public Period(Date start, Date end) {
    this.start = new Date(start);
    this.end = new Date(end);
    if (this.start.compareTo(this.end) > 0) throw new IllegalArgumentException();
}
public Date getStart() { return new Date(start); }
public Date getEnd() { return new Date(end); }
</code>

40‑43. Design method signatures carefully, avoid overload abuse, limit varargs, return empty collections instead of null

44. Document all exported API elements with Javadoc

45‑46. Minimize variable scope and prefer foreach loops

48. Avoid float / double for precise calculations; use BigDecimal

<code>System.out.println(new BigDecimal("1.03").subtract(new BigDecimal("0.42")));
</code>

49. Prefer primitive types over boxed types

50. Use proper types instead of String for numbers, booleans, enums, etc.

51. Use StringBuilder for intensive string concatenation

52‑53. Program to interfaces and avoid reflection unless necessary

54‑55. Use JNI and optimizations sparingly

56‑58. Follow naming conventions, use exceptions appropriately, and distinguish checked vs runtime exceptions

(完)

JavaprogrammingBest PracticesAPI designEffective Java
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.