简体   繁体   中英

In this Java class hierarchy, how to use Generics so that type casts are unnecessary?

Suppose I have 2 classes called RealNumber and IntNumber , and a method called plus :

public class RealNumber<S extends RealNumber> {
    public S plus(S realNumber) {
        ...
        }
    }

public class IntNumber
      extends RealNumber<IntNumber> { }

When using the plus method I get some compiler warnings and errors:

RealNumber x = new RealNumber();
IntNumber y = new IntNumber();

RealNumber sum1 = x.plus(x); // Warning: Unchecked call to 'plus(S)' as a member of raw type 'RealNumber'.
RealNumber sum2 = x.plus(y); // Warning: Unchecked call to 'plus(S)' as a member of raw type 'RealNumber'.
RealNumber sum3 = y.plus(x); // Error: plus (IntNumber) in RealNumber cannot be applied to (RealNumber).
RealNumber sum4 = y.plus(y); // This works fine.
IntNumber  sum5 = y.plus(x); // Error: plus (IntNumber) in RealNumber cannot be applied to (RealNumber).
IntNumber  sum6 = y.plus(y); // This works fine.

To fix this, I modify the plus method, as follows:

public <T extends RealNumber> S plus(T realNumber) {
    ...
    }

And now everything works fine. Good! However, I want now to add a third class called PositiveIntNumber that extends IntNumber :

    public class PositiveIntNumber extends IntNumber { }

This, of course, does not work:

RealNumber x = new RealNumber();
IntNumber y = new IntNumber();
PositiveIntNumber z = new PositiveIntNumber();

RealNumber sum1 = x.plus(x); // Fine.
RealNumber sum2 = x.plus(y); // Fine.
RealNumber sum3 = y.plus(x); // Fine.
RealNumber sum4 = y.plus(y); // Fine.
IntNumber sum5 = y.plus(x);  // Fine.
IntNumber sum6 = y.plus(y);  // Fine.
RealNumber sum7 = x.plus(z); // Fine.
IntNumber sum8 = y.plus(z);  // Fine.
PositiveIntNumber sum9 = z.plus(x);  // Error: incompatible types: no instance(s) of type variable(s) T exist so that IntNumber conforms to PositiveIntNumber
PositiveIntNumber sum10 = z.plus(y); // Error: incompatible types: no instance(s) of type variable(s) T exist so that IntNumber conforms to PositiveIntNumber
PositiveIntNumber sum11 = z.plus(z); // Error: incompatible types: no instance(s) of type variable(s) T exist so that IntNumber conforms to PositiveIntNumber

To fix this again, I modify class definitions, as follows:

public class IntNumber<S extends IntNumber>
      extends RealNumber<S> { }

public class PositiveIntNumber
      extends IntNumber<PositiveIntNumber>
    { }

This solves the problem for PositiveIntNumber , but breaks IntNumber :

RealNumber x = new RealNumber();
IntNumber y = new IntNumber();
PositiveIntNumber z = new PositiveIntNumber();

RealNumber sum1 = x.plus(x); // Fine.
RealNumber sum2 = x.plus(y); // Fine.
RealNumber sum3 = y.plus(x); // Fine.
RealNumber sum4 = y.plus(y); // Fine.
IntNumber sum5 = y.plus(x);  // Error: incompatible types: RealNumber cannot be converted to IntNumber.
IntNumber sum6 = y.plus(y);  // Error: incompatible types: RealNumber cannot be converted to IntNumber.
RealNumber sum7 = x.plus(z); // Fine.
IntNumber sum8 = y.plus(z);  // Error: incompatible types: RealNumber cannot be converted to IntNumber.
PositiveIntNumber sum9 = z.plus(x);  // Fine.
PositiveIntNumber sum10 = z.plus(y); // Fine.
PositiveIntNumber sum11 = z.plus(z); // Fine.

A cast fixes it, but it should be unnecessary, in my view:

IntNumber sum5 = (IntNumber)y.plus(x);

So, I have two questions:

1) Since y is an IntNumber , and the return type S extends IntNumber , why does y.plus(...) return RealNumber ?

2) How to fix this?

Edit : Writing RealNumber<RealNumber> should be unnecessary, since RealNumber<IntNumber> and RealNumber<PositiveIntNumber> make absolutely no sense. So maybe the entire use of generics like this is just plain wrong. I think this answers question 1: y.plus(...) returns RealNumber because y is raw, so Java type system just doesn't care anymore that S extends IntNumber . However, I want to avoid repeating the plus method in all subclasses of RealNumber, and I should be able to use generics, in some way, to avoid that. So question 2 still stands. What should I be doing?

Note: My actual classes are not Reals and Integers, but some other complicated business classes. Think of them as classes A, B, C and don't question the model. They are "addables", yes, but here X.plus(Y) should return type X, for every X and Y.

I'm assuming what you are trying to create is a type hierarchy that expresses "Addables" which return, given two "operands", the more general of the two, using inheritance to model the mathematical relationship of "special case".

Before I go any further, you should consider this link which uses the Square is-a Rectangle example to illustrate why that use of inheritance is often a bad idea, but in the case of immutable objects like the ones you're proposing you might be ok.

You want both Int.plus( Real ) and Real.plus( Int ) to return Real, and Int.plus( Int ) to return Int. The same pattern should work for Nat.plus( Int ), etc.

As long as all the types are statically known, that should be a piece of cake type-wise.

class Real {
    Real plus( Real r ) { ... }
    Int floor() { ... }
}
class Int extends Real {
    Int plus( Int i ) {
        return plus( i.asReal() ).floor();
    }
    Nat abs() { ... }
    Real asReal() {
        return this;
    }
}
class Nat extends Int {
    Nat plus( Nat n ) {
        return plus( n.asInt() ).abs();
    }
    Int asInt() {
        return this;
    }
}

Overloading plus allows the compiler to determine the "most specific" plus operation that can be statically verified. The compiler can't know that adding two reals that happen to be integers results in another real that's an integer. For that matter, some non-integers add up to integers; I'm sure you don't intend for the type system to represent that fact somehow.

Under this arrangement, all but sum5 , sum9 , and sum10 type check, which is (to my mind) exactly as it should be.

Incidentally, with respect to the "curiously recurring type constraint" pattern you were trying to demonstrate, you're doing it slightly wrong. You need to supply a type parameter everywhere the generic type occurs, even inside the type bound itself:

class G< T extends G< T > > {}

You, and all the other commenters and responders here, have been leaving off the second T .

All of the comments above are right. if you define a generic class you should define it also if you use it.

public class GenericNumber {

    public static void main(String[] args) {
        // ------
        // ------ The important change
        // ------
        RealNumber<RealNumber> x = new RealNumber<>();            
        IntNumber<IntNumber> y = new IntNumber<>();
        PositiveIntNumber z = new PositiveIntNumber();
        // Everything works fine
        RealNumber sum1 = x.plus(x); 
        RealNumber sum2 = x.plus(y); 
        RealNumber sum3 = y.plus(x); 
        RealNumber sum4 = y.plus(y); 
        IntNumber sum5 = y.plus(x); 
        IntNumber sum6 = y.plus(y); 
        PositiveIntNumber sum9 = z.plus(x);  
        PositiveIntNumber sum10 = z.plus(y); 
        PositiveIntNumber sum11 = z.plus(z); 
    }

    public static class RealNumber<S extends RealNumber> {

        public <T extends RealNumber> S plus(T realNumber) {
            return null;
        }
    }

    public static class IntNumber<S extends IntNumber> extends RealNumber<S> {
    }

    public static class PositiveIntNumber extends IntNumber<PositiveIntNumber> {
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM