【Java 基础】Java Map中的Value值如何做到可以为任意类型的值
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 using String 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 the ClassCastException 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.
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.
具体如何写hashCode和equals参见前一篇 Hash Map
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…
Reference
【Java 基础】Java Map中的Value值如何做到可以为任意类型的值的更多相关文章
- (转载)Java Map中的Value值如何做到可以为任意类型的值
转载地址:http://www.importnew.com/15556.html 如有侵权,请联系作者及时删除. 搬到我的博客来,有空细细品味,把玩. 本文由 ImportNew - shut ...
- Java基础-Java中23种设计模式之常用的设计模式
Java基础-Java中23种设计模式之常用的设计模式 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.设计模式分类 设计模式是针对特定场景给出的专家级的解决方案.总的来说设 ...
- Java基础-JAVA中常见的数据结构介绍
Java基础-JAVA中常见的数据结构介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.什么是数据结构 答:数据结构是指数据存储的组织方式.大致上分为线性表.栈(Stack) ...
- Java基础关于Map(字典)的方法使用
Java基础关于Map(字典)的方法使用 java中一般用map与hashmap来创建一个key-value对象 使用前提是要导入方法包: import java.util.HashMap: impo ...
- java基础(20):Map、可变参数、Collections
1. Map接口 1.1 Map接口概述 我们通过查看Map接口描述,发现Map接口下的集合与Collection接口下的集合,它们存储数据的形式不同,如下图. Collection中的集合,元素是孤 ...
- 怎么在java 8的map中使用stream
怎么在java 8的map中使用stream 简介 Map是java中非常常用的一个集合类型,我们通常也需要去遍历Map去获取某些值,java 8引入了Stream的概念,那么我们怎么在Map中使用S ...
- JAVA基础篇NO2--Java中的基本命名规则及数据类型
1.Java中的常量及进制 1.常量: 在程序运行的过程中,不可以改变的量,就是常量 boolean类型的值只能是true或者false null: 空常量, 代表不存在! ------------- ...
- java基础---->java中正则表达式二
跟正则表达式相关的类有:Pattern.Matcher和String.今天我们就开始Java中正则表达式的学习. Pattern和Matcher的理解 一.正则表达式的使用方法 一般推荐使用的方式如下 ...
- Java基础-Java中的堆内存和离堆内存机制
Java基础-Java中的堆内存和离堆内存机制 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.
随机推荐
- 输入指令npx webpack-dev-server报错:Error: Cannot find module ‘webpack-cli/bin/config-yargs‘的解决方法
输入指令npx webpack-dev-server报错:Error: Cannot find module 'webpack-cli/bin/config-yargs'的解决方法 输入指令:npx ...
- java解析Excel日期格式转换问题
Excel上传导入,Excel里面单元格是日期的会解析出来数字,比如2020-07-11会解析为44023解决方法一: Excel单元格格式设置为文本格式.解决方法二: 使用代码处理,把解析出来的44 ...
- ECharts 点击事件
一个问题 ECharts 点击出现多个弹窗
- java开发环境搭建,配置
java开发环境搭建 下载jdk8的地址 是oracle的 安装JDK 下载电脑对应的版本 双击安装JDK 记住安装路径 配置环境变量 我的电脑/此电脑 右键属性 高级设置 环境变量 点击新建 变量名 ...
- [loj3032]馕
(直接贪心会导致分子和分母过大) 令$S_{i}=\sum_{j=1}^{L}V_{i,j}$(即其独吞整个馕的快乐度),对第$i$个人求出$n$个位置$x_{1},x_{2},...,x_{n-1} ...
- [atARC078F]Mole and Abandoned Mine
注意到最终图的样子可以看作一条从1到$n$的路径,以及删去这条路径上的边后,路径上的每一个点所对应的一个连通块 考虑dp,令$f_{S,i}$表示当前1到$n$路径上的最后一个点以及之前点(包括$i$ ...
- 第一章 初始C语言
第一章 初始C语言 目录 第一章 初始C语言 1. C语言起源 2. 选择C语言的理由 2.1 设计特性 2.2 高效性 2.3 可移植性 2.4 强大而灵活 2.5 面向程序员 3. C语言的应用范 ...
- Redis | 第4章 Redis中的数据库《Redis设计与实现》
目录 前言 1. Redis中的数据库 2. 数据库的键空间 3. 键的生成时间与过期时间 4. Redis中的过期键删除策略 5. AOF.RDB和复制功能对过期键的处理 5.1 生成 RDB 文件 ...
- 数值最优化:一阶和二阶优化算法(Pytorch实现)
1 最优化概论 (1) 最优化的目标 最优化问题指的是找出实数函数的极大值或极小值,该函数称为目标函数.由于定位\(f(x)\)的极大值与找出\(-f(x)\)的极小值等价,在推导计算方式时仅考虑最小 ...
- AI剪辑和自定义UI,打造更智能的剪辑体验
为满足开发者构建高效的应用内视频编辑能力,7月的HMS Core 6.0 推出了视频编辑服务(Video Editor Kit),一站式的视频处理能力获得了积极反响.同时,我们也关注到开发者需要集成丰 ...