Instance methods

Instance methods #

In most object-oriented languages (like Java), methods are implemented within class declarations.

An instance method can only be called using an instance of the class where it is declared.

For instance, in Java, an instance method declared in MyClass can be called by appending . to a variable of type MyClass. The object referenced by the variable is accessible in the method, as well as its attributes.

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.

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 a 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, syntactic mistakes (a.k.a. compile time errors) are easier to fix than bugs (a.k.a. runtime errors). So when possible, it is good practice to use features of a language that prevent compilation of incorrect programs. 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()).

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 vs defender)
      ...
  }
}
public class Unicorn extends Unit {
  ...

  @Override
  public void defend(MobileUnit attacker) {
    regen();
    super.defend(attacker);
  }
}