He's right. It's easy to understand one level of variance: you can replace a return type by a subtype and a parameter type by a supertype. (I wouldn't be surprised that many programmer don't undestand this.)
Two levels already requires some deep thinking (assuming definition-site variance: `List<Object>` or `List<String>` as a return type / parameter type, was is allowed to replace it?
More than that? (`List<List<String>>`) Hahaha, good luck.
And by the way, Java has notations to specify the variance of type, but only at the use-site, which is different from doing it at the definition site (both enable expressing things the other can't do... but you can actually have both, as I think is the case in Kotlin, though there are some limitations).
Nevertheless, I have a point because:
1. Things get harder with contravariance. 2. Things get harder when you mix covariance and contravariance.
The prototypical covariance class is Producer<T> (with method produce() returning T) while for contravariance that's Consumer<T> (with method consume taking a T as parameter).
Assume each class has a superclass (Consumer0<T> and Producer0<T>) and a subclass (Consumer2<T> and Producer2<T>). Assume V extends U extends T.
Can you list the subclasses and superclasses of Consumer<Producer<U>>?
Personally, I have to think carefully about this for a minute or so, and I've been there before a couple times.
This is not an especially complex scenario either. I've seen things get worse in practice.