Module 10, Part 2: More on Java Classes and Polymorphism#

This module we address a few more elements of programming, especially related to classes and inheritance: the “final” keyword, and static class fields. We also address the topic of subtype polymorphism in more depth, discussing when it does not work as one might expect, due to the difference between early and late binding.

The final Keyword#

The final keyword can be used in various ways in a Java program.

  • If we declare a local variable of a method as final, then the value assigned to that variable cannot be modified. This also applies to variables used for method arguments. Using final can prevent subtle bugs, because any attempt to “accidentally” modify the value of a final variable will result in an error from the Java compiler. This is illustrated in Example 62 below.

  • If we declare the field of a class as final, then the value assigned to that field cannot be modified after the field is initialised (i.e. after the first assignment of a value to that field, which may take place in a constructor). This is illustrated in Example 63 below.

Example 62 (Final local variables (and arguments) in methods)

Consider again the code of the class TwoSums in Example 31. We can observe that:

  • The static method main declares several local variables (s, n1, n2, sum1, sum2), whose value is never changed after their declaration. Still, it is possible to accidentally write code that modifies the value of those variables. To prevent this, we can mark each variable in main as final: if we do so, any attempt to assign a value to a final variable will cause a Java compilation error like “cannot assign a value to final variable.”

  • Instead, the static method sum declares two local variables (summation and i) whose value is changed after their declaration. These variables cannot be marked as final — because the program actually needs to modify them to perform its tasks. However, the static method sum also has a variable called n (used for the method’s argument), and the value of n should not be modified when sum is being executed: to prevent this, we can mark the argument n as final.

The modified class TwoSums using final for local variables and method arguments looks as follows. (The differences with the code in Example 31 are highlighted.)

 1class TwoSums {
 2    public static void main(String[] args) {
 3        final var s = new java.util.Scanner(System.in);
 4        s.useLocale(java.util.Locale.ENGLISH);
 5
 6        System.out.println("Please write two positive numbers:");
 7        final var n1 = s.nextInt();
 8        final var n2 = s.nextInt();
 9
10        final var sum1 = sum(n1);
11        final var sum2 = sum(n2);
12
13        System.out.println("Sum of all numbers from 1 to " + n1 + ": " + sum1);
14        System.out.println("Sum of all numbers from 1 to " + n2 + ": " + sum2);
15
16        s.close();
17    }
18
19    static int sum(final int n) {
20        var summation = 0; // Will be updated during the computation of the sum
21        for (var i = 1; i <= n; i = i + 1) {
22            summation = summation + i;
23        }
24        return summation;
25    }
26}

Example 63 (Final fields)

Consider again the class Pet in Example 57: the two fields name and species are initialised in the constructor, and never modified again. If that is desired, it is advisable to mark the two fields as final, as follows. (The differences with the code in Example 57 are highlighted.)

 1class Pet {
 2    private final String name;
 3    private final String species;
 4
 5    public Pet(String name, String species) {
 6        this.name = name;
 7        this.species = species;
 8    }
 9
10    public String getDescription() {
11        return this.name + " (" + this.species + ")";
12    }
13}

After this change, if we try to modify the value of one of the final fields (e.g. by writing this.name = "Test"; in the body of the method getDescription()), we will get an error. (Try it!)

Note

In Java, the keyword final can also be used to control inheritance:

  • if we declare a method final, then the method cannot be overridden in derived classes;

  • if we declare a whole class final, then the class cannot be extended by other classes.

We will not use these two features in this course — but they may be useful when writing more advanced programs.

Static Class Fields (and Methods)#

Up to this point we have seen that, when writing a class in Java, we can write two kinds of methods:

In a nutshell, the difference between the two is that:

  • Object methods can only be called on an object of the class that defines the method. For instance, in Example 63 we can see that the class Pet defines the (non-static) method getDescription(); hence, if pet is an object of type Pet, then we can call pet.getDescription(). (Correspondingly, the body of getDescription() can use the special variable this to access the object on which the method has been called.)

  • Instead, static methods are called “directly”: for instance, in Example 62 the static method sum(...) is simply called by writing sum(n1) and sum(n2) (in the body of the method main). (Correspondingly, the body of sum(..) cannot use the special variable this, because sum(...) is not an object method.)

    Note

    More verbosely, in the static method main in Example 62 we could have called the static method sum(...) by explicitly writing the name of the class that contains it — i.e. we could have written TwoSums.sum(n1) and TwoSums.sum(n2). Also in this case, the static method sum(...) is not called on an object.

Java offers a similar distinction between object fields (a.k.a. non-static fields) and static fields (a.k.a. class fields). The difference between the two is where the value of the field is stored:

  • the value of an object field is stored in each object that owns the field, as explained in Defining a Simple Class and Creating Objects. For instance, if both pet1 and pet2 are objects of the class Pet, then they both have a field called name (according to the definition of the class), and each object has its own value for that field (e.g. pet1.name may have the value "Viggo" and pet2.name may have the value "Kamilla").

  • Instead, the value of a static field is stored in a single global location accessed via the class name. Therefore (unlike an object field) the value of a class field is not stored in each object of the class that defines the field. This is illustrated in Example 64 and Example 65 below.

Example 64 (Converting measures)

Suppose we need two utility methods to convert a measure in miles into kilometers, and vice versa. Both methods need to know what is the number of kilometers in a mile; instead of repeating that number in each method, we can store it in a class field called e.g. kmsPerMile, used by both methods as follows:

 1class Measures {
 2    public static double kmsPerMile = 1.609344;
 3
 4    public static double milesToKms(double miles) {
 5        return miles * Measures.kmsPerMile;
 6    }
 7
 8    public static double kmsToMiles(double kms) {
 9        return kms / Measures.kmsPerMile;
10    }
11}

Note that the field kmsPerMile is stored “globally” and can be accessed by writing Measures.kmsPerMile (without creating an object of the class Measures).

Example 65 (Converting measures, version 2)

The code in Example 64 has a design flaw: the static field Measures.kmsPerMile can be accidentally modified (e.g. by writing an assignment like Measures.kmsPerMile = 42), and this would make the two static methods Measures.milesToKms(...) and Measures.kmsToMiles(...) return incorrect results.

To avoid this issue, we can mark the static field Measures.kmsPerMile as final, thus turning it into a constant that cannot be modified.

The resulting code would look as shown below — where we follow the Java convention of writing static final field names in uppercase letters (to better recognise that they are constants):

 1class Measures {
 2    public static final double KMS_PER_MILE = 1.609344;
 3
 4    public static double milesToKms(double miles) {
 5        return miles * Measures.KMS_PER_MILE;
 6    }
 7
 8    public static double kmsToMiles(double kms) {
 9        return kms / Measures.KMS_PER_MILE;
10    }
11}

This way, any part of the program can read the constant Measures.KMS_PER_MILE without the risk of accidentally modifying it.

Note

In the Java API, the mathematical constants Math.PI (which is the value of \(\pi\)) and Math.E (which is the value of \(e\), the base of natural logarithms) are declared as static final fields of a class called Math, similarly to Measures.KMS_PER_MILE above. If you are curious, you can have a look at the Java API documentation for the class Math.

Similarly, System.out is one of the static final fields of a class called System, and it contains the object on which we call e.g. System.out.println(...). If you are curious, you can have a look at the Java API documentation for the class System.

More on Subtype Polymorphism and Early vs. Late Binding#

We have mentioned that subtype polymorphism is a cornerstone of object-oriented programming, and we have already seen it in action when writing generic code that calls methods which “behave differently” because they are overridden in a subclass:

  • in Example 53, there is a loop that calls the method .getDescription() on each object contained in an array of generic Devices — and the actual method being executed may behave differently (since the method may be provided by an object of the class Scanner, Printer, or AllInOne);

  • in Example 59 and Example 58, there is a loop that calls the method .getSound() on each object contained in an array of generic Pets — and the actual method being executed may behave differently (since the method may be provided by an object of the class Dog, Cat, or Snake).

This polymorphic behaviour takes place because when we call an object method, Java checks what is the actual class of the object while the program is running, and chooses (i.e., “binds”) the “most specialised” overridden method depending on the actual class of that object.

This technique (i.e. choosing the method to call when the program runs) is called late binding or dynamic binding, because it takes place “late” and “dynamically” while the program runs. This enables subtype polymorphism, as we have seen thus far.

However, late binding in Java is only used when calling object methods — and is not used in other situations: in fact, the default behaviour of Java is to choose (“bind”) which method to call “early”, at compile time, depending on the type of the method arguments. This technique is called early binding or static binding or compile-time binding, because it takes place “early” and “statically” when the program is compiled, before it is executed. This is illustrated in Example 66 below.

Example 66 (Late binding vs. early binding in action)

Important

You can (and should!) try the code below on the Java shell – or you should write and execute this code in some Java files, to see late and early binding in action.

Consider the code below: it contains a simplified version of our Pet class hierarchy, where the object method .printDescription() is specialised (i.e. overridden) in the subclass Dog.

 1class Pet {
 2    public String name;
 3
 4    public Pet(String name) {
 5        this.name = name;
 6    }
 7
 8    public void printDescription() {
 9        System.out.println("Generic pet called " + this.name);
10    }
11}
12
13class Dog extends Pet{
14    public Dog(String name) {
15        super(name);
16    }
17
18    @Override
19    public void printDescription() {
20        System.out.println("Doggo called " + this.name);
21    }
22}

Now, let us create an array of Pet objects, containing an object of the class Pet and an object of the class Dog:

var dog = new Dog("Mrs Wolf");
var pet = new Pet("Wilson");
var pets = new Pet[] { dog, pet };

Let us now write the loop below, that that goes through each element p of the array pets and calls the method p.printDescription().

for (var p: pets) {
    p.printDescription();
}

When this loop runs, Java choses the “most specialised” method for p — thanks to late binding. More precisely:

  • if the object referenced by p belongs to the class Dog, then the overridden object method .printDescription() (on lines 19–21 above) is called; otherwise,

  • if the object referenced by p belongs to the class Pet, then the generic object method .printDescription() (on lines 8–10 above) is called.

Consequently, the output of the loop above is:

Doggo called Mrs Wolf
Generic pet called Wilson

Importantly, variable p in the loop body has type Pet, but the behaviour of p.printDescription() changes depending on the specific subclass of the object referred by p while the program runs: this is called subtype polymorphism.

Now, let us define two static methods that print information about a dog or a pet, similarly to the object method .printDescription() above. We call both static methods printInfo(...), but notice that they take an argument of a different type (technically, we are overloading the method name).

static void printInfo(Pet pet) {
    System.out.println("Generic pet called " + pet.name);
}

static void printInfo(Dog dog) {
    System.out.println("Doggo called " + dog.name);
}

Now, if we call printInfo(pet), then Java selects which static method to execute based on the type of the argument given to the call (which is the variable pet, of type Pet). This is early (a.k.a. “static”) binding. Therefore, we get the output:

Generic pet called Wilson

Similarly, if we call printInfo(dog), then Java again selects which static method to execute based on the type of the argument given to the call (which is now the variable dog, of the Dog). Therefore, we get the output:

Doggo called Mrs Wolf

So far, the behaviour of printInfo(pet) and printInfo(dog) is similar to what we get by calling the object methods pet.printDescription() and dog.printDescription(). But what happens if we write a loop that calls the static method printInfo(p), where p is an element of the array pets?

for (var p: pets) {
    printInfo(p);
}

Although this loop may appear similar to the previous loop (which calls p.printDescription()), the output of this loop is different:

Generic pet called Mrs Wolf
Generic pet called Wilson

This happens because, in the body of the loop, printInfo(p) is called with the argument p, which is a variable of type Pet. Java uses this type information to select “early” (and “statically” at compile-time, i.e. before the loop runs) which method to call, and chooses the method that takes an argument of type Pet. This choice (“binding”) of the method ignores the fact that, while the loop runs, p may refer to an object that belongs to the more specific class Dog.

This last loop shows the difference between early binding and late binding — and also shows that, in Java, subtype polymorphism is only used when calling object methods.

Concluding Remarks#

