понедельник, 20 июля 2015 г.

Ковариантность/контравариантность в Java без wildcard'ов

Все вы знаете, что полиморфный тип может быть либо ковариантен, либо контравариантен, либо инвариантен.

Например:

  • Тип Optional<A> ковариантен. То есть, например, всегда можно привести Optional<Integer> к Optional<Number> и это будет безопасно.
  • Тип Consumer<A> контравариантен. Это значит, что всегда можно безопасно привести Consumer<Number> к Consumer<Integer>.
  • Наконец, тип List<A> инвариантен, так как он не является ни ковариантным, ни контравариантным . То есть нельзя безопасно привести не от List<Integer> к List<Number> и не от List<Number> к List<Integer>
Так получилось, что в Java ввели полиморфные типы не сразу, а лишь в пятой версии. Поэтому, когда в то время типы снабжали параметрами, оказалось, что 99% типов оказались инвариантными. Но что делать в тех случаях, когда программист имеет переменную типа List<Integer>, а функция принимает аргумент типа List<Number>? Как привести List<Integer> к List<Number>, если List не является ковариантным? Для этого в Java ввели костыль под названием usage-site variance: каждый раз объявляя переменную, вы решаете, является ли тип этой переменной ковариантным, контравариантным или инвариантным. Например:
  • Если вам нужно только читать из списка, то вы объявляете тип переменной как ковариантный: List<? extends Number>.
  • Если вам нужно только писать в список, то вы объявляете тип переменной как контравариантный: List<? super Number>.
  • Наконец, если вам нужно и читать из списка, и писать в него, то вы пишете просто List<Number>, то есть объявляете тип переменной как инвариантный.
Печально то, что люди используют usage-site variance даже в тех случаях, когда тип является ковариантным или контравариантным, усложняя сигнатуры функций и таская по всему проекту типы с этими дурацкими символами вопроса (wildcard'ами).

В этом посте я предлагаю альтернативу wildcard'ам. Конечно, лучше бы если бы в Java, наконец, ввели declaration site variance на уровне языка, т.е. возможность указать ковариантность/контравариантность типа во время его объявления (Optional<out A> / Consumer<in A>), и я не понимаю, почему это до сих пор не сделали.

Итак, правило следующее. Если вы объявляете ковариантный тип, то вы добавляете метод widen() со следующей сигнатурой:

public class Optional<A> {
  ...

  public static <B, A extends B> Optional<B> widen(Optional<A> opt) {
    return (Optional) opt;
  }
}

Теперь всегда, когда вам нужно привести Optional<Integer> Optional<Number> вы пишите:

Optional<Integer> opt = ...;
Optional<Number> opt2 = Optional.widen(opt);

И всё, никаких wildcard'ов.

Аналогично для контравариантных типов вы добавляете метод narrow():

public interface Consumer<A> {
  ...

  public static <A, B extends A> Consumer<B> narrow(Consumer<A> con) {
    return (Consumer) con;
  }
}

Теперь если нужно привести Consumer<Number> к Consumer<Integer>, вы пишите:

Consumer<Number> con = ...;
Consumer<Integer> con2 = Consumer.narrow(con);
По-моему, такой подход делает код намного чище. Я понимаю, что мы заменили одно уродство на другое, но я думаю, что моё уродство является менее страшным.