#java #generics #type-safety
#java #обобщения #безопасность типов
Вопрос:
У меня есть примерно такие типы:
interface Record {}
interface UpdatableRecord extends Record {}
interface Insert<R extends Record> {
// Calling this method only makes sense if <R extends UpdatableRecord>
void onDuplicateKeyUpdate();
}
Я хотел бы иметь дополнительное ограничение на <R>
при вызове onDuplicateKeyUpdate()
. Причина этого в том, что этот метод имеет смысл только тогда, когда <R>
привязан к любому подтипу UpdatableRecord
, а не только Record
. Примеры:
Insert<?> i1;
Insert<Record> i2;
Insert<UpdatableRecord> i3;
// these shouldn't compile
i1.onDuplicateKeyUpdate();
i2.onDuplicateKeyUpdate();
// this should compile
i3.onDuplicateKeyUpdate();
Есть ли какой-либо трюк или способ, который я могу использовать, чтобы добавить дополнительное ограничение для универсального типа класса только для одного объявления метода?
ПРИМЕЧАНИЕ:
- Объявление
Insert<R extends UpdatableRecord>
— это не вариант, потому что я хочу гибко использоватьInsert<R extends Record>
для записей, которые не подлежат обновлению - Вывод
UpdatableInsert<R extends UpdatableRecord> extends Insert<R>
и нажатие на метод может быть вариантом, но я действительно не хочу вводить новый тип. У меня есть несколько таких методов с несколькими различными ограничениями, которые привели бы к взрыву типов. - Выбрасывание
UnsupportedOperationException
довольно очевидно, но, похоже, это делается в Java 1.4. Мне действительно интересно, могу ли я использовать generics и компилятор для решения этой проблемы.
Идеальное решение:
// This would be an annotation to be interpreted by the compiler. There is no such
// thing in Java, as far as I know. But maybe there's a trick having the same effect?
@Require(R extends UpdatableRecord)
void onDuplicateKeyUpdate();
Комментарии:
1. Можно ли было бы удалить
onDupliceteKeyUpdate()
из интерфейсаInsert
и получить второеinterface UpdatableInsert<R extends UpdatableRecord> extends Insert<R>
переопределениеvoid onDuplicateKeyUpdate();
?2. Почему бы просто не объявить interface Insert равным <R extends UpdateableRecord>? Если это невозможно, то то, о чем вы просите, невозможно, если вы не сделаете что-то вроде того, что предложил Говард.
3. Спасибо за ваши подсказки. Я обновлю свой вопрос
Ответ №1:
Если вы определяете его в интерфейсе, то он должен быть реализован в любом классе, который желает реализовать интерфейс. Таким образом, вы никак не можете обойти реализацию onDuplicateKeyUpdate
в любом классе, который реализует Insert<R extends Record>
интерфейс.
Вы указали это R extends Record
в интерфейсе, и поэтому это становится частью контракта, что означает, что может использоваться любой подтип Record
. Я не могу придумать способ добавить дополнительное ограничение, используя generics, которое ограничивает его только типом UpdatableRecord
.
Кроме того, вы не можете принимать решения в своем интерфейсе на основе типа R
, потому что эта информация исчезает из-за удаления типа.
Мне кажется, что ваша проблема вращается вокруг разницы между Record
и UpdatableRecord
. Поскольку интерфейс предназначен для общего контракта, я не думаю, что в вашем интерфейсе должно быть поведение, зависящее от типа. Таким образом, вам пришлось бы найти способ устранить разницу другим способом. Моим решением было бы реализовать метод (который возвращает boolean
) в вызываемом Record
интерфейсе canUpdate
, который вы можете использовать в onDuplicateKeyUpdate
. Таким образом, вы можете выдать UnsupportedOperationException
, если запись не поддерживает эту операцию, или ничего не делать (если вы хотите избежать исключения, и если ничего не делать имеет смысл в контексте вашей бизнес-логики).
Это не укладывается у меня в голове; может быть лучшее решение. Я попытаюсь подумать об этом еще немного.
Редактировать
Я подумал об этом немного больше, и это была ваша последняя правка, которая заставила меня задуматься об этом. <R extends Record>
Применяется ко всему классу. Таким образом, нет способа переопределить или сузить его в контексте конкретного метода. Аннотация, подобная той, о которой вы упомянули, должна быть реализована на уровне компилятора, и на этом уровне я даже не уверен, будет ли у вас доступ к типу R (из-за стирания типа). Итак, то, что вы говорите, может быть невозможно.
На концептуальном уровне то, о чем вы просите, не должно быть возможным IMO, потому что вы указываете исключение из контракта в вашем контракте. Также похоже, что детали реализации проникают в ваш интерфейс, потому что интерфейсы не должны пытаться выяснить, что что-то может или не может сделать; это должно быть решено в конкретной реализации.
Комментарии:
1. Спасибо за ваш вклад. Мне не нужно добавлять какие-либо дополнительные методы или проверки, чтобы иметь возможность запускать
UnsupportedOperationExceptions
. Это самый простой способ сделать это, наряду с некоторой документацией в Javadoc. Но я подумал, что, возможно, есть способ сделать это на уровне компилятора (и, следовательно, намного безопаснее)
Ответ №2:
Vivin делает хорошее замечание о контракте, который вы заключили при создании Insert
. Я бы задался вопросом, onDuplicateKeyUpdate()
действительно ли это имеет смысл в этом контексте. Это кажется довольно специфичным только для одного подтипа Record
.
Если вы можете перенести эту функциональность в другой Insert
метод (скажем execute()
?), вы можете проверить тип объекта там и действовать соответствующим образом. Например,
[pseudo-code]
method execute( R record )
try
insertRecord( record )
catch ( KeyAlreadyExistsException e )
if ( record instanceof UpdatableRecord )
updateRecord( record )
end if
end try
end method
Если вы ищете более объектно-ориентированный подход, вы могли бы включить логику для вставки в Record
сами подклассы. Там я бы изменил Insert
на Insertable
, дал ему insert()
метод и Record
реализовал Insertable
. Тогда каждый подкласс Record
мог бы знать правильный путь к insert()
самому себе. Однако я не уверен, соответствует ли это вашей стратегии проектирования…
Комментарии:
1. @Mike, спасибо за твой вклад. Это имеет смысл. К сожалению, мой пример слишком абстрактен, чтобы действительно описать мою «реальную проблему». На самом деле, я не обновляю
R
, а таблицу базы данных типаR
.onDuplicateKeyUpdate()
на самом деле является частью более крупного DSL для моделирования SQL в Java. И это предложение, которое не следует использовать, еслиR
это неUpdatableRecord
так… Хотя трудно выразить это в нескольких словах 🙂2. Спасибо за разъяснение; это, безусловно, ставит вопрос по-другому. Похоже, что
onDuplicateKeyUpdate()
это вызывается внешним программным обеспечением при вставке, когда существует дубликат ключа. Это верно? По сути, это метод обратного вызова?3. Да, внешнее программное обеспечение вызывает этот метод. Это гораздо больше, чем это. Вы действительно можете моделировать SQL в Java так, как если бы это был встроенный DSL (что-то вроде LINQ to SQL в C #). Найдите несколько примеров здесь: sourceforge.net/apps/trac/jooq/wiki/Manual/DSL/SELECT
4. Итак, вы пишете API для этого DSL, который позволяет его пользователям использовать SQL-подобный синтаксис в Java. (Очень круто!) Этот конкретный случай представляет собой инструкцию insert, одним из предложений SQL для которого является «при обновлении дубликата ключа». Если вы проверяете запрос перед отправкой его во что бы то ни было, что вы используете в качестве базы данных, то вам в любом случае должен потребоваться доступ к
Class<R>
объекту как членуInsert<R>
, да? В этом случае вы можете выдать проверяемое исключение, если метод вызван ненадлежащим образом. Делая это проверяемым исключением, пользователь знает, что он должен быть осторожен. Похоже ли это на вашу ситуацию?5. Вы получили это! В настоящее время я создаю проверяемое исключение, но я бы предпочел проверку во время компиляции. Компилятор располагает всей необходимой информацией. Я просто не знаю, есть ли трюк (примечание: трюк. Я не думаю, что Java была разработана для этих вещей), чтобы позволить компилятору формально проверять эти вещи.