You should now have an understanding of three main topics:

These topics (especially the last two) are quite technical, and may not be relevant for all Java programs. However, being aware of them will be helpful for understanding where a Java program is storing its data (using class fields vs. object fields), or why a Java program may seem to run by executing the “wrong” method (because maybe it is using early binding while we expect late binding).

References and Further Readings#

You are invited to read the following sections of the reference book: (Note: they might mention Java features that we will address later in the course)

  • Section 2.2 - “Variables” and “Constants” (for the modifier final)

  • Section 7.3 - “Static Class Members”

  • Section 10.1 - “Late Binding”

  • Section 10.2 - “Polymorphism via Inheritance”

  • Section 10.3 - “Polymorphism via Interfaces”

Exercises#

Note

These exercises are not part of the course assessment. They are provided to help you self-test your understanding.

Exercise 32 (Experimenting with final local variables and fields)

Have a look at your solutions to previous weekly assessments (or the solutions provided by the teacher), and consider: could you mark some of the local variables and object fields as final, as in Example 62? What would the code look like, if you use final whenever possible?

In general, using final (when possible) makes the code more robust — but it may also make the code more cluttered and harder to read. Finding the best compromise between robustness and readability requires some practice, and it is often a matter of personal taste.

Lab and Weekly Assessments#

During the lab you can try the exercises above or begin your work on the weekly assessments below.

Important

05 - Counters#

In this assessment you will implement two kinds of counters:

  • a Counter class for creating objects that maintain their own internal counter value, and provide (non-static) methods to manipulate that value; and

  • a StaticCounter class, which maintains a “global” counter value, and provides static methods to manipulate it.

Note that all the fields (if any) of the classes Counter and StaticCounter must be private.

First, edit the file Counter.java provided in the handout, and implement the class Counter with the following requirements.

  • The class Counter must provide the constructor:

    public Counter(int initial)
    

    which initialises a new counter object with the given initial value.

  • The class Counter must provide the following methods:

      public int getValue()
      public void setValue(int value)
    

    which respectively retrieve the current value of this counter, or set the current value of this counter with the given value value.

  • The class Counter must provide the following methods:

      public void increment()
      public void decrement()
    

    which respectively increment or decrement the current value of this counter by 1.

Then, edit the file Counter.java provided in the handout, and add the implementation of the class StaticCounter, which maintains a “global” counter that has the initial value 0, and can be read and changed using the static methods below.

  • The class StaticCounter must provide the following static methods:

      public static int getValue()
      public static void setValue(int value)
    

    which respectively retrieve the current value of the “global” counter, or set the current value of the counter with the given new value.

  • The class StaticCounter must provide the following static methods:

      public static void increment()
      public static void decrement()
    

    which respectively increment or decrement the current value of the “global” counter by 1.

When you are done, submit the modified file Counter.java on DTU Autolab.

The handout includes some Java files called Test01.java, Test02.java, etc.: they are test programs that use the code you should write in Counter.java, and they might not compile or work correctly until you complete your work. You should read those test programs, try to run them, and also run ./grade to see their expected outputs — but you must not modify those files.

Hint

  • For the class Counter, you need to include a private field that stores the current value of the counter.

  • For the class StaticCounter, you need to include a private static field that stores the current value of the counter.

06 - Video Game Monsters, Part 4#

Note

This assessment can be solved using the contents introduced in previous Modules. It is not directly connected to the new topics introduced in this module.

Important

For this assessment you must submit two files: Monster.java and GameUtils.java. For the submission instructions, see the note at the end of this assessment.

This is a follow-up to the assessment 05 - Video Game Monsters, Part 3, and the starting point is its solution, i.e. the files Monster.java and GameUtils.java (you can use either your own files, or the ones provided by the teacher as a solution to 05 - Video Game Monsters, Part 3).

The development of the video game has reached the stage of testing some elements of the gameplay: controlling a player on the playground.

First, you will need to modify the static method displayPlayground(...) of the class GameUtils (in the file GameUtils.java), and change it into:

public static void displayPlayground(Monster[][] playground, int pRow, int pCol)

