Статьи

Гениальный обходной путь для эмуляции типов сумм в Java

Прежде чем перейти к самой статье, я хотел бы поблагодарить Даниэля Дитриха , автора потрясающей библиотеки Javaslang , у которого была идея до меня:

Контравариантные родовые границы

Все началось с твита:

Я хотел сделать что-то вроде сопоставления с шаблоном общего супертипа набора типов, например:

1
<T super T1 | T2 | ... | TN>

Обратите внимание, что мне действительно нужна поддержка типов объединения, а не типов пересечений, как я изначально заявлял.

Почему я хотел это сделать? Потому что это было бы хорошим дополнением к библиотеке jOOλ , которая содержит типизированные кортежи для Java :

1
2
3
4
5
6
7
class Tuple3<T1, T2, T3> {
    final T1 v1;
    final T2 v2;
    final T3 v3;
 
    // Lots of useful stuff here
}

В кортеже было бы неплохо использовать метод forEach() который перебирает все атрибуты:

1
tuple(1, "a", null).forEach(System.out::println);

Вышесказанное просто даст:

1
2
3
1
"a"
null

Теперь, каким будет тип аргумента этого метода forEach() ? Это будет выглядеть так:

1
2
3
class Tuple3<T1, T2, T3> {
    void forEach(Consumer<? super T1 | T2 | T3> c) {}
}

Потребитель получит объект типа T1, T2 или T3. Но потребитель, который принимает общий супертип предыдущих трех типов, тоже в порядке. Например, если у нас есть:

1
2
Tuple2<Integer, Long> tuple = tuple(1, 2L);
tuple.forEach(v->System.out.println(v.doubleValue()));

Вышеприведенное скомпилирует, потому что Number — это общий супер тип Integer и Long , и он содержит метод doubleValue() .

К сожалению, это невозможно в Java

Java в настоящее время поддерживает типы объединения / суммы ( см. Также алгебраические типы данных ) только для блоков перехвата исключений, где вы можете писать такие вещи, как:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class X extends RuntimeException {
    void print() {}
}
class X1 extends X {}
class X2 extends X {}
 
// With the above
try {
    ...
}
catch (X1 | X2 e) {
    // This compiles for the same reasons!
    e.print();
}

Но, к сожалению, блоки catch — это единственное место в Java, которое позволяет использовать типы сумм.

Именно здесь вступает в игру хитрый и хитрый обходной путь Дэниела Мы можем написать статический метод, который выполняет некоторое «сопоставление с образцом» (если вы щуритесь), используя обобщения, наоборот:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
static <
    T,
    T1 extends T,
    T2 extends T,
    T3 extends T
>
void forEach(
    Tuple3<T1, T2, T3> tuple,
    Consumer<? super T> consumer
) {
    consumer.accept(tuple.v1);
    consumer.accept(tuple.v2);
    consumer.accept(tuple.v3);
}

Вышеизложенное теперь можно безопасно использовать для определения общего супертипа (ов) T1, T2 и T3:

1
2
3
4
Tuple2<Integer, Long> t = tuple(1, 2L);
forEach(t, c -> {
    System.out.println(c.doubleValue());
});

уступая, как и ожидалось:

1
2
1.0
2.0

Это имеет смысл, потому что ограничения общего типа просто задаются «наоборот», то есть когда T1 extends T принудительно T1 extends T , T super T1

Если ты очень сильно щуришься 😉

Этот метод предположительно используется Дэниелом в предстоящем API соответствия шаблонов Javaslang . Мы с нетерпением ожидаем увидеть это в действии!