W3docs

Java Generics Restrictions

What you cannot do with Java generics — no primitives, no static type parameters, no generic arrays, and more.

Java Generics Restrictions

This chapter is the catalogue of things you cannot do with Java generics, with a short explanation of why each one is forbidden. Almost every restriction traces back to a single fact you saw in the previous chapter: the type parameter is erased before bytecode is emitted, so anything that needs the parameter to exist at runtime won't work. Read this chapter as the closing reference card for the part — it's the list of "I tried, the compiler complained" moments turned into a single page.

1. No primitives as type arguments

List<int> ints = new ArrayList<>();        // ❌
List<Integer> ints = new ArrayList<>();    // ✓

The bytecode-level erased form of List<E> stores its elements as Object, and primitives are not Objects. The fix is the corresponding wrapper class — Integer, Long, Double, Boolean, Character, etc. Autoboxing then bridges the gap: ints.add(5) and int x = ints.get(0) both work, with the cost that every element pays for an Integer object on the heap.

Project Valhalla is the long-running effort to make List<int> actually work, via value types and specialised generics. As of Java 25 it isn't here yet.

2. No new T()

public class Box<T> {
  public T newInstance() { return new T(); }      // ❌
}

At runtime, there is no T — the JVM only has Object (or the bound). It has no class object to call a constructor on, no way to know which constructor to call. The standard workaround is to take a factory as a parameter:

public class Box<T> {
  public T newInstance(Supplier<T> factory) { return factory.get(); }
}

Box<String> b = new Box<>();
String fresh = b.newInstance(String::new);

The Supplier<T> carries the actual factory at runtime, in a way the type parameter never could.

3. No T.class or instanceof T

public <T> boolean isIt(Object o) {
  return o instanceof T;        // ❌
}

public <T> Class<T> klass() {
  return T.class;               // ❌
}

Again, no T at runtime. The workaround in both cases is to pass the Class<T> token as an argument:

public <T> boolean isIt(Object o, Class<T> type) {
  return type.isInstance(o);
}

Class.isInstance(Object) is the reflective form of instanceof, and it works with the runtime Class object you handed in. The standard library does this all over the place — Collections.checkedList(List<E>, Class<E>), EnumSet.noneOf(Class<E>), JSON deserialisers, and so on.

4. No arrays of a generic type

T[] arr = new T[10];              // ❌ — generic array creation
List<String>[] lists = new List<String>[10];   // ❌ — same

This one is more subtle. Arrays in Java are reified — an Integer[] knows at runtime that it's an Integer[], and stores will check. But generics are erased — the JVM can't tell List<String>[] from List<Integer>[]. If both restrictions weren't enforced, you could break the heap with a few lines:

List<String>[] strs = new List<String>[1];   // pretend this is legal
Object[] objs = strs;                         // arrays are covariant
objs[0] = List.of(42);                        // stores an Integer list
String s = strs[0].get(0);                    // KABOOM

The compiler refuses generic array creation rather than let this happen.

Workarounds:

  • Use (T[]) new Object[n] with an @SuppressWarnings("unchecked") (you saw this in the generic Stack earlier). Safe if the array is internal and you never let it leak out as T[].
  • Or just use a List<T> instead of an array. In nine out of ten cases this is the right answer.

5. No static fields of a type parameter

public class Box<T> {
  private static T defaultValue;       // ❌
  public  static T empty() { ... }     // ❌
}

The type parameter belongs to an instance — each Box<...> carries its own T. Static members belong to the class itself, which has no T. The two scopes don't connect.

If you need a static method that's polymorphic in a type, declare its own type parameter (we covered this in generic methods):

public class Box<T> {
  public static <U> Box<U> empty() { return new Box<>(null); }
}

<U> is local to the method — independent of any class-level T.

6. No generic exception types

public class MyException<T> extends Exception { ... }    // ❌

