1.3-Object-Georienteerd-Programmeren

Week 2: Nesting, overloading and basic error handling

Competency: I can create programs that use a class design with composition, and I am able to handle errors using exceptions.

Introduction

Last week we used the basics of encapsulation and saw, how by using getters, setters and constructors we can specify the classes responsibilities. This prevents classes from reaching an invalid state. As an example, we discussed the Person class where a negative age would not make sense and potentially break algorithms. By preventing direct access to the age attribute, we are now, as programmers, in control of all modifications that might occur.

Because we are now able to give classes their own responsibilities and we are (more) in control with regards to invalid states (as much) we can now create larger applications that make use of these classes. This week we are going to look at how to do this. We’re going to look at using classes in a larger context. We will also look at how error handling works and how a class can actually report a problem by throwing an exception.

Before we can get into this, however, we first need to take a look at another concept called overloading.

Overloading: Constructors and methods

Proper names for variables, methods and pretty much everything is hugely important in programming. It makes your code more readable and easier to use. Take for example the method printLine that you’ve been using frequently by now. Due to it’s name you can easily expect what it does (print a line). And it’s very versatile, as it accepts a range of arguments, such as an int, a double, a String, etc.

int number = 7;
String sentence = "Hello";

SaxionApp.printLine(number);
SaxionApp.printLine(sentence);

What you might not have thought about is how to create this printLine method if you wanted to write it yourself. As soon as you write a method, you have to specify which arguments are required and for all these arguments you must specify their type. So in order to implement the above functionality, you must have 2 different methods (as one would accept an integer and the other a String). Luckily for us, methods are allowed to have the same name as long they have different arguments, with the result that you can have the following methods in the same class:

public void printLine(int number) {...}
public void printLine(double number) {...}
public void printLine(String text) {...}
public void printLine(boolean someBoolean) {...}

This concept, where you have four methods with exactly the same name, but with different arguments is called method overloading. Method overloading is a concept in which you give a method (with similar functionality) the same name, as long as the type of the arguments differ.

We also call this formally that the signature (signature) of the method must be different: printLine(int) is different from printLine(double), which in turn is different from printLine(String), etc.

Implementing overloaded methods

If the name of a method is the same, you would expect the functionality of these methods to be is approximately the same. And you would be right… overloaded methods probably share much of certain functionality. Depending on the reason for which you want to apply method overloading you may just have to deal with a situation where code duplication seems unavoidable, but it certainly is not.

Let’s look at this through an example. Suppose you have created these overloaded methods:

public void printLine(String sentence) {
    // Do fancy stuff to get the sentence on the screen....
}

public void printLine(int number) {
    // Do highly similar fancy stuff to get the number on the screen...
}

Then, of course, you can choose to have one method use the other! This looks like this:

public void printLine(String sentence) {
    // Do fancy stuff to get the sentence on the screen....
}

public void printLine(int number) {
    String numberInStringFormat = "" + number; // Convert number from type int to a String
    printLine(numberInStringFormat); // Call the printLine method that takes a String and let it handle this printLine!
}

The big advantage of the above solution is that now you only have your “fancy code” in one location, namely in the printLine(String) method. So if there is a mistake in printLine(..) (or you want to change things), you only need to do this in one location. This kind of solution is very often used in programming and it is good to start thinking about it from now on: avoid code duplication!

Finally, one more example to show how overloading can be used in a slightly different setting:

public void print(Person p) {
    // Do something fancy... 
}

public void print(ArrayList<Person> persons) {
    for(Person p:persons) {
        print(p); // Invoke the print(Person) method
    }
}

This way you can e.g. combine a list of objects of a certain type, easily with the method that can only handle 1 instance at a time!

Overloaded constructors

In addition to methods, you can also overload constructors. This is useful if you (for example) want to include default values and do not always want to include them. Consider the following example:

public class TrainCompartment {
    private int numberOfSeats;
    private int numberOfSeatsInUse;

    public TrainCompartment(int numberOfSeats) {
        this.numberOfSeats = numberOfSeats;

        numberOfSeatsInUse = 0; // This line has a duplicate!
    }

