Рубрики
Без рубрики

Самореферентные дженерики в Java

Почему универсальные типы с самостоятельными ссылками не так полезны, как кажутся. Помеченный java.

У меня продолжаются дебаты с коллегами об использовании самореферентных дженериков в Java, в которых я утверждаю, что они необоснованны, а другие продолжают верить в их использование. Под самореферентными обобщениями я подразумеваю тип, определенный в терминах самого себя, например:

interface Foo { ... }

interface Bar extends Foo { ... }

На первый взгляд это кажется полезным, поскольку позволяет обеспечить правильную типизацию параллельных иерархий. Например, рассмотрим складскую систему, которая работает с различными типами деталей. Каждая деталь имеет идентификатор, специфичный для конкретного типа:

class Serial

{ // stores some identifier specific to P } interface Part

{ Serial

getSerial(); }

Всякий раз, когда мы определяем класс детали, мы объявляем, что он реализует Часть параметризована самим классом. Это гарантирует, что серийный номер будет того же типа:

class Whatsit implements Part {
    private final Serial serial; 

    @Override
    public Serial getSerial() {
        return serial;
    }
}

У нас даже может быть база данных деталей, которая использует полиморфный метод, чтобы гарантировать, что вы запросите Что это вы получаете только Что это .

interface PartDatabase {
    

> P getPart(Serial

serial); }

Пока все это звучит разумно, и мы даже можем работать с деталями в целом таким образом:

class HomogenousPartCruncher

> { private final PartDatabase database; void crunchParts(List> serials) { serials.forEach(serial -> crunch(database.getPart(serial))); } void crunch(Part

part) { // TODO } }

Любой данный экземпляр Однородной части Хрустит может работать только с одним типом Стороны однако, тип, с которым он создан. Подсказки в названии. Но как только мы пытаемся использовать объектно ориентированный стиль кодирования, все разваливается:

class HeterogeneousPartCruncher {
    private final PartDatabase database;

    void crunchParts(List>> serials) {
        serials.forEach(serial -> crunch(database.getPart(serial)));
    }

    void crunch(Part part) {
        // TODO
    }
}

Мы не знаем тип каждого серийного номера в списке, но это, по крайней мере, часть > . Поэтому мы можем попытаться получить фактический Часть > из базы данных с использованием этого серийного номера. Но компилятор жалуется с

error: method getPart in interface PartDatabase cannot be applied to given types;
        serials.forEach(serial -> crunch2(database.getPart(serial)));
                                                  ^
  required: Serial

found: Serial reason: inference variable P has incompatible bounds equality constraints: CAP#2,CAP#1 upper bounds: Part

where P is a type-variable: P extends Part

declared in method

getPart(Serial

) where CAP#1,CAP#2 are fresh type-variables: CAP#1 extends Part from capture of ? extends Part CAP#2 extends Object from capture of ?

Похоже, это предполагает, что границы типа в Последовательном должны быть расширенной частью , но если наше приложение использует Последовательный ы для вещей, отличных от Части – мы не можем этого сделать. Это также создает циклическую зависимость, которая никогда не является улучшением вашей общей структуры кода.

Единственный способ исправить это – притвориться, что Часть вообще не является универсальной, и реализуйте crunch Parts() вот так:

    @SuppressWarnings("unchecked")
    void crunchParts(PartDatabase database, List> serials) {
        serials.forEach(serial -> crunch(database.getPart(serial)));
    }

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

interface Part {
    Serial getSerial();
}

class Whatsit implements Part {
    @Override
    public Serial getSerial() { ... }
    }
}

interface PartDatabase {
    

P getPart(Serial

serial); } class HomogenousPartCruncher

{ private final PartDatabase database; void crunchParts(List> serials) { serials.forEach(serial -> crunch(database.getPart(serial))); } void crunch(P part) { // TODO } } class HeterogeneousPartCruncher { private final PartDatabase database; void crunchParts(List> serials) { serials.forEach(serial -> crunch2(database.getPart(serial))); } void crunch2(Part part) { // TODO } }

Код более чистый и понятный, имеет те же гарантии безопасности типов, но не нуждается в > подстановочные знаки или @Подавление предупреждений("снято") везде.

Единственный способ использовать самореферентные дженерики без раздражающих подстановочных знаков и подавления предупреждений о непроверенных приведениях – это для всех – и я имею в виду все – ваш код для работы только с конкретными типами. Это явно не объектно-ориентированный стиль, который я нахожу любопытным в языке, настолько посвященном продвижению объектно-ориентированного программирования, что иерархии классов являются практически единственным инструментом абстракции, который он предлагает.

Оригинал: “https://dev.to/neilgall/self-referential-generics-in-java-53pc”