Instance methods #
In many object-oriented languages (like Java), methods are implemented within class declarations.
An instance method is called using an instance of the class where the method is declared.
For instance, in Java, consider an instance method myMethod()
declared in a class MyClass
:
public class MyClass {
...
public MyClass(){
...
}
public void myMethod(){
...
}
}
This method can be called by appending .
to a variable myVar
of type MyClass
, as follows:
MyClass myVar = new MyClass();
myVar.myMethod();
The object referenced by the variable myVar
is accessible from the method myMethod
(even though it is not passed as argument to the method) (even though it is not passed as argument to the method).
This intuitively allows us to write methods with one less argument.
For instance, consider the following method, which is not an instance method.
It verifies whether two instance of MobileUnit
have the same color:
public boolean sameColor(MobileUnit u1, MobileUnit u2){
return u1.color.equals(u2.color);
}
This method may be called as follows:
Unicorn myUnicorn = new Unicorn("green");
Butterfly myButterfly = new Butterfly("green");
boolean sameColor = sameColor(myUnicorn, myButterfly);
Instead, one may write an equivalent method, as an instance method of our class MobileUnit
, with one less argument:
public abstract class MobileUnit extends Unit {
String color;
...
public boolean sameColorAs(MobileUnit otherUnit){
return color.equals(otherUnit.color);
}
}
and we can call this method as follows:
Unicorn myUnicorn = new Unicorn("green");
Butterfly myButterfly = new Butterfly("green");
boolean sameColor = myUnicorn.sameColorAs(myButterfly);
}
Overriding #
A same instance method can be declared in a class $C$ and a subclass $S$ or $C$. In this case, we say that $S$ overrides the method (we also say that the method of $S$ overrides the method of $C$).
When such a method is called, the most specific applicable version is executed.
For instance, let us extend our example from the previous section with an instance method regen
, declared in both Unit
and MobileUnit
, as follows:
public abstract class Unit {
int health;
...
public void regen(){
if(health < 10){
health += 1;
}
}
}
public abstract class MobileUnit extends Unit {
...
public void regen(){
if(health < 10){
health += 1;
}
health += 1;
}
}
Now consider this program:
Unicorn myUnicorn = new Unicorn("green");
myUnicorn.regen();
This program increases the health of (the object referenced by) myUnicorn
by 2, because Unicorn
is a subclass of MobileUnit
.
However, the following program increases the health of (the object referenced by) myWall
by 1, because Wall
is a subclass of Unit
, but not a subclass of MobileUnit
.
Wall myWall = new Wall();
myWall.regen();
Hint. In Java, you can use the annotation
@Override
to indicate that a method overrides another, as follows:public abstract class MobileUnit extends Unit { ... @Override public void regen(){ if(health < 10){ health += 1; } health += 1; } }
This is not necessary. The benefit is that the program will not compile if the overridden and overriding methods have different signatures.
More generally, it is good practice to use features of a language that prevent compilation of incorrect programs. The reason is that syntactic mistakes (a.k.a. compile time errors) are easier to fix than bugs (a.k.a. runtime errors). This is why debugging in an untyped language (like Python, Javascript, Lua, etc.) can be more difficult than in a typed one (such as Java, C#, Typescript, etc.).
Dynamic dispatch (a.k.a. runtime polymorphism) #
Dynamic dispatch consists in determining which version of a method must be called when a program is executed (a.k.a. “at run time”), when this cannot be determined by analyzing the program alone. This is a feature of most (class-based) object-oriented languages.
For instance, in our example, assume a method generateRandomUnits
that generates a random array of units (butterflies, unicorns or walls).
And let us call the method regen
for each unit in this array:
Unit[] ramdomUnits = generateRandomUnits();
for (Unit unit: randomUnits){
unit.regen();
}
The most specific applicable version of the method regen
will be executed for each unit, based on its type, even though this type cannot be determined at compile time.
For instance, if there is an instance of Unicorn
in this array, then the method MobileUnit.regen()
will be executed for this instance (rather than the method Unit.regen()
).
Note that this also applies to scenarios where the type of a variable depends on the input of the program.
Code factorization #
An overriding method often extends the functionality of the overridden one.
This is a possible source of duplicate code.
For instance, in the example above, both implementations of regen()
contain:
if(health < 10){
health += 1;
}
A common way to factorize this consists in calling the overridden method inside the overriding one.
In Java, the keyword super
allows us to distinguish the two methods (since they have the same name).
For instance, in the above example, the overriding method may be better written as follows:
public abstract class MobileUnit extends Unit {
...
@Override
public void regen() {
super.regen();
health += 1;
}
}
In this example, what would be the effect of replacing super.regen()
with regen()
?
The method would not terminate.
Consider the method encounter
of the previous section.
Add it as an instance method to our example, so that:
- the method now distinguishes the attacker from the defender,
- a wall cannot attack,
- a unicorn gets a regen if it defends.
The trick here consists in viewing an encounter from the point of view of the defender:
public abstract class Unit {
int health;
...
public void defend(MobileUnit attacker) {
// subtract the health of the attacker from the health of the current unit
health -= attacker.health;
// the health of the attacket becomes the inverse of the remaining health of the current unit
attacker.health = -health;
// some code that makes the encounter asymmetric (attacker and defender are treated differently)
...
}
}
public class Unicorn extends Unit {
...
@Override
public void defend(MobileUnit attacker) {
regen();
super.defend(attacker);
}
}