The updated static method must produce the same output required in 05 - Video Game Monsters, Part 3 — but in addition, the updated method must also show the player’s position, by printing an X at row pRow and column pCol of the playground.

Then, you will need to add the following static methods to the class GameUtils in the file GameUtils.java.

  • The class GameUtils must provide the static method:

    public static boolean playerWins(Monster[][] playground)
    

    This static method must return true if all monsters in the given playground are dead. Otherwise, the method must return false.

  • The class GameUtils must provide the static method:

    public static boolean playerEaten(Monster[][] playground,
                                      int pRow, int pCol)
    

    This static method must return true if the playground contains a monster at the player’s row pRow and column pCol, and that monster is not dead. Otherwise, the method must return false.

  • The class GameUtils must provide the static method:

    public static void playGame(Monster[][] playground)
    

    This static method must implement the game loop: it must keep track of the current player position (starting at row 0 and column 0), and read whole lines of input from the terminal, where each line contains a command for controlling the game. The supported commands and their effect are:

    • show - This command causes playGame(...) to display the game playground and information about the monsters, as produced by the methods GameUtils.displayPlayground(...) and GameUtils.displayMonsters(...).

    • up, down, left, right - These commands cause playGame(...) to change the player’s current row and column by one cell in the required direction.

    • hit - This command makes playGame(...) apply 100 points of hit damage at the current player position, by using the method GameUtils.hit(...).

    • burn - This command makes playGame(...) apply 100 points of burning damage at the current player position, by using the method GameUtils.burn(...).

    After reading and executing one of the commands above, the static method playGame(...) must check whether the player has been eaten or has won. If the player has not been eaten and has not won, the game loop repeats, by reading and executing the next command. Otherwise:

    • If the player has been eaten, then playGame(...) must print a message like the following, explaining which monster is responsible (using the Monster’s method getDescription()):

      You were eaten by Blackfur (owlbear; health: 59)
      

      After printing the output above, playGame(...) must return to the caller.

    • If the player has won, then playGame(...) must print You won!, followed by the output of GameUtils.displayMonsters(...) (showing that all monsters are now dead). Therefore, the output may look like:

      You won!
      Row 1, column 1: Mashface (wumpus; health: 0)
      Row 3, column 2: Hoothowl (owlbear; health: 0)
      

      After printing the output above, playGame(...) must return to the caller.

    Note

    To see your implementation of playGame(...) in action, you can compile all .java files in the handout folder (by executing javac *.java in the terminal), and then run java Test12. The game will looks as follows: (the highlighted lines are the commands written on the terminal by the user, while the other lines are the game output)

    show
    X....
    .W...
    .....
    ..O..
    .....
    Row 1, column 1: Mashface (wumpus; health: 20)
    Row 3, column 2: Hoothowl (owlbear; health: 59)
    hit
    show
    X....
    .W...
    .....
    ..O..
    .....
    Row 1, column 1: Mashface (wumpus; health: 0)
    Row 3, column 2: Hoothowl (owlbear; health: 59)
    right
    down
    show
    .....
    .X...
    .....
    ..O..
    .....
    Row 1, column 1: Mashface (wumpus; health: 0)
    Row 3, column 2: Hoothowl (owlbear; health: 59)
    right
    right
    down
    down
    show
    .....
    .W...
    .....
    ..OX.
    .....
    Row 1, column 1: Mashface (wumpus; health: 0)
    Row 3, column 2: Hoothowl (owlbear; health: 59)
    left
    You were eaten by Hoothowl (owlbear; health: 59)
    

Note

For this assessment you need to prepare and submit a ZIP file containing your modified versions of Monster.java and GameUtils.java. To prepare that ZIP file, you can simply execute from the terminal (inside the handout directory):

./grade -z

This command will grade your work and prepare a ZIP file that you can then submit on DTU Autolab.

Hint

  • When solving this assessment, you should not need to change the file Monster.java developed for 05 - Video Game Monsters, Part 3 (i.e. you should be able to just copy&paste and reuse the file Monster.java from the previous solution, and then submit it as-is).

  • When implementing GameUtils.playGame(...), using a switch statement may simplify your work.