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. Usingfinal
can prevent subtle bugs, because any attempt to “accidentally” modify the value of afinal
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.
(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 inmain
asfinal
: if we do so, any attempt to assign a value to afinal
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
andi
) whose value is changed after their declaration. These variables cannot be marked asfinal
— because the program actually needs to modify them to perform its tasks. However, the static methodsum
also has a variable calledn
(used for the method’s argument), and the value ofn
should not be modified whensum
is being executed: to prevent this, we can mark the argumentn
asfinal
.
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}
(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:
object methods (a.k.a. non-static methods, or simply methods), and
static methods (a.k.a. class 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) methodgetDescription()
; hence, ifpet
is an object of typePet
, then we can callpet.getDescription()
. (Correspondingly, the body ofgetDescription()
can use the special variablethis
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 writingsum(n1)
andsum(n2)
(in the body of the methodmain
). (Correspondingly, the body ofsum(..)
cannot use the special variablethis
, becausesum(...)
is not an object method.)Note
More verbosely, in the static method
main
in Example 62 we could have called the static methodsum(...)
by explicitly writing the name of the class that contains it — i.e. we could have writtenTwoSums.sum(n1)
andTwoSums.sum(n2)
. Also in this case, the static methodsum(...)
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
andpet2
are objects of the classPet
, then they both have a field calledname
(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"
andpet2.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.
(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
).
(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 genericDevice
s — and the actual method being executed may behave differently (since the method may be provided by an object of the classScanner
,Printer
, orAllInOne
);in Example 59 and Example 58, there is a loop that calls the method
.getSound()
on each object contained in an array of genericPet
s — and the actual method being executed may behave differently (since the method may be provided by an object of the classDog
,Cat
, orSnake
).
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.
(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 classDog
, then the overridden object method.printDescription()
(on lines 19–21 above) is called; otherwise,if the object referenced by
p
belongs to the classPet
, 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:
using the “final” keyword to forbid changing the value of a variable or field;
using static class fields to store “global” information, and
the connection between subtype polymorphism and late binding, and the difference with early binding.
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.
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
For each assessment, you can download the corresponding handout and submit your solution on DTU Autolab: https://autolab.compute.dtu.dk/courses/02312-E24.
For details on how to use Autolab and the assessment handouts, and how to submit your solutions, please read these instructions.
If you have troubles, you can get help from the teacher and TAs.
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; anda
StaticCounter
class, which maintains a “global” counter value, and providesstatic
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 ofthis
counter with the given valuevalue
.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 aprivate
field that stores the current value of the counter.For the class
StaticCounter
, you need to include aprivate
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 givenplayground
are dead. Otherwise, the method must returnfalse
.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 theplayground
contains a monster at the player’s rowpRow
and columnpCol
, and that monster is not dead. Otherwise, the method must returnfalse
.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 causesplayGame(...)
to display the game playground and information about the monsters, as produced by the methodsGameUtils.displayPlayground(...)
andGameUtils.displayMonsters(...)
.up
,down
,left
,right
- These commands causeplayGame(...)
to change the player’s current row and column by one cell in the required direction.hit
- This command makesplayGame(...)
apply 100 points of hit damage at the current player position, by using the methodGameUtils.hit(...)
.burn
- This command makesplayGame(...)
apply 100 points of burning damage at the current player position, by using the methodGameUtils.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 theMonster
’s methodgetDescription()
):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 printYou won!
, followed by the output ofGameUtils.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 executingjavac *.java
in the terminal), and then runjava 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 fileMonster.java
from the previous solution, and then submit it as-is).When implementing
GameUtils.playGame(...)
, using a switch statement may simplify your work.