How to Map Distinct Value Types Using Java Generics--reference
原文:http://www.codeaffine.com/2015/03/04/map-distinct-value-types-using-java-generics/
Occasionally the average developer runs into a situation where he has to map values of arbitrary types within a particular container. However the Java collection API provides container related parameterization only. Which limits the type safe usage of HashMap
for example to a single value type. But what if you want to mix apples and pears?
Luckily there is an easy design pattern that allows to map distinct value types using Java generics, which Joshua Bloch has described as typesafe hetereogeneous container in his book Effective Java(second edition, Item 29).
Stumbling across some not altogether congenial solutions regarding this topic recently, gave me the idea to explain the problem domain and elaborate on some implementation aspects in this post.
Map Distinct Value Types Using Java Generics
Consider for the sake of example that you have to provide some kind of application context that allows to bind values of arbitrary types to certain keys. A simple non type safe implementation usingString
keys backed by a HashMap
might look like this:
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 );
} [...]
}
The following snippet shows how this Context
can be used in a program:
Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable ); // several computation cycles later...
Runnable value = ( Runnable )context.get( "key" );
The drawback of this approach can be seen at line six where a down cast is needed. Obviously this can lead to a ClassCastException
in case the key-value pair has been replaced by a different value type:
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
The cause of such problems can be difficult to trace as the related implementation steps might be spread wide apart in your application. To improve the situation it seems reasonable to bind the value not only to its key but also to its type.
Common mistakes I saw in several solutions following this approach boil down more or less to the following Context
variant:
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 );
} [...]
}
Again basic usage might look like this:
Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable, Runnable.class ); // several computation cycles later...
Runnable value = context.get( "key", Runnable.class );
One first glance this code might give the illusion of being more type save as it avoids the down cast in line six. But running the following snippet gets us down to earth as we still run into theClassCastException
scenario during the assignment in line ten:
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
So what went wrong?
First of all the down cast in Context#get
of type T
is ineffective as type erasure replaces unbounded parameters with a static cast to Object
. But more important the implementation does not use the type information provided by Context#put
as key. At most it serves as superfluous cosmetic effect.
Typesafe Hetereogeneous Container
Although the last Context
variant did not work out very well it points into the right direction. The question is how to properly parameterize the key? To answer this take a look at a stripped-down implementation according to the typesafe hetereogenous container pattern described by Bloch.
The idea is to use the class
type as key itself. Since Class
is a parameterized type it enables us to make the methods of Context
type safe without resorting to an unchecked cast to T
. A Class
object used in this fashion is called a type token.
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 ) );
} [...]
}
Note how the down cast within the Context#get
implementation has been replaced with an effective dynamic variant. And this is how the context can be used by clients:
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 );
This time the client code will work without class cast problems, as it is impossible to exchange a certain key-value pair by one with a different value type.
Where there is light, there must be shadow, where there is shadow there must be light. There is no shadow without light and no light without shadow….Haruki Murakami
Bloch mentions two limitations to this pattern. ‘First, a malicious client could easily corrupt the type safety [...] by using a class object in its raw form.’ To ensure the type invariant at runtime a dynamic cast can be used within Context#put
.
public <T> void put( Class<T> key, T value ) {
values.put( key, key.cast( value ) );
}
The second limitation is that the pattern cannot be used on non-reifiable types (see Item 25, Effective Java). Which means you can store value types like Runnable
or Runnable[]
but not List<Runnable>
in a type safe manner.
This is because there is no particular class object for List<Runnable>
. All parameterized types refer to the same List.class
object. Hence Bloch points out that there is no satisfactory workaround for this kind of limitation.
But what if you need to store two entries of the same value type? While creating new type extensions just for storage purpose into the type safe container might be imaginable, it does not sound as the best design decision. Using a custom key implementation might be a better approach.
Multiple Container Entries of the Same Type
To be able to store multiple container entries of the same type we could change the Context
class to use a custom key. Such a key has to provide the type information we need for the type safe behaviour and an identifier for distinction of the actual value objects.
A naive key implementation using a String
instance as identifier might look like this:
public class Key<T> { final String identifier;
final Class<T> type; public Key( String identifier, Class<T> type ) {
this.identifier = identifier;
this.type = type;
}
}
Again we use the parameterized Class
as hook to the type information. And the adjusted Context
now uses the parameterized Key
instead of Class
:
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 ) );
} [...]
}
A client would use this version of Context
like this:
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 );
Although this snippet works, the implementation is still flawed. The Key
implementation is used as lookup parameter in Context#get
. Using two distinct instances of Key
initialized with the same identifier and class – one instance used with put and the other used with get – would return null
on get
. Which is not what we want.
Luckily this can be solved easily with an appropriate equals
and hashCode
implementation of Key
. That allows the HashMap
lookup to work as expected. Finally one might provide a factory method for key creation to minimize boilerplate (useful in combination with static imports):
public static Key key( String identifier, Class type ) {
return new Key( identifier, type );
}
Conclusion
‘The normal use of generics, exemplified by the collection APIs, restricts you to a fixed number of type parameters per container. You can get around this restriction by placing the type parameter on the key rather than the container. You can use Class
objects as keys for such typesafe heterogeneous containers’ (Joshua Bloch, Item 29, Effective Java).
Given these closing remarks, there is nothing left to be added except for wishing you good luck mixing apples and pears successfully…
How to Map Distinct Value Types Using Java Generics--reference的更多相关文章
- Implement Hash Map Using Primitive Types
A small coding test that I encountered today. Question Using only primitive types, implement a fixed ...
- Error getting nested result map values for 'company'. Cause: java.sql.SQLException: Invalid value for getInt() - 'NFHK188'
我今天遇到一个我不解的问题,是mybatis多对一关系查询出问题了,但是我自己还是解决了,在网上也查过那个错误,可是找不到我想要的.不知道你们遇到过没有,我接下来分享给大家.希望我这个第一篇博客能帮助 ...
- at org.codehaus.jackson.map.ser.BeanSerializer.serialize(BeanSerializer.java:142) :json转化“$ref 循环引用”的问题
原因: entity实体中存在@OneToMany,@ManyToOne注解,在转化json是产生了循环引用 报的错误 解决方法: springmvc @ResponseBody 默认的json转化用 ...
- An internal error occurred during: "Map/Reduce location status updater". java.lang.NullPointerException
eclipse配置hadoop 2.6 服务器做的虚拟机,因为window是的hadoop会出现意想不到的错误,因为,我做了ubuntu的虚拟机供我使用 在虚拟机中进行映射设置 在eclipse中dr ...
- 细述 Java垃圾回收机制→Types of Java Garbage Collectors
细述 Java垃圾回收机制→Types of Java Garbage Collectors 转自:https://segmentfault.com/a/1190000006214497 本文非原创, ...
- thinking in java Generics Latent typing
The beginning of this chapter introduced the idea of writing code that can be applied as generally a ...
- Java Interview Reference Guide--reference
Part 1 http://techmytalk.com/2014/01/24/java-interview-reference-guide-part-1/ Posted on January 24, ...
- Thinking in java——Generics
Ordinary classes and methods work with specific types: either primitives or class types. If you are ...
- Java的Reference感觉很象C++的指针,但是区别是本质的
Java的Reference感觉很象C++的指针,但是区别是本质的 他们相同之处在于都是含有一个地址,但是在Java中你无法对这个地址进行任何数学运算,并且这个地址你不知道,是Java Runtime ...
随机推荐
- 设计模式之Memento(备忘机制)
Memento备望录模式定义:memento是一个保存另外一个对象内部状态拷贝的对象.这样以后就可以将该对象恢复到原先保存的状态. Memento模式相对也比较好理解,我们看下列代码: public ...
- 【Python】使用python的tornado配合html页面示例
背景:java写的非标加密算法,测试时执行java工程进行解密测试,很不方便. 目的:想写个web页面,使得任何测试人员都可以在输入加密串时得到解密后字段,方便日志查询及字段核对.(额,算法部分就不写 ...
- POJ 1904 King's Quest 强连通分量+二分图增广判定
http://www.cnblogs.com/zxndgv/archive/2011/08/06/2129333.html 这位神说的很好 #include <iostream> #inc ...
- java继承和多态
父类和子类 如果类C1扩展自另一个类C2,那么C1称为子类或派生类,C2称为父类或基类.派生类可以从它的基类中继承可访问的数据域和方法,还可添加新数据域和新方法 例如:实现一个几何图形基类; clas ...
- Visual Studio配置OpenCV设置全局的继承属性
1.安装完毕OpenCV后,新建一个CLR空项目,将其取名为"SetingGlobalOpenCVDir"便于以后变更版本时修改.如下图所示: 2.点击"视图->其 ...
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
ActivityA到ActivityBintent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);//ActivityB不加入后退栈android:noHisto ...
- A Tour of Go Methods with pointer receivers
Methods can be associated with a named type or a pointer to a named type. We just saw two Abs method ...
- Java & XML Tool Overview
As mentioned in the introduction Sun now provides these tools for XML Processing in Java: StAX Reade ...
- UITableView性能优化
关于UITableView的性能优化,网络上也有一些总结.在这里就介绍下我们项目中遇到的问题以及对应的解决方法.相信我们遇到的问题也有一定的普适性,能够作为其他问题的优化方案. Instruments ...
- SignalTap II逻辑分析仪的使用
一.例子 我们使用如图1所示的verilog代码所实现的开关电路作为例子.这个电路把DE系列开发板上的前8个开关简单的和对应的8个红色LED相连接.它是这样工作的:在时钟(CLOCK_50)的上升沿读 ...