Общий параметр: работает только алмазный оператор

Предыстория: в этом ответе встал вопрос (первый вариант ответа, если быть точным). Код, представленный в этом вопросе, сводится к минимуму, чтобы объяснить проблему.

Предположим, что у нас есть следующий код:

public class Sample<T extends Sample<T>> {

    public static Sample<? extends Sample<?>> get() {
        return new Sample<>();
    }

    public static void main(String... args) {
        Sample<? extends Sample<?>> sample = Sample.get();
    }
}

Он компилируется без предупреждения и выполняет штраф. Однако, если кто-то попытается каким-то образом определить предполагаемый тип return new Sample<>(); в get() явно компилятор жалуется.

До сих пор у меня создалось впечатление, что алмазный оператор - это всего лишь синтаксический сахар, чтобы не писать явные типы и, следовательно, всегда можно было заменить на какой-то явный тип. В данном примере я не смог определить какой-либо явный тип для возвращаемого значения, чтобы скомпилировать код. Можно ли явно определить общий тип возвращаемого значения или нужен бриллиант-оператор в этом случае?

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


return new Sample<Sample> результат:

Sample.java:6: error: type argument Sample is not within bounds of type-variable T
            return new Sample<Sample>();
                              ^
  where T is a type-variable:
    T extends Sample<T> declared in class Sample
Sample.java:6: error: incompatible types: Sample<Sample> cannot be converted to Sample<? extends Sample<?>>
            return new Sample<Sample>();
                   ^

return new Sample<Sample<?>> результат:

Sample.java:6: error: type argument Sample<?> is not within bounds of type-variable T
            return new Sample<Sample<?>>();
                                    ^
  where T is a type-variable:
    T extends Sample<T> declared in class Sample

return new Sample<Sample<>>(); результаты:

Sample.java:6: error: illegal start of type
           return new Sample<Sample<>>();
                                    ^

Ответы

Ответ 1

JLS просто говорит:

Если список аргументов типа для класса пуст, то выводится форма алмаза <> - аргументы типа класса.

Итак, есть ли какой-нибудь вывод X, который удовлетворит решение? Да.

Конечно, для того, чтобы явным образом определял такой X, вам придется объявить его:

public static <X extends Sample<X>> Sample<? extends Sample<?>> get() {
    return new Sample<X>();
}

Явный Sample<X> совместим с типом возвращаемого Sample<? extends Sample<?>> Sample<? extends Sample<?>>, поэтому компилятор счастлив.

Тот факт, что тип возврата является испорченным Sample<? extends Sample<?>> Sample<? extends Sample<?>> - совсем другая история.

Ответ 2

Создание генерации с помощью подстановочных знаков

Там пара проблем здесь, но прежде чем вникать в них, позвольте мне обратиться к вашему актуальному вопросу:

Можно ли явно определить общий тип возвращаемого значения или нужен бриллиант-оператор в этом случае?

Невозможно явно создать экземпляр Sample<? extends Sample<?>> Sample<? extends Sample<?>> (или Sample<?> на то пошло). Подстановочные знаки не могут использоваться в качестве аргументов типа при создании экземпляра родового типа, хотя они могут быть вложены в аргументы типа. Например, хотя законно создавать экземпляр ArrayList<Sample<?>>, вы не можете создать экземпляр ArrayList<?>.

Наиболее очевидным обходным решением было бы просто вернуть другой конкретный тип, который можно присваивать Sample<?>. Например:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}

    public static Sample<? extends Sample<?>> get() {
        return new X();
    }
}

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

  1. Использование алмазного оператора, как вы делаете прямо сейчас.
  2. Делегирование на общий метод, который фиксирует ваш шаблон с переменной типа.

Хотя вы не можете включать подстановочный шаблон непосредственно в общий экземпляр, вполне правомерно включать переменную типа и что делает возможным вариант (2). Все, что нам нужно сделать, это убедиться, что переменная типа в методе делегата привязана к шаблону на сайте вызова. Каждое упоминание переменной типа подписи метода и тела затем заменяется ссылкой на этот шаблон. Например:

