Java generics map different value types in detail and example code

  • 2020-06-12 08:57:31
  • OfStack

Detail the different value types of Java generic mappings

Preface:

In general, developers occasionally encounter situations where any type of value is mapped in a particular container. However, the Java collection API only provides parameterized containers. This limits the safe use of HashMap for types such as single 1 value types. But what if you want to mix apples and pears?

Fortunately, there is a simple design pattern that allows you to map different value types using Java generics, which Joshua Bloch describes in its Effective Java (2nd edition, item 29) as a type-safe heterogeneous container (typesafe hetereogeneous Container).

On this topic, I have encountered some inappropriate solutions recently. It gave me the idea of explaining the problem domain in this article and expounding on some implementation details.

Use Java generics to map different value types

Consider an example where you need to provide the context of an application that can bind a particular key to any type of value. With HashMap as the key, a simple, non-type-safe (type safe) implementation 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 code snippet shows how to use Context in a program:


Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable );

// several computation cycles later...
Runnable value = ( Runnable )context.get( "key" );

As you can see, the downside of this approach is that a downward transformation is required at line 6 (down cast). If you replace the type of the median value of the key-value pair, you will obviously throw an ClassCastException exception:


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 this problem is difficult to trace, because the implementation steps may already be widely distributed throughout your program.

To improve the situation, it seems reasonable to bind value to both its key and its value.

The common errors I have seen in various solutions following this approach are more or less due to variations of Context as follows:


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 );
 }

 [...]
}

The same basic usage might go something 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 );

At first glance, this code might give you the illusion of more type-safety because it avoids the downward transition at line 6 (down cast). But running the following code will bring us back to reality, because we'll still fall into the arms of ClassCastException at line 10 of the assignment statement:


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

What went wrong?

First, downward transitions in Context#get are invalid because type erasors use statically transformed Object instead of unbounded parameters (unbonded parameters). More importantly, the implementation does not use the type information provided by Context#put at all. At best, this is just one more step of beauty.

Type-safe heterogeneous containers

While the above variant of Context does not work, it does point the way. The next question is: How do you parameterize the key reasonably? To answer this question, let's look at a simplified implementation of the type-safe heterogeneous container pattern described by Bloch (typesafe heterogenous container pattern).

The idea is to use key's own class type as key. Because Class is a parameterized type, it ensures that we make the Context method type-safe without resorting to an unchecked cast to T. An Class object in this form is called a type token (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 ) );
 }

 [...]
}

Notice how in the implementation of Context#get the downward transformation is replaced with a valid dynamic variable. The client can use this context as follows:


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 conversions, because it is not possible to exchange a key-value pair with a different value type.

Where there is light, there must be shadow, and where there is shadow, there must be light. There is no light without shadow, and there is no shadow without light. Haruki murakami
The Bloch points out that this model has two limitations. "First, malicious clients can easily break type safety by using class objects in their native form (raw form)." Dynamic conversions (dynamic cast) are used in Context#put to ensure type safety at run time.


public <T> void put( Class<T> key, T value ) {
 values.put( key, key.cast( value ) );
}

The second limitation is that it cannot be used in types that are not reifiable (ES117en-ES118en) (see Item 25 of Effective Java). In other words, you can save Runnable or Runnable[], but not List < Runnable > .

That's because of List < Runnable > There is no specific class object; all parameterized types refer to the same ES134en.class object. Therefore, Bloch points out that there is no satisfactory solution to this limitation.

But what if you need to store two entries of the same value type? If you just want to store a type-safe container, you might consider creating a new type extension, but this is clearly not the best design. Using a custom Key might be a better solution.

Multiple container entries of the same type

To be able to store multiple container entries of the same type, we can change the Context class with custom key. This key must provide the type information we need for type safety, as well as the identity that distinguishes the different value objects (value objects). A naive implementation of key identified by an instance of String 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 the hook for the type information, and the adjusted Context will use the parameterized Key instead of Class.


Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable );

// several computation cycles later...
Runnable value = ( Runnable )context.get( "key" );

0

The client will use this version of Context as follows:


Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable );

// several computation cycles later...
Runnable value = ( Runnable )context.get( "key" );

1

While this code snippet is available, it is still flawed. In Context#get, Key is used as a query parameter. Initialize two different instances of Key with the same identifier and class, one for put and one for get, and finally the get operation returns null. That's not what we want...


Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable );

// several computation cycles later...
Runnable value = ( Runnable )context.get( "key" );

2

Fortunately, designing the right equals and hashCode for Key can easily solve this problem and make the HashMap lookup work as expected. Finally, you can provide a factory method for creating key to simplify the creation process (useful with static import1) :


public static Key key( String identifier, Class type ) {
 return new Key( identifier, type );
}

conclusion

"Collection API illustrates the general use of generics, limiting you to a fixed number of type parameters per container. You can get around this restriction by putting type parameters on keys instead of containers. For this type of safe heterogeneous container, you can use the Class counterpart as the key. (Joshua Bloch, Item 29 of Effective Java).

Given the above closing statement, there is nothing to add, except to wish you success in mixing apples and pears...

Thank you for reading, I hope to help you, thank you for your support to this site!


Related articles: