From a specific expression tree to a generic one - java

Viewed 92

I want to write an abstract class 'Expression' that takes either Integer or Boolean expressions and evaluates them through subclasses like 'Add', 'Divide' for Integer and 'And', 'Or' for Boolean expressions. In the end it comes down to writing subclasses that implement their own evaluate() method. I found an implementation in a book but it only works with double values. Here it is:

abstract class Expr {
    abstract double eval();
}
abstract class BinOp extends Expr {
    Expr left;
    Expr right;
    BinOp(Expr l, Expr r) {
    }
}
class Const extends Expr {
    private double value;
    Const( double val ) {
        this.value = val;
    }
    double eval () {
        return this.value;
    }//eval
}

Now, for the class BinOp I can write a class 'Add' that extends it, calls it's constructor and implements eval() with the multiplication of 2 Const objects, which eval() themselves and simply return the value they were instantiated with.

How would this work if I wanted to do it with Expr that are not strictly bound to evaluate to double, but to either int or boolean? I've read up on generics but I can't seem to correctly design the classes like Expr such that my code compiles. Here's my try:

public abstract class Expression<T> {
    abstract T evaluate();
}
public class Const<T> extends Expression{
    private T n;
    public Const(T x) { n = x; }
    public Const<Integer> integerConst(Integer n) {
        return new Const<>(n);
    }
    public Const<Boolean> booleanConstConst(Boolean n) {
        return new Const<>(n);
    }
    public T evaluate() {
        return n;
    }
}

Now I don't want anybody to do my homework for me, so I'm just asking where the fault in my approach lies and if someone could point me in the right direction. Thank you.

2 Answers

Here are some suggestions:

  • First of all, you shouldn't use raw types, so Const<T> should extend Expression<T>.

  • Now, your integerConst and booleanConstConst methods look like factory methods, so they should be static.

  • That said, I'm not sure it's a good idea to have these factory methods in the Const class, since it would force you to change the Const type if you wanted to support a 3rd expression type (in addition to Boolean and Integer). Instead you might want to consider sub-classing Const:

    public class IntegerConst extends Const<Integer> {
        public IntegerConst(Integer n) {
            super(n);
        }
    }
    
    public class BooleanConst extends Const<Boolean> {
        public BooleanConst(Boolean b) {
            super(b);
        }
    }
    

Hopefully you can continue from here.

I think you should hide all logic behind Expression and all type specific logic should be in separate class.

To hide concrete implementation, you should use parametrized Expression class and factory method for each concrete implementation:

public interface Expression<T> {

    T evaluate();

    static BooleanExpression with(boolean val) {
        return new BooleanExpression(val);
    }

    static DoubleExpression with(double val) {
        return new DoubleExpression(val);
    }

}

Implementation for `boolean' values with required operators:

public final class BooleanExpression implements Expression<Boolean> {

    private final boolean left;

    public BooleanExpression(boolean left) {
        this.left = left;
    }

    public BooleanExpression and(boolean right) {
        return new BooleanExpression(left && right);
    }

    public BooleanExpression or(boolean right) {
        return new BooleanExpression(left || right);
    }

    @Override
    public Boolean evaluate() {
        return left;
    }
}

Implementation for integer values with required operators (I use double, because you ought to implement divide):

public final class DoubleExpression implements Expression<Double> {

    private final double left;

    public DoubleExpression(double left) {
        this.left = left;
    }

    public DoubleExpression add(double right) {
        return new DoubleExpression(left + right);
    }

    public DoubleExpression divide(double right) {
        return new DoubleExpression(left / right);
    }

    @Override
    public Double evaluate() {
        return left;
    }
}

Client code shows you that all concrete implementation are hidden and you have only acceptable operators for each expression:

boolean res1 = Expression.with(false).and(true).or(true).evaluate();   // true
double res2 = Expression.with(0.5).add(2.5).divide(2).evaluate();       // 1.5
Related