Remember inheritance defines a "is a" kind of relationship. The code
class Position2D extends Vector2D
tells the compiler that every Position2D is a Vector2D, but not vice versa.
Check out the following examples assuming that you already made the above declaration:
// obviously okay, type of instance matches reference type
Vector2D vecVecRef = new Vector2D();
Position2D posPosRef = new Position2D();
// this is okay because a Position2D is a Vector2D
Vector2D posVecRef = new Position2D();
// The compiler will not automatically convert a Vector2D to a Position2D
Position2D vecPosRef = new Vector2D(); //Compiler error!
A Position2D variable can never refer to a Vector2D object. If you try typecasting it, the compiler will say "fine...I trust you" but then the JVM will get angry when it actually finds out that you are trying to assign a Vector2D to a Position2D. It will raise a ClassCastException
at runtime.
// compiles fine but raises ClassCastException in runtime
Position2D vecPosRef = (Position2D) new Vector2D();
This is because you can only Class cast a subclass into a superclass and not vice versa. So basically, you cannot cast Vector2D to Position2D and you cannot assign it without casting either.
The simplest solution to this problem is to have a constructor defined in your subclass that makes a Position2D object out of a given Vector2D object.
class Position2D extends Vector2D {
Position2D() {
// default stuff
}
Position2D(Vector2D v) {
// you currently don't have the getX and getY methods
// so define them in your superclass
setX(v.getX());
setY(v.getY());
}
}
With that one simple and convenient constructor, you can use code like this:
public class Inheritance {
public static void main(String[] args) {
Position2D pos1 = new Position2D();
Position2D pos2 = new Position2D();
pos1.setX(3);
pos1.setY(4);
pos2.setX(5);
pos2.setY(6);
Position2D pos3 = new Position2D(pos1.add(pos2)); // using constructor
System.out.println(pos3.getX()); // this prints 8.0
}
}
As you can see, this way is much more extensible than rewriting all of the subclass methods.
Your problem is that you link method calls before the program is fully type-checked. In a complicated language with subtype polymorphism and parametric polymorphism, you cannot do any kind of linking before you know the type (or in the case of subtyping, the type bounds) of an expression. The solution is to introduce type variables for expressions of (initially) unknown type, and properly unify type variables with concrete types. If you cannot resolve a type variable, that's an error and not an invitation to deduce the most general type.
A proper type inferer would look at the code
var list2 = list.mapped { s => s.toUpperCase }
in the static environment
list : List[String]
List[E]#mapped[U] : (E => U) => List[U]
String#toUpperCase : () => String
like this:
- infer
var list2 = list.mapped { s => s.toUpperCase }
- infer
list.mapped { s => s.toUpperCase }
- infer
list
- known to be
List[String]
- save
E = String
to the static environment for this expression.
- resolves to
List[E]#mapped[U]
- introduce variable
$U
for U
.
- known to be
(E => $U) => List[$U]
- unify arguments:
(E => $U) >: typeof({ s => s.toUpperCase })
- infer
{ s => s.toUpperCase }
- introduce variables
$a, $b
- known to be
$a => $b
- unify
(E => $U) >: ($a => $b)
: E <: $a
and $b <: $U
. Technically, these are just type bounds, but let's consider them equal.
- save
E = $a
, $b = $U
to the static environment for this expression
- save
s : $a
to the static environment for this expression
- infer
s.toUpperCase
- infer
s
- infer
.toUpperCase
.
- matches
String#toUpperCase
- known to be
() => String
- is
String
- unify
String <: $b
, but lets consider them equal
- save
$b = String
to the static environment for this expression
- is
($a => $b) = (String => String)
- already unified
(E => $U) >: ($a => $b)
.
- is
List[$U] = List[String]
- infer
var list2
- introduce variable
$c
- save
list2 : $c
into the static environment for this scope
- is
$c
- unify
$c >: List[String]
, but let's consider them equal
- save
$c = List[String]
into the static environment
After this round of type inference, asking the static environment for the type of list2
gives us List[String]
. The above steps roughly follow along the type inference steps for a Hindley-Milner type system, but note that you are probably dealing with subtyping, which makes type inference far more complicated: unifying a type only provides us bounds on the type and very rarely a concrete type. In the above example, I ignored these details and always unified the types as equals.
The second major problem is infering the type of the object on which to dispatch for a method call. In this example, I always tried to infer the type of the object before resolving the method call. However, this is not necessary. A method List[E]#mapper[U] : (E => U) => List[U]
could also be viewed as a free function .mapper[E, U]: (List[E], E => U) => List[U]
. In other words, the implicit this
argument is transformed to an explicit argument. This allows us use type variables for the invocant variable, and makes it easier to deal with a set of overloads. Consider the following example:
static env:
.add[E] : (List[E], E) => void
.add[E] : (List[E], List[E]) => void
.add[K, V] : (Map[K, V], K, V) => void
.add : (Integer, Integer) => Integer
a : $a
b : $b
expression:
a.add(b)
when type-infering that expression, we initially only know that this is some kind of .add
call, but we do not know which one – we cannot resolve it at this point. We have to unify each of the possible types with the given types. If one unification fails, we remove that possibility from the set of overloads.
- infer
a.add(b)
- candidate
(List[E], E) => void
- variable
$c
for E
- unify arguments
- is
void
- candidate
(List[E], List[E]) => void
- variable
$c
for E
- unify arguments
$a = List[$c]
$b = List[$c]
- is void
- candidate
(Map[K, V], K, V) => void
- variable
$c
for K
- variable
$d
for V
- unify arguments
$a = Map[$c, $d]
$b = $c
- ERROR: no argument for
V
-parameter
- discard candidate
- candidate
(Integer, Integer) => Integer
- unifiy arguments
$a = Integer
$b = Integer
- is
Integer
So after doing a round of type inference, we are still left with a number of choices. Some languages employ some ranking mechanism, e.g. methods on subtypes are preferred over methods on supertypes. In our example, there is no sensible ranking. If you want your type-checker to be moderately well-performing, you should issue a compiler exception here, e.g.
foo.sourcefile:42:3 error: could not resolve call to method `add`
a.add(b)
^
candidates are:
List[E]#add(E elem)
List[E]#add(List[E] elems)
Integer#add(Integer rhs)
Likewise, you should display an error when the set of possible methods is empty after attempting unification.
Note that for a different static environment, the the set of possible solutions would only contain one method which you could bind to. E.g.:
a : List[$c]
b : $c
Since the call can be resolved unambiguously in that case, we can continue doing type inference for the rest of the compilation unit in hopes of resolving the $c
type variable.
Depending on how you implement generics, you could link the method call directly after successfully infering the type for the call. If you need to know the value of the type variable $c
, you need to wait until type inference for the compilation unit has completed, and can do linking in a second pass. This is now easy to do since all expressions are already annotated with their inferred type.
Best Answer
An implementation of an interface is the realization of that interface. Realization is being implemented within the class. Just return an instance of the class (A or B) which realizes the return-type interface.