public class Sample<T extends Sample<T>> {
    public static Sample<? extends Sample<?>> get() {
        final Sample<?> s = get0();
        return s;
    }

    private static <T extends Sample<T>> Sample<T> get0() {
        return new Sample<T>();
    }
}

Здесь возвращаемый тип Sample<T> get0() расширяется до Sample<WC#1 extends Sample<WC#1>>, где WC#1 представляет собой захваченную копию подстановочного знака, выведенного из целевой цели в Sample<?> s = get0().

Несколько подстановочных знаков в типе подписи

Теперь позвольте адресовать эту подпись вашего метода. Трудно точно сказать, какой код вы предоставили, но я бы предположил, что возвращаемый тип Sample<? extends Sample<?>> Sample<? extends Sample<?>> is * not *, что вы действительно хотите. Когда подстановочные знаки появляются в типе, каждый шаблон отличается от всех остальных. Нет принудительного применения, когда первый подстановочный знак и второй подстановочный знак относятся к одному типу.

Пусть say get() возвращает значение типа X Если бы вы хотели убедиться, что X расширяет Sample<X>, то вы потерпели неудачу. Рассматривать:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}
    static class Y extends Sample<X> {}

    public static Sample<? extends Sample<?>> get() {
        return new Y();
    }

    public static void main(String... args) {
        Sample<?> s = Sample.get(); // legal (!)
    }
}

В main переменная s содержит значение, которое представляет собой Sample<X> и Y, но не Sample<Y>. Это то, что вы намеревались? Если нет, я предлагаю заменить подстановочный знак в вашей сигнатуре метода на переменную типа, а затем разрешить вызывающему пользователю определить аргумент типа:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}
    static class Y extends Sample<X> {}

    public static <T extends Sample<T>> Sample<T> get() { /* ... */ }

    public static void main(String... args) {
        Sample<X> x = Sample.get();     // legal
        Sample<Y> y = Sample.get();     // NOT legal

        Sample<?> ww = Sample.get();    // legal
        Sample<?> wx = Sample.<X>get(); // legal
        Sample<?> wy = Sample.<Y>get(); // NOT legal
    }
}

Вышеупомянутая версия гарантирует, что для некоторого возвращаемого значения типа A возвращаемое значение расширяет Sample<A>. Теоретически, он работает даже тогда, когда T привязан к шаблону. Зачем? Он возвращается к подстановочному экрану:

В вашем исходном методе get эти две маски могут в конечном итоге ссылаться на разные типы. Фактически, ваш тип возврата был Sample<WC#1 extends Sample<WC#2>, где WC#1 и WC#2 являются отдельными подстановочными знаками, которые никоим образом не связаны. Но в приведенном выше примере привязка T к подстановочному знаку захватывает его, позволяя тому же подстановочному знаку появляться в нескольких местах. Таким образом, когда T привязано к шаблону WC#1, возвращаемый тип расширяется до Sample<WC#1 extends Sample<WC#1>. Помните, что нет способа выразить этот тип непосредственно на Java: это можно сделать только с помощью вывода типа.

Теперь я сказал, что это работает с условными символами в теории. На практике вы, вероятно, не сможете реализовать get таким образом, чтобы общие ограничения выполнялись во время исполнения. Это из-за стирания типа: компилятор может classcast команду classcast для проверки того, что возвращаемое значение является, например, как X и Sample, но не может проверить, что это фактически Sample<X>, потому что все общие формы Sample имеют одинаковый тип времени выполнения. Для аргументов конкретного типа компилятор обычно может запрещать компиляцию подозрительного кода, но когда вы вводите в микс подстановочные знаки, сложные общие ограничения становятся трудными или невозможными для обеспечения соблюдения. Предостережение для покупателя :).


В сторону

Если все это вас сбивает с толку, не волнуйтесь: подстановочные знаки и подстановочный знак являются одними из самых сложных аспектов генериков Java для понимания. Также неясно, понимает ли это то, что на самом деле поможет вам с вашей непосредственной целью. Если у вас есть API, лучше всего представить его в обмене стека кода обзора и посмотреть, какую обратную связь вы получите.