Using TypeTokens to retrieve generic parameters
From JQuantLib
Using techniques exposed in this article you will see how we can retrieve actual generic parameters passed during instantiation of your classes. This can be helpful when you would like to provide additional adaptability to service classes. Contrary to what is generally accepted 'type erasure' can be avoided, given certain circumstances are met. Once type information still remains in the bytecode, you can traverse type information and retrieve all actual generic parameters, even generic parameters of generic parameters. Richard Gomes
Contents |
Overview
Certain developers specially those writing libraries or service classes, are interested on adapting the behavior of their classes depending on the purpose these classes are being used. A good example is a class which populates a list which aims to be as fast as possible: when this class is called to populate a list of numbers, it could use an array of primitive types instead of a list of Objects. In other words, depending on the type of the actual generic parameters being passed, our service class would ideally adapt itself for maximum performance. So,
MyList<Something>()
would be backed by a List of Objects whilst
MyList<Double>()
would be backed by a double[] array because this is the most economical and fastest way to store such kind of information.
In order to do so, our class MyList needs to be able to retrieve Something and Double. These identifiers were specified at compile time but somehow we need to be retrieved them at runtime.
How type erasure can be avoided
(to be done)
How type information can be obtained
(to be done)
Proof of concept
Suppose we are instantiatin our test class like this:
Object o = new MyClass<
HashMap<String,Double>, // 1st generic parameter
TreeMap<String, LinkedList<List<Double>>>, // 2nd generic parameter
List<Integer> // 3rd generic parameter
>( ) // whatever arguments you need
{ }; // anonymous class
The proof of concept below demonstrates how actual generic parameters can be retrieved at runtime.
/**
* This class demonstrates how generic parameters can be retrieved from the caller statement.
* <p>
* The 'magic' starts by calling {@link Class#getGenericSuperclass()} which returns a {@link Type} descriptor.
* This type descriptor is the starting point for obtaining all actual generic types passed during the call.
* It's important to observe that <i>actual</i> generic types are retrieved, not <i>declared</i> generic parameters.
* <p>
* In order to work, <i>type erasure</i> must be avoided so that type information can be retrieved
* at runtime. This can be done by extending a class which retrieves type information, i.e: type information
* can be retrieved <i>by</i> an abstract class <i>from</i> some extended class from it. This is inconvenient
* in general because oblige us to artificially define an unneeded object model just because we are willing
* to retrieve some type information.
* <p>
* A more convenient approach is simply a class which retrieves type information from its extended anonymous class.
* This is the approach we use in this example
*
* @author Richard Gomes
*/
public class TypeTokenRecursiveTest {
private final static Logger logger = LoggerFactory.getLogger(TypeTokenRecursiveTest.class);
public TypeTokenRecursiveTest() {
TypeTokenRecursiveTest.logger.info("\n\n::::: "+this.getClass().getSimpleName()+" :::::");
}
@Test
public void MyClassTest() {
Object o = new MyClass<HashMap<String,Double>,
TreeMap<String, LinkedList<List<Double>>>,
List<Integer>>() {};
}
private class MyClass<X, Y, Z> {
public MyClass() {
Type superclass = this.getClass().getGenericSuperclass();
if (superclass instanceof Class) {
throw new IllegalArgumentException(
"Class should be anonymous or extended from a generic class");
}
for (Type t : ((ParameterizedType) superclass).getActualTypeArguments() ) {
printType("", t);
}
}
private void printType(String indent, Type t) {
if (t instanceof Class<?>) {
System.out.println(indent+ ((Class) t).getSimpleName());
} else {
if (t instanceof ParameterizedType) {
Type rawType = ((ParameterizedType) t).getRawType();
printType(indent, rawType);
for (Type arg : ((ParameterizedType) t).getActualTypeArguments()) {
printType(indent+" ", arg);
}
} else {
System.out.println("? "+t.toString());
}
}
}
}
}
it prints
::::: TypeTokenRecursiveTest :::::
HashMap
String
Double
TreeMap
String
LinkedList
List
Double
List
Integer
Support classes
Once we proved it works, it's time to create some support classes.
The first support class is a node which is intended to hold a Class information. A node also has a data structure intended to hold all its children. Doing so, a node is a basic data structure we need to have a tree of type information.
The second support class we need is a helper class which retrieves all actual generic parameters and build a tree made of nodes as we already described.
Putting it all together
Below you can see a test class which demonstrates how our support classes can be used:
package org.jquantlib.testsuite.lang;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import org.jquantlib.lang.reflect.TypeNode;
import org.jquantlib.lang.reflect.TypeTokenTree;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TypeTokenTreeTest {
private final static Logger logger = LoggerFactory.getLogger(TypeTokenTreeTest.class);
private final MyClass testClass;
public TypeTokenTreeTest() {
logger.info("\n\n::::: "+this.getClass().getSimpleName()+" :::::");
// notice the usage of an anonymous class denoted by "{ }"
this.testClass = new MyClass<HashMap<String, Double>, TreeMap<String, LinkedList<List<Double>>>, List<Integer>>() { };
}
@Test
public void testFirstGenericParameter() {
TypeNode node = testClass.get(0);
assertTrue("First generic parameter should be a HashMap", node.getElement().isAssignableFrom(HashMap.class));
TypeNode subnode;
subnode = testClass.get(node, 0);
assertTrue("Inner first generic parameter should be a String", subnode.getElement().isAssignableFrom(String.class));
subnode = testClass.get(node, 1);
assertTrue("Inner second generic parameter should be a Double", subnode.getElement().isAssignableFrom(Double.class));
}
@Test
public void testSecondGenericParameter() {
TypeNode node = testClass.get(1);
assertTrue("First generic parameter should be a TreeMap", node.getElement().isAssignableFrom(TreeMap.class));
TypeNode subnode;
subnode = testClass.get(node, 0);
assertTrue("Inner first generic parameter should be a String", subnode.getElement().isAssignableFrom(String.class));
subnode = testClass.get(node, 1);
assertTrue("Inner second generic parameter should be a LinkedList", subnode.getElement().isAssignableFrom(LinkedList.class));
subnode = testClass.get(subnode, 0);
assertTrue("Inner generic parameter should be a List", subnode.getElement().isAssignableFrom(List.class));
subnode = testClass.get(subnode, 0);
assertTrue("Inner generic parameter should be a Double", subnode.getElement().isAssignableFrom(Double.class));
}
@Test
public void testThirdGenericParameter() {
TypeNode node = testClass.get(2);
assertTrue("First generic parameter should be a List", node.getElement().isAssignableFrom(List.class));
TypeNode subnode;
subnode = testClass.get(node, 0);
assertTrue("Inner first generic parameter should be a Integer", subnode.getElement().isAssignableFrom(Integer.class));
}
private class MyClass<X, Y, Z> {
private final TypeNode root;
public MyClass() {
this.root = new TypeTokenTree(this.getClass()).getRoot();
}
public TypeNode get(int index) {
return root.get(index);
}
public TypeNode get(final TypeNode node, int index) {
return node.get(index);
}
}
}
Use case
TimeSeries is a class which accepts a Double or a IntervalPrice as generic parameter.
- When a Double is passed, TimeSeries delegates to a private inner class TimeSeriesDouble which uses an array of primitive types and avoid boxing/unboxing in order to speed up performance;
- When an IntervalPrice is passed, TimeSeries delegates to a private inner class TimeSeriesIntervalPrice which uses an ArrayList as usual.
public class TimeSeries<T> {
private final Series delegate;
public TimeSeries() {
final Class<?> klass = new TypeTokenTree(this.getClass()).getRoot().get(0).getElement();
if (Double.class.isAssignableFrom(klass)) {
this.delegate = new TimeSeriesDouble();
} else if (IntervalPrice.class.isAssignableFrom(klass)) {
this.delegate = new TimeSeriesIntervalPrice();
} else {
throw new UnsupportedOperationException("only Double and IntervalPrice are supported");
}
}
// blah blah blah
}
Doing this way, we can adjust the behavior of our classes depending on actual generic parameter passed by the caller.
Calling TimeSeries in order to store double values ordered by date
final TimeSeries<Double> retValue = new TimeSeries<Double>() { /* anonymous */ };
for (int i=0; i< dsize; i++)
retValue.add (dates[i], values[i]);
return retValue;
Calling TimeSeries intented to store OHLC values (Open, High, Low, High)
final TimeSeries<IntervalPrice> retval = new TimeSeries<IntervalPrice>() { /* anonymous */ };
for (int i=0; i< dsize; i++)
retval.add(d[i], new IntervalPrice(open[i], close[i], high[i], low[i]));
return retval;
See also
Support classes
Example of usage in a real-world situation
References
Richard Gomes 13:14, 3 November 2009 (UTC)