    public TrainCompartment() {
        this.numberOfSeats = 50; // Default value!

        numberOfSeatsInUse = 0; // This line has a duplicate!
    }
}

By adding two constructors to this class, it has suddenly become possible to instantiate the class TrainCompartment in two ways, namely:

TrainCompartMent tc1 = new TrainCompartment();
TrainCompartMent tc2 = new TrainCompartment(100);

The difference, of course, is in what happens next to the internal state of TrainCompartment. tc1 will in this case have the default 50 seats, tc2 will be provided with 100 seats. So now the choice is up to the programmer: You may choose your own number of seats or opt for a default value!

The previous example still has room for improvement! This is because there is double code included which we still want to get rid of. (In this case it is only 1 line, but in the future there may be many more). In the case of constructors, to call another constructor you must use the word this(...). (This is because constructors don’t have actual names.)

This looks like this:

public class TrainCompartment {
    private int numberOfSeats;
    private int numberOfSeatsInUse;

    public TrainCompartment(int numberOfSeats) {
        this.numberOfSeats = numberOfSeats;

        numberOfSeatsInUse = 0; // This line WAS a duplicate!
    }

    public TrainCompartment() {
        this(50); // Invoke the other constructor that requires an integer, providing 50 (the default value) as an argument.
    }
}

This way we can easily overload constructors, without getting code duplication in the process!

Using composition

Because we can now give classes more responsibilities, we can also start thinking about a more complex structure of our programs. As an example, we are going to extend an assignment from last week (assignment 1, about a train). We are going to use the class TrainCompartment to write a complete class Train. After all, it is quite conceivable that if your passenger wants to “get on the train”, you may need to find a seat and for that you will need go through several TrainCompartments. A Train thus contains a list of TrainCompartments. In OOP, this relationship is also called composition.

In doing so, it is not inconceivable that the method enter() that you created earlier for the class TrainCompartment could also be useful for the entire train. After all, you usually want to have a seat “on the train” and not be limited exclusively to 1 train set. So we choose to include an enter() method in the class Train as well.

Consider the following Train implementation:

public class Train {

    private ArrayList<TrainCompartment> compartments;

    public Train(int nrOfCompartments) {
        compartments = new ArrayList<>();

        // Initialize compartments
        for (int i = 0; i < nrOfCompartments; i++) {
            TrainCompartment newCompartment = new TrainCompartment();

            compartments.add(newCompartment);
        }
    }

    public void enter() {
        for (TrainCompartment tc : compartments) {
            if (!tc.isFull()) { // If there is room in the compartment.
                tc.enter(); // Enter it.
                break; // Stop the loop: We found a seat!
            }
        }
    }

    // Omitted other code.
}

(Note that we did add a helper method isFull() in the class TrainCompartment that checks whether there is still room in this train compartment).

We now use the TrainCompartment class as a building block to create our entire Train. We now also speak of the fact that the TrainCompartment objects are now contained within the Train objects. The class Train is composed of one or more TrainCompartments, and has become dependent on this class.

By adding the enter() method to the Train class, we have provided some additional functionality based on pre-existing code. After all, the Train class has now become responsible for finding a place in the train where a passenger can take a seat and we no longer have to iterate over all compartments ourselves.

But what if there is no room in the train? And can a train also have no compartments? Still, things can go wrong and at the moment we solve these errors mainly by printing an error message to the user. But there is also a way in which we can deal with errors programmatically, a way in which we can indicate in our code that something is going wrong and then what to do to fix it.

For this, most object-oriented programming languages have come up with a similar solution: Exceptions.

Error handling: Exceptions

Exceptions, and exception handling, is the way in which within programming, errors are handled on a code-by-code basis. So no longer are we just going to print error messages to the user, but really make sure that our program also knows that an error has occurred. An exception is best seen as “a problem” that needs to be solved in a certain way or your application might crash. If a problem occurs somewhere, then an exception is thrown. After an exception is thrown, it must be resolved or your program will crash, also called catching an exception. “Throw” and “catch” are the terms you will encounter a lot here and in this chapter we will explain what they mean.

