Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check generic type before casting at runtime

We were given the task to create an object structure for evaluating expressions (e.g. "1 + 2" or "true & false"). The parser is provided, as well as a factory interface to create the expression objects.

We use generics for the different expressions: Expression<Integer> returns an Integer, Expression<Boolean> returns a Boolean, etc.

The problem we're facing is that the provided interface uses the raw type Expression, for example:

public Expression createSumExpression(Expression left, Expression right);

The sum expression is only defined when the operands are of type Expression<Integer> or Expression<Double>. How can we check this? Due to type erasure, the type information is not available at run time, and it just results in a ClassCastException if it's incorrect.

I can modify everything, except for the createSumExpression and createAndExpression functions.


This is a simplified piece of code that demonstrates the problem. It works, but it doesn't look very nice, and it gives multiple warnings.

Main.java

public class Main {

    public static void main(String[] args) {
        Expression<?> left1 = new BasicExpression<>(42);
        Expression<?> right1 = new BasicExpression<>(3);        
        Expression<?> sum = createSumExpression(left1, right1);
        System.out.printf("%d + %d = %d%n",left1.getValue(), right1.getValue(), sum.getValue());

        Expression<?> left2 = new BasicExpression<>(true);
        Expression<?> right2 = new BasicExpression<>(false);
        Expression<?> and = createAndExpression(left2, right2);
        System.out.printf("%b & %b = %b%n",left2.getValue(), right2.getValue(), and.getValue());
    }

    private static Expression createSumExpression(Expression left, Expression right) { // Raw types because of given interface
        return new BinaryExpression<Integer,Expression<Integer>,Expression<Integer>>(left, right) {
            @Override
            protected Integer operation(Expression<Integer> left, Expression<Integer> right) {
                return left.getValue() + right.getValue();
            }
        };
    }

    private static Expression createAndExpression(Expression left, Expression right) { // Raw types because of given interface
        return new BinaryExpression<Boolean,Expression<Boolean>,Expression<Boolean>>(left, right) {
            @Override
            protected Boolean operation(Expression<Boolean> left, Expression<Boolean> right) {
                return left.getValue() & right.getValue();
            }
        };
    }

}

Expression.java

abstract public class Expression<V> {
    public abstract V getValue();
}

BasicExpression.java

public class BasicExpression<V> extends Expression<V> {
    public BasicExpression(V value) {
        this.value = value; 
    }
    @Override
    public V getValue() {
        return value;
    }
    private V value;
}

BinaryExpression.java

abstract public class BinaryExpression<V, L, R> extends Expression<V> {
    public BinaryExpression (L l, R r) {
        this.left = l;
        this.right = r;
    }
    @Override
    public V getValue() {
        return operation(left, right);
    }
    abstract protected V operation(L left, R right);

    private L left;
    private R right;
}

The provided interface is:

/**
 * @param <E>
 *            Your class for representing an expression.
 */
public interface IExpressionFactory<E> {
    public E createSumExpression(E left, E right) throws ModelException;
    public E createAndExpression(E left, E right) throws ModelException;
    // ...
}
like image 680
tttapa Avatar asked Nov 08 '22 07:11

tttapa


1 Answers

Generics are not really useful in this case, since most of the types will be derived from an input string at run time, and generics are a compile time thing. There is no way to know the types ahead of time any ways. It's only really useful in a simple example like this.

So my suggestion is to discard the generics and implement your own dynamic type system. For instance something like:

enum Type {
    Integer,
    Boolean;
}

class ScriptObject {
    private final Object value;
    private final Type type;

    private ScriptObject(Object value, Type type) {
        this.value = value;
        this.type = type;
    }

    public static ScriptObject of(boolean b) {
        return new ScriptObject(b, Type.Boolean);
    }

    public static ScriptObject of(int i) {
        return new ScriptObject(i, Type.Integer);
    }

    public int asInt() {
        return (int) value;
    }

    public boolean asBoolean() {
        return (boolean) value;
    }

    public static boolean areType(Type type, ScriptObject...objects) {
        for(ScriptObject o : objects) {
            if(o.type != type)
                return false;
        }

        return true;
    }

    @Override
    public String toString() {
        return value.toString();
    }

}

abstract class Expression {
    public abstract ScriptObject getValue();
}

class BasicExpression extends Expression {
    private final ScriptObject value;

    public BasicExpression(ScriptObject value) {
        this.value = value;
    }

    @Override
    public ScriptObject getValue() {
        return value;
    }

}

abstract class BinaryExpression extends Expression {
    private final Expression left;
    private final Expression right;

    public BinaryExpression (Expression l, Expression r) {
        this.left = l;
        this.right = r;
    }

    @Override
    public ScriptObject getValue() {        
        return operation(left.getValue(), right.getValue());
    }

    protected abstract ScriptObject operation(ScriptObject left, ScriptObject right);

}

Converting your example will look like this:

public static void main(String[] args) {
    Expression left1 = new BasicExpression(ScriptObject.of(42));
    Expression right1 = new BasicExpression(ScriptObject.of(3));     
    Expression sum = createSumExpression(left1, right1);
    System.out.printf("%s + %s = %s%n",left1.getValue(), right1.getValue(), sum.getValue());

    Expression left2 = new BasicExpression(ScriptObject.of(true));
    Expression right2 = new BasicExpression(ScriptObject.of(false));
    Expression and = createAndExpression(left2, right2);
    System.out.printf("%s && %s = %s%n",left2.getValue(), right2.getValue(), and.getValue());

    createAndExpression(left1, right2).getValue(); // fails with: Can not apply '&' to '42' and 'false' 
}

private static Expression createSumExpression(Expression left, Expression right) {
    return new BinaryExpression(left, right) {
        @Override
        protected ScriptObject operation(ScriptObject left, ScriptObject right) {
            if(!ScriptObject.areType(Type.Integer, left, right)) {
                throw new RuntimeException("Can not apply '+' to '" + left + "' and '" + right + "'");
            }
            return ScriptObject.of(left.asInt() + right.asInt());
        }
    };
}

private static Expression createAndExpression(Expression left, Expression right) {
    return new BinaryExpression(left, right) {
        @Override
        protected ScriptObject operation(ScriptObject left, ScriptObject right) {
            if(!ScriptObject.areType(Type.Boolean, left, right)) {
                throw new RuntimeException("Can not apply '&' to '" + left + "' and '" + right + "'");
            }
            return ScriptObject.of(left.asBoolean() && right.asBoolean());
        }
    };
}

I've tried to keep the example simple, but some notes are that you probably want to create a bunch of helper functions for the type checks and also use a custom exception type, probably a checked one that you then catch and output it's message somewhere, instead of just crashing.

You have to do the type checks yourself, which is more work, but the advantage is that you can totally decide when (and even if) you check the types, and what kind of error message to supply.

There is also still room to create a conversion mechanism for types, so instead of just checking that the type is what you want, you could try to convert it to the desired type, which is not a feature (besides auto-(un)boxing) of Java's type system.

like image 122
Jorn Vernee Avatar answered Nov 13 '22 03:11

Jorn Vernee