Java Generics - quick question on Greater Than Method with Objects

Viewed 655

I have an assignment in my Java class, we are learning Generics, I have looked in my notes, my lessons, and even on the internet and I still cant figure out what this last assignment question is asking me to do, which is:

Write a generic “greater-than” function that takes two objects as arguments, each of which has a “value” method which returns an integer and returns the argument whose “value” method returns the larger integer. Your generic function should constrain its type argument so that types without a “value” method cannot be used.

Is there a reason why "value" in quotation marks and how can a method constrain its type argument so that types without a "value" method cant be used. I'm mainly asking for clarification or maybe a little example. Am I creating a Value class and then doing something like this:

public static <T> int greaterThan(Value obj1, Value obj2){ // Value will have the "value" method?
    // do I compare the objects here or in the Value class?
}
3 Answers

They are apparently asking you to implement a generic maxByValue method. Since greater-than contains a hyphen, and is thus an invalid Java identifier anyway, I'll stick to maxByValue.

The requirement of having a value method is mentioned a few times, so let's encode it as an interface:

interface Value {
    int value();
}

Now, the main point of having a generic parameter here is to make sure that the return type of the maxByValue is specific enough to be useful. Let's call this type T. In order for the arguments to be comparable, T must be a subtype of Value. The only meaningful source for obtaining the return type is from the types of arguments. Putting together the three points:

  • Type parameter T subtype Value
  • Type inferred from the arguments
  • Result type is T

gives you the signature:

    public static <T extends Value> T maxByValue(T a, T b)

There are basically just two meaningful ways of implementing this. Let's take the left-biased one (i.e. left argument is returned if value is the same):

public static <T extends Value> T maxByValue(T a, T b) {
    if (a.value() < b.value()) {
        return b;
    } else {
        return /* left as an exercise */;
    }
}

Let's try it on a trivial integer example:

    class IntValue implements Value {
        final int v;
        public IntValue(int v) {
            this.v = v;
        }
        public int value() {
            return v;
        }
        @Override
        public String toString() {
            return "" + v;
        }
    }
    
    IntValue a = new IntValue(42);
    IntValue b = new IntValue(58);
    IntValue c = max(a, b);
    System.out.println(c); // prints "58"

So far so good. Let's see how precise the type inference is:

static interface Vehicle extends Value {
    String doGenericVehicleSound();
}
static abstract class Car implements Vehicle {
    public abstract String doCarSpecificSound();
    public String doGenericVehicleSound() {
        return doCarSpecificSound();
    }
}
static class Train implements Vehicle {
    public String doGenericVehicleSound() {
        return "tk-tk-------tk-tk--tk-tk--------------tk-tk";
    }
    public int value() {
        return 10000000;
    }
}
static class Ferrari extends Car {
    public String doCarSpecificSound() {
        return "rr-rrr-rrrr-rrrrrrr-rrrrrrrrrrrrrrrrrrrr-RRRRRRRRRR";
    }
    public int value() {
        return 222000;
    }
}
static class Tesla extends Car {
    public String doCarSpecificSound() {
        return "...        ...           ¯\\_(ツ)_/¯";
    }
    public int value() {
        return 50000;
    }
}

public static void main(String []args){
    System.out.println(maxByValue(new Ferrari(), new Tesla()).doCarSpecificSound());
    System.out.println(maxByValue(new Tesla(), new Train()).doGenericVehicleSound());
    // System.out.println(maxByValue(new Tesla(), new Train()).doCarSpecificSound());
        
}

The point to see here is the following. We have the following subtyping relation:

Train   extends Vehicle
Car     extends Vehicle
Ferrari extends Car
Tesla   extends Car

and the following least upper bounds for the concrete instances:

LUB(Train, Train) = Train
LUB(Train, Ferrari) = Vehicle
LUB(Train, Tesla) = Vehicle
LUB(Ferrari, Ferrari) = Ferrari
LUB(Ferrari, Tesla) = Car
LUB(Tesla, Tesla) = Tesla

(and all symmetric cases too).

Now, when we

  • put two cars into maxByValue, we get out a car (first example), but
  • when we put a car and a train into maxByValue, we get a more general Vehicle, so that the car-specific methods become unavailable (examples two and three; third does not compile - rightly so, because a train has no car-methods).
  1. Let's define the interface with value() method
    and provide default implementation of isGreaterThan

We could make this interface generic (to work with the types bounded to HasValue)

public interface HasValue<T extends HasValue<T>> {
    int value();
    default boolean isGreaterThan(T that) {
        return this.value() > that.value();
    }
}
  1. Now generic methods should be upper-bound T extends HasValue:
static <T extends HasValue> boolean isGreaterThanGeneric(T arg1, T arg2) {
    return arg1.value() > arg2.value();
}

static <T extends HasValue> T getGreaterGeneric(T arg1, T arg2) {
    return arg1.isGreaterThan(arg2) ? arg1 : arg2;
}
  1. Let's provide some test implementations of HasValue interface and test them. Note: implementations should be bound HasValue<MyClass> to restrict the interface to be applied to the same type (thanks @Kayaman)
class Apple implements HasValue<Apple> {
    final String type;
    Apple(String t) {
        this.type = t;
    }
    @Override
    public int value() { return 10; }
    
    @Override
    public String toString() {
        return "apple " + type;
    }
}

class Orange implements HasValue<Orange> {
    @Override
    public int value() { return 20; }
    
    @Override
    public String toString() {
        return "orange";
    }
}

Test:

public static void main(String args[]) {
    Apple apple = new Apple("Golden");
    Apple apple1 = new Apple("Jonagold");
    Orange orange = new Orange();
    
    System.out.println("apple greater than orange = " + getGreater(apple, orange));
    System.out.println("apple greater than another apple generic = " + getGreaterGeneric(apple, apple1));
    // System.out.println("apple greater than orange generic = " + getGreaterGeneric(apple, orange)); // <-- won't compile: 
            // required: T,T
            // found: Apple,Orange
            // reason: inference variable T has incompatible bounds
}

Output:

apple greater than orange = orange
apple greater than another apple generic = apple Jonagold

The previous example is a little complex, but all you're doing is binding your generic value so that it won't accept something that doesn't compile.

With your code...

public static <T> int greaterThan(Value obj1, Value obj2){ }

...the generic here is useless since you only ever use Value objects. This might incidentally work because if you have a FooValue extends Value object, because FooValue is-a Value, it'd be seen as a valid parameter here.

(Note: this is how Java pre-generics was done: inheritance.)

What you're probably looking for is this instead:

public static <T extends Value> int greaterThan(T obj1, T obj2) { }

This is a bound such that the generic type T is bound to all values that are Value and its subclasses.

Related