Иногда средний разработчик сталкивается с ситуацией, когда ему приходится отображать значения произвольных типов в определенном контейнере. Однако API коллекции Java обеспечивает только параметризацию контейнера. Что ограничивает безопасное использование типа HashMap
например, одним типом значения. Но что, если вы хотите смешать яблоки и груши?
К счастью, существует простой шаблон проектирования, который позволяет отображать различные типы значений с использованием обобщенных типов Java, которые Джошуа Блох описал как безопасный гетерогенный контейнер в своей книге « Эффективная Java» (второе издание, пункт 29).
В последнее время я наткнулся на некоторые не совсем подходящие решения по этой теме, и у меня возникла идея объяснить проблемную область и проработать некоторые аспекты реализации в этом посте.
Типы отличительных значений карты с использованием обобщенных данных Java
Для примера рассмотрим, что вы должны предоставить некоторый контекст приложения, который позволяет привязывать значения произвольных типов к определенным ключам. Простая не типичная безопасная реализация с использованием ключей String
поддерживаемых HashMap
может выглядеть так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class Context { private final Map<String,Object> values = new HashMap<>(); public void put( String key, Object value ) { values.put( key, value ); } public Object get( String key ) { return values.get( key ); } [...] } |
Следующий фрагмент показывает, как этот Context
может использоваться в программе:
1
2
3
4
5
6
|
Context context = new Context(); Runnable runnable = ... context.put( "key" , runnable ); // several computation cycles later... Runnable value = ( Runnable )context.get( "key" ); |
Недостаток этого подхода можно увидеть в шестой строке, где необходим нисходящий бросок. Очевидно, это может привести к ClassCastException
в случае, если пара ключ-значение была заменена другим типом значения:
01
02
03
04
05
06
07
08
09
10
|
Context context = new Context(); Runnable runnable = ... context.put( "key" , runnable ); // several computation cycles later... Executor executor = ... context.put( "key" , executor ); // even more computation cycles later... Runnable value = ( Runnable )context.get( "key" ); // runtime problem |
Причину таких проблем может быть трудно отследить, так как соответствующие шаги реализации могут широко распространяться в вашем приложении. Чтобы улучшить ситуацию, представляется разумным связать значение не только с его ключом, но и с его типом.
Распространенные ошибки, которые я видел в нескольких решениях, следующих за этим подходом, более или менее сводятся к следующему варианту Context
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class Context { private final <String, Object> values = new HashMap<>(); public <T> void put( String key, T value, Class<T> valueType ) { values.put( key, value ); } public <T> T get( String key, Class<T> valueType ) { return ( T )values.get( key ); } [...] } |
Опять же, базовое использование может выглядеть так:
1
2
3
4
5
6
|
Context context = new Context(); Runnable runnable = ... context.put( "key" , runnable, Runnable. class ); // several computation cycles later... Runnable value = context.get( "key" , Runnable. class ); |
На первый взгляд этот код может создать иллюзию большего сохранения типов, поскольку он избегает броска вниз в шестой строке. Но выполнение следующего фрагмента возвращает нас к земле, поскольку мы все еще сталкиваемся ClassCastException
сценарием ClassCastException
во время назначения в строке десять:
01
02
03
04
05
06
07
08
09
10
|
Context context = new Context(); Runnable runnable = ... context.put( "key" , runnable, Runnable. class ); // several computation cycles later... Executor executor = ... context.put( "key" , executor, Executor. class ); // even more computation cycles later... Runnable value = context.get( "key" , Runnable. class ); // runtime problem |
Так что пошло не так?
Прежде всего, приведение к Context#get
в Context#get
типа T
неэффективно, поскольку стирание типа заменяет неограниченные параметры статическим приведением к Object
. Но что более важно, реализация не использует информацию о типе, предоставляемую Context#put
качестве ключа. Максимум это служит лишним косметическим эффектом.
Typesafe гетерогенный контейнер
Хотя последний вариант Context
работал не очень хорошо, он указывает в правильном направлении. Вопрос в том, как правильно параметризовать ключ? Чтобы ответить на этот вопрос, взгляните на урезанную реализацию в соответствии с типом безопасного гетерогенного контейнера, описанного Блохом.
Идея состоит в том, чтобы использовать тип class
качестве самого ключа. Поскольку Class
является параметризованным типом, он позволяет нам сделать методы типа Context
безопасными, не прибегая к непроверенному приведению к T
Объект Class
используемый таким образом, называется токеном типа.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class Context { private final Map<Class<?>, Object> values = new HashMap<>(); public <T> void put( Class<T> key, T value ) { values.put( key, value ); } public <T> T get( Class<T> key ) { return key.cast( values.get( key ) ); } [...] } |
Обратите внимание на то, как преобразование вниз в реализации Context#get
было заменено эффективным динамическим вариантом. И вот как контекст может использоваться клиентами:
01
02
03
04
05
06
07
08
09
10
|
Context context = new Context(); Runnable runnable ... context.put( Runnable. class , runnable ); // several computation cycles later... Executor executor = ... context.put( Executor. class , executor ); // even more computation cycles later... Runnable value = context.get( Runnable. class ); |
На этот раз клиентский код будет работать без проблем приведения классов, поскольку невозможно заменить определенную пару ключ-значение на другую с другим типом значения.
Там, где есть свет, должна быть тень, там, где есть тень, должен быть свет. Нет тени без света и нет света без тени …
Блох упоминает два ограничения этого паттерна. «Во-первых, злонамеренный клиент может легко повредить безопасность типов […], используя объект класса в его сыром виде». Чтобы обеспечить инвариант типа во время выполнения, в Context#put
можно использовать динамическое приведение.
1
2
3
|
public <T> void put( Class<T> key, T value ) { values.put( key, key.cast( value ) ); } |
Второе ограничение заключается в том, что шаблон нельзя использовать для типов без переопределения (см. Пункт 25, Эффективная Java). Это означает, что вы можете хранить типы значений, такие как Runnable
или Runnable[]
но не List<Runnable>
безопасным для типов способом.
Это связано с тем, что для List<Runnable>
нет определенного объекта класса. Все параметризованные типы ссылаются на один и тот же объект List.class
. Следовательно, Блох указывает на то, что не существует удовлетворительного обходного пути для такого рода ограничений.
Но что, если вам нужно хранить две записи одного и того же типа значения? Хотя создание новых расширений типов только для целей хранения в безопасном контейнере может быть вполне приемлемым, это не является лучшим решением для проектирования. Использование пользовательской реализации ключа может быть лучшим подходом.
Несколько записей контейнера одного типа
Чтобы иметь возможность хранить несколько записей контейнера одного и того же типа, мы могли бы изменить класс Context
чтобы использовать пользовательский ключ. Такой ключ должен предоставлять информацию о типе, которая нам нужна для безопасного поведения типа, и идентификатор для различения объектов фактических значений.
Реализация простого ключа с использованием экземпляра String
качестве идентификатора может выглядеть следующим образом:
01
02
03
04
05
06
07
08
09
10
|
public class Key<T> { final String identifier; final Class<T> type; public Key( String identifier, Class<T> type ) { this .identifier = identifier; this .type = type; } } |
Снова мы используем параметризованный Class
как крючок для информации о типе. И настроенный Context
теперь использует параметризованный Key
вместо Class
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class Context { private final Map<Key<?>, Object> values = new HashMap<>(); public <T> void put( Key<T> key, T value ) { values.put( key, value ); } public <T> T get( Key<T> key ) { return key.type.cast( values.get( key ) ); } [...] } |
Клиент будет использовать эту версию Context
следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
Context context = new Context(); Runnable runnable1 = ... Key<Runnable> key1 = new Key<>( "id1" , Runnable. class ); context.put( key1, runnable1 ); Runnable runnable2 = ... Key<Runnable> key2 = new Key<>( "id2" , Runnable. class ); context.put( key2, runnable2 ); // several computation cycles later... Runnable actual = context.get( key1 ); assertThat( actual ).isSameAs( runnable1 ); |
Хотя этот фрагмент работает, реализация все еще имеет недостатки. Реализация Key
используется в качестве параметра поиска в Context#get
. Использование двух разных экземпляров Key
инициализированных с одним и тем же идентификатором и классом — один экземпляр, используемый с put, а другой — с get — вернет null
при get
. Что не то, что мы хотим.
К счастью, это может быть легко решено с помощью соответствующей реализации equals
и hashCode
Key
. Это позволяет поиску HashMap
работать как положено. Наконец, можно создать фабричный метод для создания ключа, чтобы минимизировать шаблон (полезно в сочетании со статическим импортом):
1
2
3
|
public static Key key( String identifier, Class type ) { return new Key( identifier, type ); } |
Вывод
«Обычное использование обобщений, примером которых являются API-интерфейсы сбора, ограничивает вас фиксированным числом параметров типа на контейнер. Вы можете обойти это ограничение, поместив параметр типа на ключ, а не на контейнер. Вы можете использовать объекты Class
качестве ключей для таких безопасных разнородных контейнеров »(Joshua Bloch, Item 29, Effective Java).
Принимая во внимание эти заключительные замечания, больше нечего добавить, кроме как пожелать вам удачи в смешивании яблок и груш…
Ссылка: | Как сопоставить различные типы значений с использованием обобщенных данных Java от нашего партнера по JCG Рудигера Херрманна в блоге Code Affine . |