Instance methods #
In many object-oriented languages (like Java), methods are implemented within classes.
An instance method is called using an instance of the class where the method is declared.
Example. In Java, consider an instance method
myMethod()declared in a classMyClass:public class MyClass { ... public MyClass(){ ... } public void myMethod(){ ... } }This method can be called by appending
.to a variablemyVarof typeMyClass, as follows:MyClass myVar = new MyClass(); myVar.myMethod();
The object used to call an instance method can be accessed within this method.
Example (continued). The object referenced by the variable
myVaris accessible withinmyMethod(even though it is not passed as argument).
Benefit. This intuitively lets us write a method with one less argument.
Example. The following method is not an instance method. It verifies whether two instance of
MobileUnithave 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 $m$ can be declared in a class $C$ and a subclass $S$ or $C$. In this case, we say that $S$ overrides $m$.
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;
}
}
The following increases the health of (the object referenced by) myUnicorn by 2, because Unicorn is a subclass of MobileUnit.
Unicorn myUnicorn = new Unicorn("green");
myUnicorn.regen();
In contrast, the following 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, we can use the annotation
@Overrideto indicate an overriding method, 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 determines which version of a method must be called when a program is executed (a.k.a. “at run time”), if 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 MobileUnit {
...
@Override
public void defend(MobileUnit attacker) {
regen();
super.defend(attacker);
}
}