Общий параметр: работает только алмазный оператор
Предыстория: в этом ответе встал вопрос (первый вариант ответа, если быть точным). Код, представленный в этом вопросе, сводится к минимуму, чтобы объяснить проблему.
Предположим, что у нас есть следующий код:
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<>
содержащий подстановочные знаки, тогда вы должны полагаться на общий вывод, чтобы выработать аргументы типа для вас. Есть несколько способов сделать это, но обычно это связано с одним из следующих:
- Использование алмазного оператора, как вы делаете прямо сейчас.
- Делегирование на общий метод, который фиксирует ваш шаблон с переменной типа.
Хотя вы не можете включать подстановочный шаблон непосредственно в общий экземпляр, вполне правомерно включать переменную типа и что делает возможным вариант (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, лучше всего представить его в обмене стека кода обзора и посмотреть, какую обратную связь вы получите.