Альтернативы CRTP в Java

Шаблон CRTP позволяет имитировать так называемый самотипы в Java, e. грамм.:

abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> implements Comparable<SELF> {
    @Override
    public final int compareTo(final SELF o) {
        // ...
    }
}

final class C1 extends AbstractFoo<C1> {
    // ...
}

final class C2 extends AbstractFoo<C2> {
    // ...
}

С помощью приведенного выше кода (интерфейс Comparable был выбран только для ясности; конечно, есть и другие варианты использования), я могу легко сравнить два экземпляра C1:

new C1().compareTo(new C1());

но не AbstractFoo потомков, чьи конкретные типы различны:

new C1().compareTo(new C2()); // compilation error

Однако этим шаблоном легко злоупотребить:

final class C3 extends AbstractFoo<C1> {
    // ...
}

// ...

new C3().compareTo(new C1()); // compiles cleanly

Кроме того, проверки типов выполняются исключительно во время компиляции, т.е. е. можно легко смешать экземпляры C1 и C2 в одном TreeSet и сравнить их друг с другом.

Какая альтернатива CRTP в Java имитирует собственные типы без возможности злоупотреблений, как показано выше?

П. S. Я заметил, что этот шаблон широко не используется в стандартной библиотеке - только EnumSet и его потомки реализуют его.


person Bass    schedule 02.10.2018    source источник
comment
Какое измерение определяет, лучше чем альтернатива CRTP? Вопрос звучит субъективно.   -  person jaco0646    schedule 02.10.2018
comment
@ jaco0646 Хорошо, я понимаю, что вопрос может показаться основанным на мнении, но я был бы признателен за любой ответ, описывающий возможные альтернативы. Соответственно отредактировал вопрос.   -  person Bass    schedule 02.10.2018
comment
Если любая альтернатива приемлема, вопрос становится слишком широким: нет основы для сравнения возможных ответов. К вашему сведению, я думаю, что вопрос интересный, поэтому я пытаюсь подтолкнуть его к тому, чтобы он был менее открытым. Возможно, Какая альтернатива CRTP в Java эмулирует самотипы без возможности злоупотребления, как показано выше?   -  person jaco0646    schedule 02.10.2018
comment
@ jaco0646 Спасибо, отредактировано, как вы предлагаете.   -  person Bass    schedule 02.10.2018
comment
@Bass это может быть?   -  person Eugene    schedule 03.10.2018
comment
@ Евгений Спасибо. Формально это можно квалифицировать как правильный ответ на мой вопрос. Тем не менее, если вы являетесь автором API, требуется написать гораздо больше кода библиотеки, дублирующего идентичные реализации. Взгляните на библиотеку AssertJ - ее код увеличился бы вдвое по сравнению с текущим размером = )   -  person Bass    schedule 03.10.2018
comment
@Eugene Кроме того, ваш подход работает для типов, возвращаемых методом, но не для параметров метода.   -  person Bass    schedule 03.10.2018
comment
@Bass Я почти не читал, это было просто предложение ..   -  person Eugene    schedule 03.10.2018


Ответы (1)


Я не думаю, что то, что вы показали, является «оскорблением». Весь код, в котором используются AbstractFoo и C3, по-прежнему полностью типобезопасен, пока не выполняется небезопасное приведение типов. Границы SELF в AbstractFoo означают, что код может полагаться на тот факт, что SELF является подтипом AbstractFoo<SELF>, но код не может полагаться на тот факт, что AbstractFoo<SELF> является подтипом SELF. Так, например, если у AbstractFoo был метод, который возвращал SELF, и он был реализован путем возврата this (что должно было быть возможным, если бы это действительно был «самотип»), он не компилировался:

abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> {
    public SELF someMethod() {
        return this; // would not compile
    }
}

Компилятор не позволяет вам скомпилировать это, потому что это небезопасно. Например, запуск этого метода на C3 вернет this (который на самом деле является экземпляром C3) как тип C1, что вызовет исключение приведения к классу в вызывающем коде. Если вы попытались проскользнуть мимо компилятора, используя приведение типа return (SELF)this;, вы получите предупреждение о непроверенном приведении, что означает, что вы берете на себя ответственность за его небезопасность.

И если ваш AbstractFoo действительно используется таким образом, который опирается только на тот факт, что SELF extends AbstractFoo<SELF> (как сказано в ограничении), и не полагается на тот факт, что AbstractFoo<SELF> extends SELF, тогда почему вас беспокоит "злоупотребление" C3? Вы все еще можете писать свои классы C1 extends AbstractFoo<C1> и C2 extends AbstractFoo<C2> нормально. И если кто-то другой решит написать класс C3 extends AbstractFoo<C1>, то до тех пор, пока он будет писать его таким образом, чтобы не использовать небезопасное приведение типов, компилятор гарантирует, что он по-прежнему является типобезопасным. Возможно, такой класс не сможет сделать ничего полезного; Я не знаю. Но это все еще безопасно; так почему это проблема?

Причина, по которой рекурсивная граница типа <SELF extends AbstractFoo<SELF>> не так часто используется, заключается в том, что в большинстве случаев она не более полезна, чем <SELF>. Например, параметр типа интерфейса Comparable не имеет привязки. Если кто-то решает написать класс Foo extends Comparable<Bar>, он может это сделать, и он безопасен по типу, хотя и не очень полезен, потому что в большинстве классов и методов, которые используют Comparable, у них есть переменная типа <T extends Comparable<? super T>>, которая требует, чтобы T был сопоставим с сам, поэтому класс Foo нельзя использовать в качестве аргумента типа ни в одном из этих мест. Но для кого-то все еще нормально писать Foo extends Comparable<Bar>, если они хотят.

Единственные места, где рекурсивная граница, такая как <SELF extends AbstractFoo<SELF>>, находится в месте, которое фактически использует тот факт, что SELF расширяет AbstractFoo<SELF>, что довольно редко. Одно место находится в чем-то вроде шаблона построителя, в котором есть методы, возвращающие сам объект, который можно связать в цепочку. Итак, если у вас есть такие методы, как

abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> {
    public SELF foo() { }
    public SELF bar() { }
    public SELF baz() { }
}

и у вас было общее значение AbstractFoo<?> x, вы можете делать такие вещи, как x.foo().bar().baz(), чего не могли бы делать, если бы оно было объявлено как abstract class AbstractFoo<SELF>.

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

abstract class AbstractFoo<SELF must be own type> {
    public abstract int compareTo(SELF o);
}

class C1 extends AbstractFoo<C1> {
    @Override
    public int compareTo(C1 o) {
        // ...
    }
}

class SubC1 extends C1 {
    @Override
    public int compareTo(/* should it take C1 or SubC1? */) {
        // ...
    }
}

Здесь SubC1 неявно наследует AbstractFoo<C1>, но это нарушает договор, согласно которому SELF должен быть типом реализующего класса. Если SubC1.compareTo() должен принимать C1 аргумент, то уже неверно, что тип полученной вещи совпадает с типом самого текущего объекта. Если SubC1.compareTo() может принимать SubC1 аргумент, то он больше не переопределяет C1.compareTo(), так как он больше не принимает такой широкий набор аргументов, который принимает метод в суперклассе.

person newacct    schedule 01.02.2020