Почему Java не может вывести правильный тип с помощью связанного универсального типа Self в подклассах - PullRequest
0 голосов
/ 06 июля 2019

Предположим, у меня есть этот код:

 class A<THIS extends A> {
        public THIS self() {
            return (THIS) this;
        }
    }

 class B extends A<B> { }

 A a = new A<>().self().self().self();  // OK
 B b = new B().self().self().self().self().self().self(); // OK

И это прекрасно компилируется. Но когда я добавляю еще один уровень наследования, он не работает.

    class A<THIS extends A> {
        public THIS self() {
            return (THIS) this;
        }
    }
    class B<THIS extends B> extends A<B> { }
    class C extends B<C> { }

  A<A> self2 = new A<A>().self().self().self();  // OK
  B<B> self = new B<B>().self().self().self().self(); // error -> A
  C self1 = new C().self().self();  // error -> A

Я пробовал разные универсальные типы, но ничего не помогает.

Что я должен сделать, чтобы этот код компилировался?

Ответы [ 2 ]

2 голосов
/ 06 июля 2019

Этот подход несовершенен.

Это не мешает объявлению, например

class D extends A<B> {
}

, которая скомпилирует, но выдает исключение во время выполнения, более конкретно:

new D().self().getClass() // => ClassCastException

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

Обычно это интерфейс, например

interface Adaptable {
    T getAdapter(Class<? extends T> key);

    // for those who don't like to type long method names
    default T as(Class<? extends T> key) {
        return getAdapter(key);
    }
}

Реализация может выглядеть как

class A implements Adaptable {
    @Override
    public T getAdapter(Class<? extends T> key) {
        /*
         * To be less strict, one might also check for 'key.isInstance(this)',
         * but it's an implementation decision.
         */
        if(getClass() == key) {
            return key.cast(this);
        }
        return null;
    }
}

Однако шаблон адаптера позволяет предоставлять другие объекты, обычно специализированные виды на цель, см. Пример FileSource ниже.

Основным недостатком этого подхода является то, что клиент всегда должен проверять наличие адаптера. Однако, если клиент знал, что объект является искомого подкласса, который он ищет, он мог бы просто разыграть его, поэтому мы ничего не теряем. Интерфейс также можно расширить с помощью java.util.Optional, но основная идея остается прежней.

interface Adaptable {
    Optional<T> getAdapter(Class<? extends T> key);
}

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

class Source implements Adaptable {
    @Override
    public Optional<T> getAdapter(Class<? extends T> key) {
        if(getClass() == key) {
            return Optional.of(key.cast(this));
        }
        return Optional.empty();
    }
}

Теперь есть базовая реализация, FileSource, которая обычно доступна как java.io.File.

class FileSource extends Source {
    private File pointer;

    public File asFile() {
        return pointer;
    }
}

Клиент теперь может проверить, доступен ли источник в виде файла, и выполнить некоторую операцию, используя базовый java.io.File.

Source source;
...
source.getAdapter(FileSource.class).ifPresent(fileSource -> {
    File file = fileSource.asFile();
    // do your magic with 'file'
});

Еще лучше, FileSource может просто предоставить адаптер для File. На этом этапе клиенту даже не нужно заботиться о подклассе реализации, а только о том, что он на самом деле хочет.

class FileSource extends Source {
    private File pointer;

    @Override
    public Optional<T> getAdapter(Class<? extends T> key) {
        if(File.class == key) {
            return Optional.of(key.cast(asFile()));
        }
        return super.getAdapter(key);
    }

    public File asFile() {
        return pointer;
    }
}

и

Source source;
...
source.getAdapter(File.class).ifPresent(file -> {
    // do your magic with file
});
0 голосов
/ 06 июля 2019

После нескольких часов страданий я нахожу правильный путь.

class A<THIS extends A<THIS>> {
    public THIS self() {
        return (THIS) this;
    }
}
class B<T extends B<T>> extends A<T> { }
class C<T extends C> extends B<C<T>> { }
A a = new A<>().self().self().self().self().self().self();     // OK
B b = new B<>().self().self().self().self().self().self();     // OK
C c = new C<>().self().self().self().self().self().self();    // OK

И да, это не безопасно, как @Izruo points.

...