Throwing Exceptions

Let’s start with an example, using the TrainCompartment set up earlier. Take a look at the following code:

public class TrainCompartment {
    private int numberOfSeats;
    private int compartmentClass;
    private int numberOfSeatsInUse;

    // Cut out some code...

    public void enter() {
        if (numberOfSeatsInUse < numberOfSeats) {
            numberOfSeatsInUse++;
        } else {
            // Produce some kind of error!
            SaxionApp.printLine("The compartment is full!");
        }
    }

    // Cut out some more code...
}

In the above snippet, you can see the enter() method that checks if there is room in this traincompartment. If this is not the case, an error message will be displayed. For the user, this is certainly nice, but for your program, after the error message is printed, the enter() method will continue and actually it will appear as everything is okay: There is nothing problematic about this code!

If we also want our software to really “throw” an error that we as a programmer can respond to further, we have to throw an exception. Throwing an exception breaks the execution of a method (the method is not completed) and a message is sent to the invoking method. This looks like this:

public void enter(){
    if (numberOfSeatsInUse<numberOfSeats) {
        numberOfSeatsInUse++;
    } else {
        // Produce some kind of error!
        throw new IllegalStateException("There are no more seats available!");
    }
}

Note that the word throw is actually a keyword in Java, just like if and for, etc. The purpose of throw is to actually “throw” an instance of a exception class. An exception itself is really just a class within Java with several subtypes (we’ll come back to this later), but when you want to start throwing the exception, you throw a normal object just like Train and TrainCompartment. So the word new is mandatory to instantiate an exception. So you throw each exception in the following way: throw new <NameOfException>. The constructor of class IllegalStateException allows us to pass a message (in String form) so that it can be printed later. In addition, you never throw a standard exception, but always try to make it as specific as possible. We will come back to this later.

For this week’s assignments, you should use 2 possible exceptions:

A possible IllegalArgumentException is also hidden in our train example, just look at the constructor:

public Train(intOfCompartments) {
    compartments = new ArrayList<>();
  
    // Initialize compartments
    for(int i = 0; i < nrOfCompartments; i++) {
        TrainCompartment newCompartment = new TrainCompartment();
  
        compartments.add(newCompartment);
    }
}

If we decide that it is not allowed to have no compartments (so nrOfCompartments > 0 must apply), then we can throw an IllegalArgumentException in the event that it does not. The updated constructor can look like this:

public Train(int nrOfCompartments) {
    if(nrOfCompartments <= 0) {
          throw new IllegalArgumentException("nrOfCompartments must be > 0!"); // If this exception is thrown, the constructor will end.
    }
  
    compartments = new ArrayList<>();
  
    // Initialize compartments
    for (int i = 0; i < nrOfCompartments; i++) {
        TrainCompartment newCompartment=new TrainCompartment();
    
        compartments.add(newCompartment);
    }
}

Note that if the IllegalArgumentException is thrown, the constructor will actually abort and no instance is created.

Catching Exceptions

Once you throw one of the two exceptions mentioned above, you often want to deal with them. You must imagine that an exception is “thrown” from method to method. Suppose that a run() method calls the enter() method, then the exception (when thrown) will be thrown from enter() to run() (up the call stack) In the run() method, you can then decide to either catch the exception or let it pass. In the latter case, the method that calls the run() will have to handle the exception. For now, we are going to catch exceptions immediately when they are thrown.

Given the enter() method above, you can catch the exception that is (potentially) thrown as follows:

public void run(){
    Train myTrain = new Train();

    try{
        myTrain.enter(); // This method could potentially "throw" an exception, so we are going to TRY the method and catch the exception if thrown!
    } catch (IllegalStateException ise) {
        SaxionApp.printLine(ise);
    }
}

Literally you may read the above code as: Try to execute the … method and IF an exception is thrown, catch it and give it the name “ise” and then print out the error message stored in that Exception.

