Using TypeTokens to retrieve generic parameters

From JQuantLib

Jump to: navigation, search

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)