The JVM's exception handling tables look up catch blocks by erased class. If two different generic exception types both erased to the same class, a catch (MyException<String> e) would also catch a MyException<Integer> — which would silently corrupt the type system. Rather than try to make that work, Java forbids the declaration outright. You also can't have a generic type parameter in a catch clause:

try { ... } catch (T e) { ... }                          // ❌

If your exception genuinely needs to carry a typed payload, store the payload as a generic field on a non-generic exception:

public class TaggedException extends Exception {
  public final Object payload;
  public TaggedException(String message, Object payload) {
    super(message);
    this.payload = payload;
  }
}

Or declare the throw site narrowly and reserve typed payloads for normal return paths.

7. No overloads that differ only in generic parameters

public void process(List<String> list)  { ... }
public void process(List<Integer> list) { ... }    // ❌ — both erase to process(List)

After erasure, both methods have signature process(List). Java treats overload resolution by erased signatures, so it can't tell the two apart. The workaround is to give them different names — processStrings and processInts — or to accept a List<Object> and check at runtime.

8. Generic types are invariant

This isn't a "the compiler rejects this" rule, it's a "the compiler rejects what you expected to be legal" rule, and we covered it in detail in Wildcards:

List<Integer> ints = ...;
List<Number>  nums = ints;       // ❌ — generic types are invariant
List<? extends Number> nums = ints;   // ✓ — wildcard restores the flexibility

Worth knowing because it's the restriction you'll bump into the most. Wildcards are the relief valve.

9. No generic enum types

public enum Box<T> {                                   // ❌
  EMPTY, FULL;
  T value;
}

Enums are translated to a single class with a fixed set of constants — there's no way for the constants to share a single, sensible T. The fix is usually to make the methods generic, not the enum itself:

public enum Box {
  EMPTY, FULL;
  public <T> T orDefault(T fallback) { return this == FULL ? null : fallback; }
}

Or, if every constant really does want its own type, use a non-enum class hierarchy and a Map<Name, Box>.

10. Calling a generic method through a raw type

List rawList = new ArrayList();
rawList.add("hi");                  // unchecked-warning, but allowed
List<String> typed = rawList;       // unchecked-warning, dangerous

Mixing raw and generic types disables all of generics' compile-time checks for that variable. The compiler will warn (Unchecked call to add(E) as a member of raw type java.util.List), and ignoring the warning gives you back the pre-Java-5 footgun — wrong-type values silently smuggled in, exploding at the next read.

Raw types exist for backward compatibility, not as a feature. Treat the warning as an error in any new code.

A worked example: each restriction, side by side

The program below tries to do each of the things the rules forbid (commented out so the file compiles), then shows the canonical workaround for each. Read the comments — they map one-to-one onto the numbered restrictions above.

java— editable, runs on the server

Every numbered restriction maps to a one-line workaround in the program. The pattern across all of them is the same: anything that wants the type parameter at runtime gets handed the information explicitly — a Class<T> token, a Supplier<T>, a method-level type parameter, a wildcard. Erasure took the implicit form away; you give it back at the API boundary.

That closes Part 10

Generics is the deepest single feature in the language outside the JVM itself. You now have the working vocabulary: type parameters on classes, methods, and interfaces; bounds; wildcards and PECS; erasure; and the catalogue of restrictions that erasure forces on the design. Every modern Java API is shaped by these rules, and reading library code (or designing your own) is much easier with the model in your head.

What's next

Generics aren't an end in themselves — they exist because Java needed a way to express "container of T" without copy-pasting a class per element type. The next part of the book is the place every piece of generic machinery in this part was secretly preparing you for: the Collections Framework. List, Set, Map, Queue, and the dozens of implementations behind them — all of them parameterised, all of them designed around the rules you just learned. Continue to Java Collections intro.

Practice

Practice

You want a generic method that returns `true` when its argument is an instance of `T`. You write `<T> boolean isIt(Object o) { return o instanceof T; }`. The compiler rejects it. What's the standard fix?