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…

时间: 2024-10-30 02:12:57

How to Map Distinct Value Types Using Java Generics--reference的相关文章

Error getting nested result map values for &#39;user_inf&#39;. Cause: java.sql.SQLException: Cannot convert

问题?Error getting nested result map values for 'user_inf'.  Cause: java.sql.SQLException: Cannot convert value '0000-00-00 00:00:00' from column 8 to TIMESTAMP. 错误:org.apache.ibatis.exceptions.PersistenceException: ### Error querying database.  Cause:

细述 Java垃圾回收机制→Types of Java Garbage Collectors

细述 Java垃圾回收机制→Types of Java Garbage Collectors 转自:https://segmentfault.com/a/1190000006214497 本文非原创,翻译自Types of Java Garbage Collectors在Java中为对象分配和释放内存空间都是由垃圾回收线程自动执行完成的.和C语言不一样的是Java程序员不需要手动写垃圾回收相关的代码.这是使得Java如此流行,同时也是Java能帮助程序员写出更好的Java应用的优点之一. 本文将

Java Interview Reference Guide--reference

Part 1 http://techmytalk.com/2014/01/24/java-interview-reference-guide-part-1/ Posted on January 24, 2014 by Nitin Kumar JAVA Object Oriented Concepts Java in based on Object Oriented concepts, which permits higher level of abstraction to solve any p

java的Reference学习

java中Reference学习 谈到Reference想到了什么 Reference提供了一种与jvm gc交互的一种方式,提到Reference,脑中应该浮现一些名词,gc.ReferenceQueue.SoftReference.WeakReference.PhantomReference.FinalReference以及最常见的强引用.我认为当一个小白开始学习Reference的时候应该想一个问题,强引用不够好么,为什么我还需要弱引用.软引用等等这些东西. 为什么我们需要它们 应用程序经

java1.8 新特性(五 如何使用filter,limit ,skip ,distinct map flatmap ,collect 操作 java集合)

使用filter 根据 条件筛选 出结果:例如 找出 user 中 age >=15 的用户 package lambda.stream; /** * @author 作者:cb * @version 创建时间:2019年1月4日 下午2:35:05 */ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; publi

Data Types in Java

In Java, Data types are classified into two categories: 1. Primitive Data Type 2. Non-Primitive Data Type there are 8 primitive data types: char boolean byte short int  long  float double 8 primitive can be classfiied into 4 groups; group 1: Integer

Java常用的数据结构与类型(Map,数组,泛型等等)——Java篇

今天做着做着项目,突发奇想,决定把自己常用的Java数据类型整理一下(Java高手和基础好的网友别再看下去了,跪求勿喷),都是很基础很简单的东西,但是笔者觉得很有必要,好记性不如烂笔头,自己为了方便以后的开发吧,Java高手直接无视这篇博客吧,仅给有需要的人看,未完待续! List<String> counList=new ArrayList<String>();//定义数组链表 String country[]=coun.split(",");//定义数组,分

jQuery——map()函数以及它的java实现

map()函数小简介 map()函数一直都是我认为比较实用的函数之一,为什么这么说呢? 先来考虑一下,你是否碰到过以下场景:需要遍历一组对象取出每个对象的某个属性(比如id)并且用分隔符隔开 我想你一定有碰到过吧!因为我已经碰到过很多次了,这就是map()函数的一个典型的适用场景 不仅在前台,后台也是(所以后面我在java中也简单实现了这么一个方法),下面就看看吧 map()函数实例 在jQuery中,如.each()一样,.map()函数也有两种形式:一种是提供给jQuery对象用的.map(

[Quote] Java - Generics

From http://www.tutorialspoint.com/java/java_generics.htm It would be nice if we could write a single sort method that could sort the elements in an Integer array, a String array or an array of any type that supports ordering. Java Generic methods an