TL;DR: you can do
class LoopClass(){
...
Car car = new Car();
Bicycle bicycle = new Bicycle();
LoopClass(){
ArrayList<TickInterface> rides = new ArrayList<TickInterface>();
rides.add(car);
rides.add(bicycle);
}
void thisLoopsEveryFrame(){
for(TickInterface ride : rides){
ride.tick();
}
}
void refuelCar(){
car.refuel(); // possible now car has compile-time type of Car
}
}
Explanation:
You're confusing "compile-time type" and "runtime type": the statement
I want to call tick()
on both different objects with same interface
but this causes me not being able to call refuelCar()
from Car
object.
is not true.
The methods that an object actually has, i.e. the members of the object, are determined by the actual type of the object in memory at runtime (the "runtime type"). This in turn is determined by the constructor that was used to create the object.
So when you write
TickInterface car = new Car();
then when this code is executed at runtime, it creates an object in memory (on the heap) of type Car
. You can think of this object as having both a tick()
method and a refuel()
method.
On the other hand, the methods the compiler will allow you to call are determined by the compile-time type: that is, the type of the reference variable used to refer to an object.
By writing
TickInterface car ;
you create a reference (called car
) of compile-time type TickInterface
. This means that the compiler will only let you call
car.tick();
(because the compiler knows car
is of type TickInterface
, and it knows TickInterface
declares a method called tick()
), but it will not let you do
car.refuel();
because not every TickInterface
instance has a method called refuel()
.
When you assign a value to car
with
car = new Car();
you are performing an upcast. The type of the expression on the right hand side of the =
is Car
, while the type of the expression on the left hand side is TickInterface
. Since the compiler is assured that every Car
instance is also a TickInterface
instance, this is perfectly legal.
When you add car
to your list:
rides.add(car);
you effectively create a second reference to the Car
object you created. The second reference is kept internally in the List
. Since you declared the list to be of type TickInterface
, with
List<TickInterface> rides = new ArrayList<TickInterface>();
you can think of that hidden internal reference as being of compile-time type TickInterface
as well.
However, there is no reason for both these references to be the same type. You can do
Car car = new Car();
Bicycle bicycle = new Bicycle();
LoopClass(){
ArrayList<TickInterface> rides = new ArrayList<TickInterface>();
rides.add(car);
rides.add(bicycle);
void thisLoopsEveryFrame(){
for(TickInterface ride : rides){
ride.tick();
}
}
Now car
has compile-time type Car
(and bicycle
has compile-time type Bicycle
). The call
rides.add(car);
is perfectly legal: rides.add(...)
is expecting something of type TickInterface
, and you are giving it a Car
: the compiler again is assured that every Car
instance is also an instance of TickInterface
. In this version, you have moved the upcast to this point in the code, instead of to the assignment to car
.
Now, because the compile-time type of car
is Car
, the method you wanted to write:
void refuelCar(){
car.refuel();
}
will compile and execute just fine.