Ковариантность – следование существующей иерархии (последовательности) наследования. Например, если класс Dog наследник Animal, то IEnumerble <Dog> будет потомком IEnumerble <Animal>. Перечисление собак – отдельный случай перечисления всех животных.
Контравариантность – это изменение порядка наследования на противоположную. Рассмотрим следующий вариант. String является наследником Object. Делегат Action<T> это метод, который принимает объекты типа Т. Тогда Action<Object> – наследник Action<String>. Ведь если строки являются объектами, то метод способный производить операции над любыми объектами, способна выполнять их и над строкой.
Существует также понятие инвариантность, которое означает отсутствие наследования, а значит ковариантности и контравариантности.
В версии Java 5 и более поздних переопределение методов ковариантно.
interface A {
Object foo() throws Throwable;
}
interface В extends A {
// в типе-наследнике используются типы-наследники – ковариантность
@Override
Integer foo() throws NullPointerException;
}