Note especially that the method call enter() is now included in a new construction that looks as try {... } catch(IllegalStateException ise). The curly braces are used to indicate blocks of code, similar to if-statements and loops. The catch (..) block is used as a catchall for any error messages generated from the code after try { ... }. The code you add in the catch block is only executed if an exception is actually caught. For all curly braces, you may of course write more lines of code. After all, the name “ise” for the exception is freely chosen, but is derived from the IllegalStateException.

For an IllegalArgumentException, the same goes, but you write catch (IllegalArgumentException iae). After all, in the catch block you must specify exactly what kind of exception you expect to receive.

Videos

Constructor overloading

IllegalArgument and IllegalStateExceptions

Larger demo

Let’s have another look at the train example and this time we’ll try to make it even better. We’ll only show you the solution, it’s up to you to study it and make sure you understand why and how we make certain things possible. If you have questions with regards to the code, let us know!

public class Passenger {
  private String name;

  public Passenger(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}
public class TrainCompartment {
    private int numberOfSeats;
    private int compartmentClass;

    private ArrayList<Passenger> listOfPassengers;

    public TrainCompartment(int nrOfSeats, int compartmentClass) {
        if(!(compartmentClass == 1 || compartmentClass == 2)) {
            throw new IllegalArgumentException("Compartment class cannot be anything other than 1 or 2");
        }
        
        if(nrOfSeats <= 0) {
            throw new IllegalArgumentException("nrOfSeats must be at least 1");
        }
        
        this.numberOfSeats = nrOfSeats;
        this.compartmentClass = compartmentClass;

        listOfPassengers = new ArrayList<>();
    }

    public TrainCompartment() {
        this(50, 2);
    }

    public boolean isFull() {
        return listOfPassengers.size() < numberOfSeats;
    }

    public void enter(Passenger somePassenger) {
        if (isFull()) {
            throw new IllegalStateException("The compartment is full!");
        }

        listOfPassengers.add(somePassenger);

    }

    public int getNumberOfSeatsInUse() {
        return listOfPassengers.size();
    }

    public void leave(Passenger somePassenger) {
        if(!hasPassenger(somePassenger)) {
            throw new IllegalStateException("This passenger is not in this compartment!");
        }

        listOfPassengers.remove(somePassenger);
    }

    public boolean hasPassenger(Passenger somePassenger) {
        return listOfPassengers.contains(somePassenger);
    }

    /**
     * Instead of returning the actual list (that can be manipulated), we'll just return a list of names!
     * Alternatively, we could return a copy of listOfPassengers. We just want to keep our data safe!
     * @return a list of all passenger names.
     */
    public ArrayList<String> getListOfPassengerNames() {
        ArrayList<String> result = new ArrayList<>();

        for(Passenger p : listOfPassengers) {
            result.add(p.getName());
        }

        return result;
    }

    @Override
    public String toString() {
        return "There are currently " + getNumberOfSeatsInUse() + " seats in use out of a total of " + numberOfSeats + " on the train compartment with class " + compartmentClass + ".";
    }
}
public class Train {

    private ArrayList<TrainCompartment> compartments;

    public Train(int nrOfCompartments) {
        if(nrOfCompartments <= 0) {
            throw new IllegalArgumentException("nrOfCompartments cannot be less or equal to 0");
        }

        compartments = new ArrayList<>();

        // Initialize compartments
        for (int i = 0; i < nrOfCompartments; i++) {
            TrainCompartment newCompartment = new TrainCompartment();

            compartments.add(newCompartment);
        }
    }
    
    public Train() {
        this(5);
    }

    public void enter(Passenger somePassenger) {
        for (TrainCompartment tc : compartments) {
            if (!tc.isFull()) { // If there is room in the compartment
                tc.enter(somePassenger); // Enter it..
                break; // Stop the loop: We found a seat!
            }
        }
    }

    public void leave(Passenger somePassenger) {
        for (TrainCompartment tc : compartments) {
            if (tc.hasPassenger(somePassenger)) { // If the passenger is in that compartment
                tc.leave(somePassenger); // Remove the passenger from the compartment.
                break; // Stop the loop: The passenger got off the train
            }
        }

        // We won't report an error if the passenger was never on board: We'll just ignore it.
    }
}