Тип псевдонимов для Java-дженериков
У меня довольно сложный набор общих классов в Java. Например, у меня есть интерфейс
interface Doable<X,Y> {
X doIt(Y y);
}
и реализация
class DoableImpl implements Doable<Foo<Bar<Baz,Qux>>,Foo<Bar<Zot,Qux>>> {
Foo<Bar<Baz,Qux>> doIt(Foo<Bar<Zot,Qux>> fooBZQ) { ... }
}
В реальной реализации Doable
имеет довольно много методов, поэтому Foo<Bar<Baz,Qux>>
и т.д. появляются снова и снова.
(Верьте или нет, общие типы довольно болезненны, чем это. Я упростил их для примера.)
Я хотел бы упростить их, чтобы сохранить себя набрав и облегчить напряжение на моих глазах. Я хотел бы иметь простой "псевдоним типа" для Foo<Bar<Baz,Qux>>
и т.д., Например FooBBQ
и FooBZQ
.
Моя нынешняя идея - определить классы-оболочки:
class FooBBQ {
public static FooBBQ valueOf(Foo<Bar<Baz,Qux>> fooBBQ) {
return new FooBBQ(fooBBQ);
}
private Foo<Bar<Baz,Qux>> fooBBQ;
private FooBBQ(Foo<Bar<Baz,Qux>> fooBBQ) {
this.fooBBQ = fooBBQ;
}
public Foo<Bar<Baz,Qux>> toGeneric() {
return fooBBQ;
}
}
class FooBZQ { /* pretty much the same... */ }
class DoableImpl implements Doable<FooBBQ,FooBZQ> {
FooBBQ doIt(FooBZQ fooBZQ) { ... }
}
Это хорошо работает, но имеет несколько недостатков:
- Нам нужно определить отдельные обертки для каждого общего экземпляра. Классы-оболочки являются короткими и стилизованными, но я не могу определить способ их макроопределения.
-
У нас есть служебные данные перевода (концептуально, если не оперативно) вызова valueOf
и toGeneric
для преобразования между FooBBQ
и Foo<Bar<Baz,Qux>>
. Например, если doIt
вызывает некоторую библиотечную процедуру, которая ожидает Foo<Bar<Zot,Qux>>
(что делает реальная реализация), мы получаем что-то вроде
return FooBBQ.valueOf( libraryCall( fooBZQ.toGeneric() ) )
где мы первоначально имели бы
return libraryCall(fooBZQ);
Есть ли другой способ получить поведение типа "псевдоним", которое я хочу здесь? Возможно, используя некоторые сторонние инструменты для макросов? Или мне нужно принять, что мне придется много набирать, одним способом (используя общие типы в реализации) или другим (писать обертки для них)? Может быть, если это множество общих параметров, которые летают, это всего лишь плохая идея, и мне нужно переосмыслить проблему?
[UPDATE] ОК, я запрещаю дальнейшие ответы "не делай этого". Возьмите его как данность, что Foo<Bar<Baz,Qux>>
имеет истинную ценность в моей проблемной области (Пит Киркхам может быть прав, что у него достаточно значения, чтобы получить надлежащий класс-оболочку с описательным именем). Но это проблема программирования; не пытайтесь определить проблему.
Ответы
Ответ 1
Если вам нужна полная безопасность типов, я не думаю, что вы можете сделать лучше без каких-либо классов-оболочек. Но почему бы не сделать эти классы наследовать/реализовывать исходные общие версии, например:
public class FooBBQ extends Foo<Bar<Baz,Qux>> {
...
}
Это устраняет необходимость в методе toGeneric()
, и, на мой взгляд, более понятно, что это просто псевдоним типа. Кроме того, общий тип может быть переведен в FooBBQ
без предупреждения компилятора. Мое личное предпочтение заключалось бы в том, чтобы сделать интерфейсы Foo, Bar, Baz...
, если это возможно, даже если в реализации произойдет некоторое дублирование кода.
Теперь, не зная конкретную проблемную область, трудно сказать, нужно ли вам сказать FooBBQ
, как в вашем примере, или, возможно,
public class FooBar<X, Y> extends Foo<Bar<X, Y>> {
...
}
С другой стороны, подумали ли вы о простой настройке компилятора Java, чтобы не показывать некоторые общие предупреждения, и просто опустить части общего определения? Или используйте стратегически размещенную @SuppressWarnings("unchecked")
? Другими словами, вы можете сделать DoableImpl
только "частично обобщенным":
class DoableImpl implements Doable<Foo<Bar>>,Foo<Bar>> {
Foo<Bar> doIt(Foo<Bar> foobar) { ... }
}
и игнорировать предупреждения ради меньшего количества помех кода? Опять же, трудно решить без конкретного примера, но это еще одна вещь, которую вы можете попробовать.
Ответ 2
Scala имеет приятную поддержку псевдонимов типов. Например:
type FooBBQ = Foo[Bar[Baz,Qux]]
Я понимаю, что этот ответ не поможет, если у вас нет возможности переключения на Scala. Но если у вас есть возможность переключения, возможно, вам будет легче.
Ответ 3
Возможно, если это множество общих параметров, которые летают, это всего лишь плохая идея, и мне нужно переосмыслить проблему?
Скорее всего. Нужно ли специализировать "Doit" в 8 измерениях?
Во многих случаях эти типы не существуют в вакууме, и вы должны думать о том, какие объекты домена используют вашу "оболочку", а не использовать их в качестве удобства кодирования.
Ответ 4
Ну, у Java нет псевдонимов типов, поэтому вам не повезло. Однако псевдонимы типов иногда могут быть заменены переменными типа! Таким образом, мы решаем проблему слишком большого числа генериков с еще большим количеством дженериков!
Как вы лишили весь контент из своего примера, я не могу догадаться, где или имеет смысл вводить дополнительные переменные типа, но здесь есть одно возможное разложение:
class DoableImplFoo<A,B> implements Doable<Foo<A>,Foo<B>> {
public DoableImplFoo(SomePropertyOf<A,B> aAndBAreGoodEnough) { ... }
Foo<A> doIt(Foo<B> fooB) { ... }
}
Когда вы создаете экземпляр A
до Bar<Baz,Qux>
и B
до Bar<Zot,Qux>
позже, вы можете обнаружить, что есть еще один шаблон, но он может быть меньше, чем вы изначально имели.
Ответ 5
Я бы сказал, что да, вам нужно переосмыслить проблему. Объявление
class DoableImpl implements Doable<Foo<Bar<Baz,Qux>>,Foo<Bar<Zot,Qux>>> {
Foo<Bar<Baz,Qux>> doIt(Foo<Bar<Zot,Qux>> fooBZQ) { ... }
}
- довольно очевидный случай использования дженериков.