What Are Java Generics?

My most recent project involved using Java generic types to add flexibility and type safety to our codebase.

My most recent project involved using Java generic types to add flexibility and type safety to our codebase. A big part of that involved wrangling type variables and wildcards, sifting through confusing error messages, and googling how to effectively google question marks. Given that I had so much trouble finding simple explanations, I wanted to condense what I'd learned into a quick crash course.

What Are Generics?

Java generics can be broken into wildcards and type variables and they allow "a type or method to operate on objects of various types while providing compile-time type safety." Type variables allow you to define a class in terms of a variable that could represent one of many object types and then use that variable to define dependencies throughout the class. In class ObjectHolder for instance, the type of object to be stored is decided at instantiation and used to enforce type safety on that object whenever its methods are called.

public class ObjectHolder<U> { private U object; public void setObject(U obj) { object = obj; } public U getObject() { return object; } }

Type variables can also be defined for the scope of a method instead of a whole class, and type bounds can be used to constrain the allowed types. This getFirst method enforces that the list type be a subtype of number and it creates a dependency between the method's argument and return type.

public <U extends Number> U getFirst(List<U> nums) { return nums.get(0); }

Wildcards can be used (with or without bounds) when you'd otherwise use a type variable but when it's not necessary to express dependencies between class properties, arguments or return types.

public boolean has3Entries(List<?> list) { return list.size() == 3; }

So When Are These Useful?

Say you have a method that takes in a list of Numbers.

public class ListChecker { public boolean has3Entries(List<Number> list) { return list.size() == 3; }

public void foo() {
    List list = Arrays.asList(3, 1);
    has3Entries(list);
}

}

This works when you input a list of type Number, but what if we want to be more specific and pass in a list of Integers?

public class FailedChecker { public boolean has3Entries(List<Number> list) { return list.size() == 3; }

public void foo() {
    List&lt;Integer&gt; list = Arrays.asList(3, 1);
    has3Entries(list); // compilation error: List&lt;Integer&gt; is not compatible with List&lt;Number&gt;
}

}

To solve this problem we could use a type parameter bounded by Number to enforce that the list hold Numbers but still allow subtypes.

public class TypeVariableChecker { public <U extends Number> boolean has3Entries(List<U> list) { return list.size() == 3; }

public void foo() {
    List&lt;Number&gt; numberList = Arrays.asList(3.0, 1);
    List&lt;Integer&gt; integerList = Arrays.asList(3, 1);
    has3Entries(numberList);
    has3Entries(integerList);
}

}

Because we are not expressing any dependencies between the argument and return type of this method it is not necessary to use a type variable, and so it makes more sense to use a wildcard.

public boolean has3Entries(List<?> list) { return list.size() == 3; }

More Type Variables vs. Wildcards

As I said before, all references to a type variable in a scope must refer to the same type while wildcards could refer to anything that satisfies their type.

public class FailedTypeVariable<U> {

public U foo(List&lt;U&gt; nums) {
        return nums.get(0);
}

public void test() {
    List intList = Arrays.asList(1, 2);
    List floatList = Arrays.asList(0.1, 0.2);
    foo(intList);
    foo(floatList);
}

}

This example doesn’t work because the type variable U must refer to the same type throughout the class, so foo cannot take in an Integer and a Double in the same class, even though they are both subtypes of Number. We cannot use a wildcard in this case because we need to express a dependency between the parameter and return type, but we can define the type variable at the level of the method. That way, the type variable is allowed to vary between calls but is consistent within the scope of the method!

public class MethodVariable { public <U> U foo(List<U> nums) { return nums.get(0); }

public void test() {
    List intList = Arrays.asList(1, 2);
    List floatList = Arrays.asList(0.1, 0.2);
    foo(intList);
    foo(floatList);
}

}

You should keep in mind that while you can pass an object declared with a type variable to an argument defined by a wildcard, you can't go the other way so easily. For example:

public class WildcardToVariable<U extends Number> { public U foo(List<U> nums) { return nums.get(0); }

public void test() {
    List&lt;? extends Number&gt; numList = Arrays.asList(1, 2);
    foo(numList);
}

}

You might think that you'd be able to convert from <? extends Number> to U since they are both subtypes of Number, but this code won't compile because the type of the type variable is set during instantiation of the class, so it is impossible to tell whether the type of the wildcard will satisfy it.

Nice Examples, How Did You Actually Use This?

It turned out to be crucially useful to be able to give type variables multiple constraints. The code I worked on has a concept of UserObjects and Monitoring where our monitoring methods take in subclasses of UserObject as arguments. However, while UserObject has many subtypes we only want to allow monitoring methods to be called on some of them. Up to this point we had parameterized those methods with wildcards, like

public void monitor(List<? extends UserObject> obj) { ... }

but this allows all UserObject subclasses to be passed in, even the ones that aren't Monitorable. I considered adding another layer to the class hierarchy, an abstract MonitorableObject subtype of UserObject for only some subclasses to extend, but would have cluttered the class hierarchy without adding any real functionality. Instead I created a Monitorable interface because type variables can be constrained by interfaces in addition to classes! You can constrain a type variable with multiple interfaces but at most one class.

public <U extends UserObject & Monitorable> U monitor(List<U> obj) { ... }

The limitation to this is that, seemingly for no particular reason, wildcards cannot be constrained by multiple bounds. So while it's useful be able to ensure only certain objects can be monitored, that guarantee only stands for methods using type variables. For more fun with generics (including the use of ‘super’ instead of ‘extends’) see https://docs.oracle.com/javase/tutorial/extra/generics/index.html