Variance in Java
Want to learn more about variance in Java?
Join the DZone community and get the full member experience.
Join For FreeThe other day I came across this post describing the pros and cons of using Go after 8 months. I mostly agree after working full-time with Go for a comparable duration.
Despite that preamble, this is a post about variance in Java, where my goal is to refresh my understanding of what variance is and some of the nuances of its implementation in Java.
ProTip: You’ll need to know this for your OCJP certificate exam.
I will write down my thoughts on using Go in a later post.
What Is Variance?
The Wikipedia article on variance says:
Variance refers to how subtyping between more complex types relates to subtyping between their components.
“More complex types” here refers to higher level structures like containers and functions. So, variance is about the assignment compatibility between containers and functions composed of parameters that are connected via a Type Hierarchy. It allows the safe integration of parametric and subtype polymorphism1. For example, can I assign the result of a function that returns a list of cats to a variable of type “list of animals”? Can I pass in a list of Audi cars to a method that accepts a list of cars? Can I insert a wolf in this list of animals?
In Java, variance is defined at the use-site 2.
Four Kinds of Variance
Paraphrasing the Wiki article, a type constructor is:
- Covariant if it accepts subtypes but not supertypes
- Contravariant if it accepts supertypes but not subtypes
- Bivariant if it accepts both supertypes and subtypes
- Invariant does not accept either supertypes nor subtypes
(Obviously, the declared type parameter is accepted in all cases.)
Invariance in Java
The use-site must have no open bounds on the type parameter.
IfA
is a supertype ofB
, thenGenericType<A>
is not a supertype ofGenericType<B>
and vice versa.
This means these two types have no relation to each other and neither can be exchanged for the other under any circumstance.
Invariant Containers
In Java, invariants are likely the first examples of generics you’ll encounter and are the most intuitive. The methods of the type parameter are useable as one would expect. All methods of the type parameter are accessible.
They cannot be exchanged:
You can add objects to them:
You can read objects from them:
Covariance in Java
The use-site must have an open lower bound on the type parameter.
IfB
is a subtype ofA
, thenGenericType<B>
is a subtype ofGenericType<? extends A>
.
Arrays in Java Have Always Been Covariant
Before generics were introduced in Java 1.5
, arrays were the only generic containers available. They have always been covariant, eg. Integer[]
is a subtype of Object[]
. The danger has always been that if you pass your Integer[]
to a method that accepts Object[]
, that method can literally put anything in there. It’s a risk you take — no matter how small — when using third-party code.
Covariant Containers
Java allows subtyping (covariant) generic types but it places restrictions on what can “flow into and out of” these generic types in accordance with the Principle of Least Astonishment3. In other words, methods with return values of the type parameter are accessible, while methods with input arguments of the type parameter are inaccessible.
You can exchange the supertype for the subtype:
Reading from them is intuitive:
Writing to them is prohibited (counterintuitive) to guard against the pitfalls with arrays described above. In the example code below, the caller/owner of a List<Joe>
would be astonished if someone else’s method with covariant arg List<? extends Person>
added a Jill
.
Contravariance in Java
The use-site must have an open upper bound on the type parameter.
IfA
is a supertype ofB
, thenGenericType<A>
is a supertype ofGenericType<? super B>
.
Contravariant Containers
Contravariant containers behave counterintuitively: contrary to covariant containers, access to methods with return values of the type parameter are inaccessible while methods with input arguments of the type parameter are accessible:
You can exchange the subtype for the supertype:
But you cannot capture a specific type when reading from them:
You can add subtypes of the “lower bound”:
But you cannot add supertypes:
Bi-variance in Java
The use-site must declare an unbounded wildcard on the type parameter.
A generic type with an unbounded wildcard is a supertype of all bounded variations of the same generic type. For example, GenericType<?>
is a supertype of GenericType<String>
. Since the unbounded type is the root of the type hierarchy, it follows that of its parametric types and it can only access methods inherited from java.lang.Object
.
Think ofGenericType<?>
asGenericType<Object>
.
Variance of Structures With N-Type Parameters
What about more complex types such as Functions? The same principles apply; you just have more type parameters to consider:
Variance and Inheritance
Java allows overriding methods with covariant return types and exception types:
But attempting to override methods with covariant arguments results in merely an overload:
Final Thoughts
Variance introduces additional complexity to Java. While the typing rules around variance are easy to understand, the rules regarding accessibility of methods of the type parameter are counterintuitive. Understanding them isn’t just “obvious” — it requires pausing to think through the logical consequences.
However, my daily experience has been that the nuances generally stay out of the way:
- I cannot recall an instance where I had to declare a contravariant argument, and I rarely encounter them (although they do exist).
- Covariant arguments seem slightly more common (example4), but they’re easier to reason about (fortunately).
Covariance is its strongest virtue considering that subtyping is a fundamental technique of object-oriented programming (case in point: see note 4).
Conclusion: variance provides moderate net benefits in my daily programming, particularly when compatibility with subtypes is required (which is a regular occurrence in OOP).
References
- Taming the Wildcards: Combining Definition- and Use-Site Variance by John Altidor, et. al. ↩
- As I understand it, the difference between use-site and definition-site variance is that the latter requires the variance be encoded into the generic type itself (think of having to declare
MyGenericType<? extends Number>
), forcing the API developer to preempt all use cases. C# defines variance at the definition-site. On the other hand, use-site variance doesn’t have this restriction — the API developer can simply declare his API as generic and let the user determine variance for his use cases. The downside of use-site invariance is the “hidden” surprises described above, all derived from “conceptual complexity, […] anticipation of generality at allusage points” (see Taming the Wildcards paper above). ↩ - Principle of least astonishment — Wikipedia. I vaguely remember a reference somewhere about the designers of Java following this principle but I can’t seem to find it now. ↩
Joined
concatenates severalText
s. Declaring an invariant iterable ofText
would make this constructor unusable to subtypes ofText
. ↩ ↩2
Published at DZone with permission of George Aristy. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments