Self-referential generic, часть 1



Кто участвовал в декабрьском адвенте, точно помнит, что еnum компилируется в наследник класса Enum:



public enum Animal {WOLF, TIGER}



public class Animal extends Enum {

public static final Animal WOLF;

public static final Animal TIGER;

}

Подробнее об этом и енамах в целом можно почитать тут — раз, два и три.



В определении класса Enum используется конструкция, которая называется self-referential generic (или self-bound type, или recursive generic):



EnumᐸE extends EnumᐸEᐳᐳ



В этом посте расскажу, что это такое и зачем нужно.



Чтобы понять, какая проблема решается, представим, что этой конструкции нет. И определение енама выглядит так:



public abstract class MyEnum implements ComparableᐸMyEnumᐳ



Пользователь определяет enum Animal и enum Converter. Компилятор превращает это в классы



Animal extends MyEnum

Converter extends MyEnum



Каждый класс должен реализовать интерфейс ComparableᐸMyEnumᐳ и метод compareTo. Чтобы не сравнивать животных и конвертеры, придётся использовать instanceof:



public final int compareTo(MyEnum o) {

if (o instanceOf Animal other) {

// сравниваем зверюшек

// return ...

}

throw IllegalArgumentException();

}



В самом instanceOf нет ничего плохого. Тем более этот код генерируется при компиляции и остаётся за кадром.



Есть более важный момент. Пользователь может спокойно сравнить животное и конвертер, ошибка возникнет только в рантайме. Это выглядит странно, ведь enum Animal и enum Converter никак не связаны между собой.



Здесь дженерик выходит на сцену:



public abstract class EnumᐸE extends EnumᐸEᐳᐳ implements ComparableᐸEᐳ



🔸 Добавляем параметр E, совместимый с классом Enum

🔸 Используем E в интерфейсе Comparable

🔸 Компилируем enum Animal в

public class Animal extends EnumᐸAnimalᐳ

🔸 Теперь Comparable использует тип Animal, и метод compareTo станет таким:

public int compareTo(Animal o)



Убрали instanceOf, код стал меньше и быстрее

При компиляции происходит проверка типов:



Animal zebra = Animal.ZEBRA;

Converter csv = Converter.CSV;

zebra.compareTo(csv); // не скомпилируется!



Self-referential generic позволяет использовать дочерний тип в интерфейсах и методах родителя. Для некоторых кейсов этот приём здорово упрощает код и снижает количество ошибок. В следующем посте покажу ещё один пример использования.



Ответ на вопрос перед постом: self-referential generic помогает ограничить сравнение разных enum между собой.