What you need to understand is that every object has one concrete class. It "is" an instance of that class, and it "is" an instance of every class that class inherits, all at the same time. Not only that but it "is" an instance of every interface any of those classes inherit, all at the same time.
So, assuming:
class Animal
class Mammal extends Animal implements Suckler
class Dog extends Mammal implements Woofer
... if I create a new Dog()
then that object "is" an Object
(because all objects inherit Object
), an Animal
, a Mammal
, a Suckler
, a Dog
and a Woofer
, all at the same time.
However, a variable is not the same thing as an object. A variable points at an object, and a variable has a type. The type must be compatible with the object assigned, but that's all.
So:
Suckler s = new Dog();
works, but from that moment on, all the compiler knows about the object via the s
variable is that it's a Suckler
. It doesn't know it's a dog; it doesn't know it's a mammal. So we cannot then go:
Dog d = s;
... because the compiler can't guarantee that the variable pointed to by s
is a Dog
.
The type of the variable cannot ever be changed. s
has type Suckler
for the whole of its lifetime, no matter what. We could assign a Sheep
or a Pig
to s
, but we won't be able to do anything to those objects except operations that are part of the Suckler
definition.
I'm going to assume that the various classes and interfaces are defined like this:
public interface Edible {
...
}
public class Sandwich implements Edible {
...
}
public class Rectangle { // note, does not implement or extend anything
// (except Object)
...
}
So:
Sandwich sub = new Sandwich();
Edible e = null;
e = sub;
This is fine. sub
is a Sandwich
, and Sandwich
is a kind of Edible
.
Sandwich sub = new Sandwich();
Edible e = null;
sub = e;
This won't compile. e
is an Edible
, but there could be any number of classes that implement Edible
, as well as Sandwich
. sub
has to be a Sandwich
and since the compiler can't be sure that e
is a sandwich, it will refuse to compile.
Sandwich sub = new Sandwich();
Edible e = null;
sub = (Sandwich) e;
This works. As you've correctly worked out, the cast tells the compiler "OK, you can't be sure that e
is a Sandwich
, but as the coder, I'm telling you it is.
If you did this, and at runtime e
was actually an Apple implements Edible
, and not a Sandwich
, the JRE would throw a ClassCastException
. It's your job to make sure this doesn't happen -- avoiding casting is the best way to do this.
Sandwich sub = new Sandwich();
Rectangle cerealBox = new Rectangle(5, 10, 20, 30);
sub = (Sandwich) cerealBox;
... will refuse to compile. Sandwich
and Rectangle
are not related to one another. The compiler knows that no Sandwich
is also a Rectangle
, so it refuses to compile.
The variable sub
must always point to a Sandwich
, and cerealBox
must always point to a Rectangle
. The only way a Rectangle
could be a Sandwich
is if Rectangle
inherited Sandwich
, or vice versa. Since neither of these is the case, it won't compile.
This is assuming the declarations above. It's possible for a class to implement multiple interfaces, so if it was public class Sandwich implements Edible, Rectangle {...}
, this code would work.
Rectangle cerealBox = new Rectangle(5, 10, 20, 30);
Edible e = null;
e = cerealBox;
... will not compile. A Rectangle
is not an Edible
.
Rectangle cerealBox = new Rectangle(5, 10, 20, 30);
Edible e = null;
e = (Edible) cerealBox;
.. at first glance, you might think will not compile. A Rectangle
is not an Edible
, and you can't tell the compiler it is. However the compiler can't guarantee that there isn't a class like this:
public class Flapjack extends Rectangle implements Edible { ... }
A Flapjack
would be a kind of Rectangle
that is also an Edible
, and since the compiler isn't clever enough to know that cerealBox
is not a Flapjack
, it must compile (it will fail in runtime).
A really clever compiler might be able to analyse the program logic to see that cerealBox
has been initialised as new Rectangle()
, and that there has been no opportunity for that to change at runtime. But the Java standard does not have that kind of sophisticated static analysis.
The writer of Rectangle
could ensure that Square
can't exist, by defining it as public final class Rectangle
-- the final
keyword forbids subclasses.
Rectangle cerealBox = new Rectangle(5, 10, 20, 30);
Edible e = null;
e = (Rectangle) cerealBox;
... won't compile. e
is still an Edible
. You can't assign a Rectangle
to it, because Edible
and Rectangle
are not related.
Edible e = null;
e = (Rectangle) null;
Casting null to a Rectangle
is fine, however e
is an Edible
, and you can't assign a Rectangle
to an Edible
, since they are unrelated types.