Competence: I understand what abstraction is and how I can use the keyword abstract
and interfaces.
Competence: I can appropriately create variables, methods, and classes using the keyword final
.
In recent weeks, with the help of inheritance, you have ensured that you can prevent code duplication for certain shared functionality (e.g. a method ‘getName()’, in the case of teachers and students) by creating methods in so-called superclasses (e.g. the Person
class in this case). In most cases, this is fairly doable: all attributes you use in your methods for are in the same class (e.g. for getters and setters) so implementation is easy. Sometimes, however, a “standard implementation” is not easy to provide. In these situations, we have so far chosen to return dummy values, such as “-1”, an empty String (or we threw an exception). However, as you might have already figured out, this approach is risky and prone to introducing errors in case if you forget to override that “default behaviour”.
This week we are going to solve this issue by looking at the last part of inheritance: how do you deal with classes that have methods that you can only implement in subclasses? For example when a calculation that you need to do, might differ from subclass to subclass. (Think of the example of the Shape class of recent weeks.) This week we will learn how to use abstract
methods (or classes), by which we want to say that a method “needs to be completed” at a later moment (e.g. in the subclasses). Using this approach has multiple benefits. First of all: we nog longer depend on dummy values, but also secondly: we can force certain classes to implement certain methods (e.g. we can have all “Teacher” instances and their respective subclasses implement the “getTeacherCode()” method, even though the generation of this code might differ between subtypes of teachers).
This week we will also look at how we accomplish some form of multiple inheritance in Java. Multiple inhertiance means that a class can have more than 1 parent class (and thus has more types). Java unfortunately does not support this (- it’s not possible to mention multiple classes after the word ‘extends’ -), however it is possible to give classes multiple other types, not using inheritance. Java uses interfaces for this purpose, which allows us to extend the polymorphism we have talked about before. This week we’re going to see how interfaces work, what you can do with it and what impact this has on your code.
Finally, there is another topic that we have ignored throughout all programming modules: making something immutable. Almost from day 1 we have taught you how to create variables, how to store values in them and that you encapsulate them to prevent invalid values from appearing in a variable. However, you can also indicate that a variable should not change its value at all. Or that you are not allowed to overwrite a method during inheritance, or that you can’t inherit from a class at all. It may sound like these topics are quite different, but within Java all these 3 topics are linked together by the same keyword: final
. This week we’re going to learn what this keyword means and what you can do with it.
However, let’s start at the beginning and first focus on abstraction and the use of interfaces.
When implementing solutions that use inheritance, you may have noticed that giving a “standard implementation” is sometimes difficult. Take, for example, the Shape
class that you have made before. (We show our version here, your version may differ slightly, but for this example that should not matter.)
public class Shape {
protected int x;
protected int y;
private Color fillColor;
public Shape(int x, int y, Color fillColor) {
this.x = x;
this.y = y;
this.fillColor = fillColor;
}
public double getArea() {
return -1; //We don't know how to calculate this yet!
}
public double getCircumference() {
return -1; //We don't know how to calculate this yet!
}
}
Note that the methods getArea()
and getCircumference()
have a problem within the class Shape
: since this class represents a general shape, we cannot calculate these values. Circumference and area calculation differs between Rectangle
s and Circle
s. However, since we want all shapes to provide this calculation functionality, we have so far always returned an “invalid” dummy value in the getArea()
and getCircumference()
methods, such as “-1”.
By creating a class Shape
you have created a class that does not represent anything (but just holds some code). You have to wonder whether or not instances of this type make sense. You have the classes Rectangle
and Circle
which can be implemented (and actually do make sense), but what about the class Shape
? What does a Shape
represent? And how do you calculate it’s area? The conclusion is mostlike that you do not want Shape
objects to exist in your solution. And that’s exactly what you can achieve using the keyword abstract
.
You can use the keyword abstract
in multiple places in your code: when defining a class or by defining a method.
By adding the word abstract
to your class definition, you indicate that the class is not meant to be instantiated (in other words: you cannot turn them into objects). You mainly use abstract classes as basis for inheritance; they therefore contain code (variables and methods) that you want to use more often, but never come as individual objects (instances) in your program.
Abstracting a class is very easy. You only need to add 1 word for this to a class definition:
public abstract class Shape {
//omitted rest of the example
}
Note that the key word abstract
is an addition to the definition and not a substitute for other words. So you can always add the word abstract
to your class definitions.
By making the Shape
class abstract, you will now immediately receive an error message when you try to create an object:
In this example of the Shape
class, it may make sense to use abstract
, because the class tries to describe something that has no real value on it’s own (what is a shape?).
However, it is also very common to make classes abstract
if you just don’t want those objects to exist. Take for example, the case of Saxion and its student administration. So far, we’ve seen how Student
and Teacher
objects have been created and used, both inheriting from the Person
class. Instances of this Person
class however, might be undesireable in your program and thus even though the Person
class is fully implemented (and implemented well), you will often see that this class is made abstract
. This is done intentionally: the programmer does not want separate Person
instances to be created. The Saxion administration system knows of students and teachers (and perhaps other employees), but normal persons do not serve any purpose in the system! And so declaring the Person
class to be abstract is logical: we want to have this class to prevent code duplication, but we don’t want instances to be used in our program. In short: abstract allows you to “disable” a class for instantiation.
In addition to abstract classes, you also have abstract methods. The next paragraph deals with this.
An abstract method is a method without implementation. However, as methods cannot be empty, we need to provide an implementation somewhere and thus making an individual method abstract
causes subclasses to be required to implement certain functionality. In the case of the Shape
class, it makes sense to make the getArea()
methods and getCircumference()
abstract: after all, the Shape
class cannot give a good implementation for this and therefore we returned a dummy value, so we might as well not give an implementation at all and force subclasses to implement this method for us!
We do this by making these methods abstract
. Here’s what it looks like:
public abstract class Shape {
protected int x;
protected int y;
private Color fillColor;
public Shape(int x, int y, Color fillColor) {
this.x = x;
this.y = y;
this.fillColor = fillColor;
}
public abstract double getArea(); // NOTICE THIS LINE!
public abstract double getCircumference(); // NOTICE THIS LINE!
// ...
}
Especially look at the methods getArea()
and getCircumference()
. By adding the word abstract
you declare that the method is not going to be implemented in this class, but requires a subclass to do so. In fact, as soon as you make a method abstract, it is even impossible to give any implementation. If you do try this, you will immediately receive
an error message:
However, at some point abstract methods need to get an implementation. And this is what you normally do in a subclass, such as in the Circle
class which is a subclass of the Shape
class.
You will see that IntelliJ also gives you a hint for this when you want to implement a subclass:
(Note: the error message/ hint indicates that you ONLY need to implement getArea()
, but after you have done this, the same error is shown for the getCircumference()
method.)
It is also important to know that you can’t have abstract methods in a class that isn’t abstract itself (the error message above gave that suggestion as well). If the Circle
class does not (or cannot) give an implementation to abstract methods, the class just isn’t fully implemented (or “finished”) and thus remains abstract.
However, you can choose to push the implementation of the method “further down” the inheritance tree by e.g. also making the Circle
class abstract, but ultimately there must be an implementation for all abstract methods. Java does not allow you to instantiate a class of which methods are unimplemented.
The choice whether or not to make a class or method abstract
is something you have to think about every time. We encourage you to use the following guidelines:
abstract
.abstract
.An additional effect of using abstract
methods is that you can now make methods mandatory to implement. We already discussed the example for the Shape
class. This class ensured that there must be a method getArea()
and a method getCircumference()
, but did not give an implementation for this itself. This implementation had to be written in a subclass, so any class that wanted to be a subclass of Shape
needed either to implement these methods (or become abstract themselves).
This principle, which you can enforce which functionality (methods) should be implemented, in programming is also widely used in polymorphism. It’s best to imagine this as follows: Imagine a class where you model something that has a weight. A human being, an animal, a book, etc. A lot of things have a weight and can therefore be “weighable”. Implementing a method getWeight()
therefore does not sound so crazy for all these classes and thus you might consider implementing this in any superclass.
So here too we could propose a class WeighableObject
that we might implement with some attributes, getters, etc. But what if the class Book
already inherits from another class (e.g. LibraryObject
) and it wouldn’t make sense at all to add the getWeight()
method to this superclass? How can we then enforce the implementation of this method?
The problem lies in the fact that you actually want to inherit from multiple classes. And unfortunately, in Java this is not possible. Java however uses interfaces to resolve this issue. An interface is an abstract class that has exclusively abstract methods and nothing else; so no (non-static) variables, constructors, etc. In fact, an interface holds nothing more than a list of “unimplemented” methods. Interfaces are therefore designed to dictate what type of methods should be implemented. An object that is given a certain interface, has to implement that interface.
Check out the following interface for the aforementioned weight measurement issue:
public interface WeighableObject {
double getWeight();
}
(Please note that nothing has been omitted from this class. This is the entire file WeighableObject.java
.)
In terms of syntax, you should read this interface as: All classes that are of the WeighableObject
type must have a method getWeight()
that returns a double. Think of an interface as a new type declaration: we can now give each class an extra type (and therefore grouping it with other classes with that type) by requiring a class to have a number of methods to implement. (For the record: Interfaces can also have no methods!)
Back to our example of the Person
class. Suppose we want to make this class also “weighable”, meaning that it should implement the requirements set by the WeighableObject
interface. As the interface only has one method requirement, we can easily do so by implementing the getWeight()
method. (So the Person
class can also become an instance of WeighableObject
, only if it implements the getWeight()
method.) In Java you enforce this by using the keyword implements
in the class definition. This looks like this:
public class Person implements WeighableObject {
private int weight;
//Common stuff like constructors, getters, etc..
public double getWeight() { // <-- This method is now MANDATORY because Person also wants to be of type "WeighableObject".
return weight;
}
}
Note that in Java we are only allowed to use extends
once per (sub)class, but you can implement multiple interfaces at the same time. (You can also combine both “extends” and “implements” at the same time!)
Let’s consider another example from earlier weeks: The ‘Train’ class, which we are now also going to turn into a WeighableObject
:
public class Train implements WeighableObject {
public double getWeight() { // <-- Once again this method is MANDATORY to implement.
//Let's assume each compartment weighs 500 kg.. and each passenger weighs (on average) 80 kg.
return 500 * nrOfCompartments + getNrOfPassengers() * 80;
}
}
Because Train
and Person
now both implement the WeighableObject
interface, you can treat them in the same way in your code as you would use superclasses. The only difference lies in the fact that these classes do not share a similar parent (other than Object
). An interface is a completely “stand-alone” something of the regular inheritance structure you have seen so far.
We finish this introduction of interfaces with a code example, in which you can see what their use looks like:
ArrayList<WeighableObject> weighableObjects = new ArrayList<>();
Person somePerson = new Person(...);
Train demoTrain = new Train(...);
//Omitted code to fill the train.
weighableObjects.add(somePerson);
weighableObjects.add(demoTrain);
for(WeighableObject w : weighableOBjects) {
System.out.println(w.getWeight()); //Note that we can ONLY use "getWeight()". There is no other method in a WeighableObject.
}
This example might be a bit unusual (it really is!), but it shows how to group classes that initially may not seem to have a similar base (which they still do not have!). And as you learned last week, being able to group types (so you can approach them in the same way) is very nice! After all, one of the most important things when writing code is the prevention of code duplication. And using interfaces helps doing so.
You will see more uses for interfaces in the future, but for now we will not dive into too much detail. However, it is wise to look at some “default” interfaces that Java already offers (and uses!). And probably the best known example of an interface in Java is the Comparable
interface. So let’s take a closer look at this interface.
The Comparable
interface in Java is used to make classes comparable. Now you might think: I can already compare objects, but in recent weeks we have mostly compared some internal attributes with each other (e.g. last names, student numbers, etc.). However, with the help of the Comparable
interface, we can teach Java to know when two entire objects are equal, or when one is less than the other, etc. And that is very useful if we want to sort (put in order) objects. Consider the following code:
ArrayList<Person> listOfPeople = readFromCSV("persons.csv");
Collections.sort(listOfPeople); //You might have seen/used this already, but this is the official way Java can sort a list for you!
//The list is now sorted.. but we just don't know how!
for(Person p : listOfPeople) {
System.out.println(p);
}
Java already has enough functionality to sort lists itself, but needs to know which attribute you want to sort by. Take e.g. a Person
class with the usual attributes (first name, last name, age, etc.). If we use Java to sort the ArrayList<Person> persons
, Java will not know how to do this: should we look at the first name, last name, age …?
And that problem is resolved by implementing the Comparable
interface.
The Comparable
interface in Java consists of just one method, so to make something comparable we only need to implement this one compareTo(T o)
method.
(Click on the image to go to the official Java documentation page!)
The T
mentioned here as the type of argument is short for “type”. The Comparable
interface is written in such a way
that it actually works for all types in Java. In this case, we also speak of the fact that Comparable
is generic. Generic classes fall outside the scope of this course so we’ll only use them. And although this might sound new, you have been using generic classes since the course Introduction to Programming: the ArrayList is also a generic class. The one thing to remember when using generic classes is that you need to specify the type that you want between the <..>
characters (a.k.a. diamond operators). This same principle also applies to the Comparable
interface.
It’s easiest to just show this. Let’s make the Person
class Comparable
first:
public class Person implements Comparable<Person> { // <-- Note the <Person> part. We can compare one Person to another!
//Omitted load of attributes you already know..
public int compareTo(Person p) { // <-- Because we said on line 1 that we are creating a comparison between “Person”’s, the T can now be replaced by “Person"!
//implement this.
}
}
The compareTo(..)
method needs to return an int
value based on whether the compared object is less than, equal or more than the other. This sounds perhaps complicated, but it’s not so bad. Any negative numbers means that this object is less than the other object, or better: if you were to sort these items, this object should go in front of the object it’s being compared to. If a positive number is returned, it means this object should go after the compared object. Finally, a result of exactly “0” means that the objects are equal.
Let’s look at an example by “organizing” people based on age. A possible implementation may then be:
//Assume the person has an integer "age"
public int compareTo(Person otherPerson) {
//You are allowed to look at the other person's private age attribute because it's the same class!
if(age < otherPerson.age) {
return -1;
}
if(age > otherPerson.age) {
return 1;
}
return 0;
}
The reason why compareTo
returns an int is because there are 3 possible options (and there is no other data type that is smaller than an integer that can do this easily). However, in this case, it also offers an opportunity to write the above code a lot shorter, since it does not matter what value is returned exactly, as long as it is “negative”, 0 or “positive”.
The following code is therefore similar in functionality to the previous code:
public int compareTo(Person otherPerson) {
return age - otherPerson.age; //Just think about this for a second..
}
Of course you also want to be able to compare on other attributes, e.g. by name (String). Fortunately, the Comparable
interface is also implemented by many other classes (in Java) and the String
class is an example of one of these implemented classes. So if we want to adjust our code and compare by last name this looks like this:
public int compareTo(Person otherPerson) {
return lastName.compareTo(otherPerson.lastName); //Let's compare on last name basis!
}
(Note that we just use the compareTo(String)
implementation that is default in Java to compare Strings. This will result a “normal” alphabetical comparison as you might expect!)
Of course, there are many more examples to come up with. Students can be sorted by student number, teachers by teacher code and so on! The lesson you should take with you, especially from this example, is that by implementing the interface Comparable
the Person
class (or any of its subclasses) must provide an implementation of the compareTo
method. Doing so increases the functionality (and responsibility) of this class, which in turn increases the reusability of this class.
Finally, we will look at the keyword final
and what this can mean for your code. In short: with final
you can indicate that something shouldn’t be able to change anymore. For variables we can use this to create so-called “constants”, which you may remember in mathematics, such as π (3.14159265…).
But in addition to variables, you can also make methods and classes final
, which means something else. Today we will list all forms of final
and show the differences between all the different forms. We will start at the “highest level” by looking at what happens when you make a class final
. Then we will make a method final
and finally we’ll look at regular variables.
By adding the word final
to a class definition, you say that you are not supposed to extend this class, i.e. this
is the “last” class in this branch of the inheritance.
Take the following example where you create a BankAccount class:
public final class BankAccount {
//Skipped details..
}
The word final
ensures that you can never inherit from this class again. If you try doing this, the compiler will throw an error message on this:
Of course, you have to think about whether you really want to limit inheritance. One of the most important principles of good object-oriented programming is that you can expand your system by creating new classes, based on some other class. And you can prevent this extension by just adding one little word to the class definition. We don’t expect you to make entire classes final
that often, but it’s good that you know it’s possible!
By making a method final
, you can prevent a specific method from being ‘overridden’ by subclasses. Where we just wrote that you will not often make an entire class final
, you will see that methods regularly become final
. This is done to prevent unwanted behavior from occurring due to inheritance.
Let’s look into this with an example. Take the following class and method:
public class BankAccount {
public boolean checkPin(String pinNumber) {
//Do some fancy checks..
}
//Omitted rest..
}
Due to inheritance, it is now possible to create a new subtype of BankAccount
, overriding the checkPin(String)
method.
This may mean that the following implementation is possible:
public class MyHackedAccount extends BankACcount {
public boolean checkPin(String pinNumber) {
return true;
}
//Omitted rest..
}
By overriding the checkPin(String)
method, you may ignore all the protections provided in the BankAccount
class that are included with regards to checking the pin of an account. It is therefore a good idea to make the method checkPin(String)
final
in the BankAccount class, so that it cannot be overwritten. You do this, by adding the word final
:
public class BankAccount {
public final boolean checkPin(String pinNumber) {
//Do some fancy checks..
}
//Omitted rest..
}
Now, if you try to override the checkPin(String)
method in a subclass, you will receive an error message. It is no longer allowed to override this method!
In short: You can use final
to make sure no-one messes up your programs (or internal state of your classes) by not allowing certain methods to be overriden.
Finally, there are final
variables. In the case of variables, the word final
indicates that a value of a variable may not be changed once you have given it a value. You use final
for example, to record the maximum capacity of a bus or aircraft.
To understand how final
works on variables, it is important to distinguish between primitive variables and reference based variables. Primitive types are the types you always write in lowercase (such as int, double, boolean, etc.) and are stored on the stack while reference types (String, SaxionApp, Person, any other class you’ve written yourself so far) are always capitalized and are created on the heap.
A final
primitive variable is also often called a “constant” and is, of course, the exact opposite of a variable. The value can no longer be modified. This usually looks like this:
private final int MAX_PASSENGERS = 3; // final <type> <name>;
The first thing you will probably notice (besides the word final
of course) is that we suddenly capitalize the name of the variable. This is because there is an documentation convention (followed by most programming languages) that constant (primitive) values should always be capitalized. This way, it is easy to tell from the name of a variable that it is a constant.
If you add the word final
to a variable declaration, the compiler checks that you are not trying to modify the variable somewhere else in your program. If you do, you get an error message:
You do not have to give a final
variable a value immediately, as shown in the following example:
private final int MAX_PASSENGERS;
public Bus(int maxNrOfPassengers){
this.MAX_PASSENGERS = maxNrOfPassengers;
}
Note that after the value has been placed in the constructor, it can no longer be adjusted. So you can only set a value for this variable once.
By using final
variables you can make your code clearer, so that it becomes easier for other programmers to read. In addition, this makes the code more maintainable. We therefore want to encourage you declare variables final
if you are sure they no longer require any modification (or they are not supposed to change!).
The best way to demonstrate this is by means of an example: The legal age when you are allowed to drink alcohol in the Netherlands is 18. You are also allowed to obtain your drivers license when you are 18. For Dutch law, the number “18” is therefore important as it declares your “legal age” (or better: when you are seen as an adult). It is therefore useful to create a constant in a Person
class to store this kind of information.
Study the following (simple) example:
public class Person {
private final int LEGAL_AGE = 18;
private int age; //Is set by a constructor..
//Omitted other code.
public boolean canDrinkAlcohol() {
return age >= LEGAL_AGE;
}
}
Note especially that because we now have the “official age at which you are an adult” stored in a separate variable it is now easier for us to adjust it. If it is ever decided that the legal age should be set to 21 (or 16, etc.), we only need to adjust this once in our code. Otherwise, you had to manually search the code to see if “18” is used anywhere and replace these values. And let’s not mention the fact that some smart guy perhaps crammed a line somewhere with return age < 19;
which meant searching for just the value 18 was not enough…
When you start looking at the use of the word final
in variables that refer to an instance (reference type), it is a little trickier to see exactly what is and is not allowed. The best description we can give is that by making a variable (of a reference type) final, this ensures that the variable may no longer refer to another object. There is also something remarkable about using capital letters to generate constants.
Let’s look at this right away in the form of an example. Assume that the class Person
represents a simple person as you are used to by now:
public class Loan {
private final Person debtor; //a debtor is someone who OWES the money.
private String purpose;
private double amount;
public Loan(String purpose, double amount, Person debtor) {
this.purpose = purpose;
this.amount = amount;
this.debtor = debtor;
}
//Left out other code, such as decreasing/adding money to the loan etc.
}
Note: Unlike primitive types, final
attributes with a reference type are never capitalized unless the attribute is also `static`. So static final
attributes (regardless of whether they are private, public or protected) you should capitalize while in all other cases you do not.
In the preceding example, we showed you a class Loan
that represents a loan. A loan is taken out by a person (the debtor) for a specific reason, such as buying a house, car, etc. and that person always remains responsible for that loan. So being able to just link the Loan
to another debtor would be weird.
In such a case, you can therefore choose to make the reference to the Person
instance final
: thus saying that technically the loan cannot be transferred to another person.
However, this does not mean that the Person
instance cannot now be modified further. In principle, you can still call all methods / setters on the instance, so changing a mobile number, the age or last name is still possible, but no matter how you look at it: the loan remains linked to the same Person
instance.
So a final
variable to a reference type is not a trick to make an instance read-only or otherwise no longer modifiable. If you want to achieve such a thing you will have to look at the attributes of the class Person
to see if you can make it final (or remove setters). So the word final
in this case only refers to the variable containing the reference to the object: you are not allowed to remove or replace the connection to that object.
The code below therefore results in an error message:
// Assume the constructor above is used. This code could go anywhere else.
// The following code is NOT allowed:
debtor = new Person(...);
//or
public void setDebtor(Person p) {
this.debtor = p;
}
The code below on the other hand, is allowed:
//This IS allowed and will cause the DEBTOR to change some internal attributes:
debtor.setMobileNumber("06-12345678");
//or
debtor.setLastName("I changed my name!");