Advanced programming course

Arto Vihavainen and Matti Luukkainen

Note for the reader

This is direct continuation to the programming basics course Object-Oriented Programming with Java, part 1material.

This material is meant for the University of Helsinki Department of Computer Science advanced programming course, and released here as an Massive Open Online Course. The material is based on courses in 2012, 2011, and 2010, of which content has been affected by Matti Paksula, Antti Laaksonen, Pekka Mikkola, Juhana Laurinharju, Martin Pärtel, Joel Kaasinen and Mikael Nousiainen.

Read the material so that you do all of the examples you read yourself. It's worth making small changes to the examples and observe how the changes affect the program. At first you might think that doing the examples yourself and editing them too would slow down your learning. However, this isn't true at all. As far as we know, no one has yet learned to program by just reading (or by just listening to a lecture). Learning is based substantially on actively doing and growing a routine. The examples, and especially doing your own experiments, are one of the best ways to truly internalize the read text.

Try to do assignments, or at least to try them out as you read the text. If you can't get an assignment pass right off the bat, don't get depressed, since you'll be always able to get help with the assignment at the workshop.

The text isn't meant to be just read once. You'll most certainly have to return to parts you've already read, or to assignments you've already done. This text doesn't contain everything essential related to programming. As a matter of fact, no book exists that would have everything essential. So you will in every case - on your programming career - have to find information on your own. The excersises of the course already hold some instructions on how and where you'd be able to find useful information.

The course picks up where the programming basics left off, and everything learned in the programming basics course is now assumed to be known. It's a good idea to go and check the material of the programming basics course.

Week1

Recapping programming basics

In this chapter we briefly recap a few concepts we became familiar with in Part 1. You can familiarize yourself with the programming basics course material here.

Program, commands and variables

A computer program consists of a series of commands that a computer runs one at a time, from top to bottom. The commands always have a predefined structure and semantics. In Java - the programming language we use in this course - the commands are read from top to bottom, left to right. Programming courses are traditionally started by introducing a program that prints the string Hello World!. Below is a command written in Java that prints the Hello World! string.

        System.out.println("Hello World!");
    

In the command the method println - which belongs to the System class - gets called, which prints the string passed in to it as a parameter, and after that a linebreak. The method is given the string Hello World! as a parameter; consequently the program prints out Hello World! followed by a linebreak.

Variables can be used as part of the functionality of the program. Below is a program which introduces the variable length of the integer type. The value 197 is set to this variable on the next line. After this the value 179 of the variable length is printed.

        int length;
        length = 179;
        System.out.println(length);
    

The execution of the program above would happen one line at a time. First the line int length; is executed, in which the variable length is introduced. Next the line length = 179; is executed, in which we set the value 179 to the variable that was introduced on the previous line. After this the line System.out.println(length); is run, in which we call the print method we saw earlier. To this method we give the variable length as a parameter. The method prints the content - the value - of the variable length, which is 179.

In the program above we really wouldn't have to introduce the variable length on one line and then set its value on the next. The introduction of a variable and setting its value can be done on the same line.

        int length = 179;
    

When executing the above line, the variable length is introduced and as it is introduced the value 179 is set to it.

In reality all information within a computer is represented as a series of bits - ones and zeros. Variables are an abstraction offered by the programming language with which we can handle different values more easily. The variables are used to store values and to maintain the state of the program. In Java, we have the primitive variable types int (integer), double (floating-point), boolean (truth value), char (character), and the reference variable types String (character string), ArrayList (array), and all classes. We'll return to primitive data type variables and to reference type variables and their differences later.

Comparing variables and reading input

The functionality of programs is built with the help of control structures. Control structures make different functions possible depending on the variables of the program. Below, an example of an if - else if - else control structure, in which a different function is executed depending on the result of the comparison. In the example a string Accelerate is printed if the value of the variable speed is smaller than 110, the string Break if the speed is greater than 120, and the string Cruising in other cases.

int speed = 105;

if (speed < 110) {
    System.out.println("Accelerate");
} else if (speed > 120) {
    System.out.println("Break");
} else {
    System.out.println("Cruising");
}
    

Because in the example above the value of the variable speed is 105, the program will always print the string Accelerate. Remember that the comparison of strings is done with the equals method that belongs to the String class. Below is an example in which an object created from Java's Scanner class is used to read the input of a user. The program checks if the strings entered by the user are equal.

Scanner reader = new Scanner(System.in);

System.out.print("Enter the first string: ");
String first = reader.nextLine();

System.out.print("Enter the second string: ");
String second = reader.nextLine();

System.out.println();

if (first.equals(second)) {
    System.out.println("The strings you entered are the same!");
} else {
    System.out.println("The strings you entered weren't the same!");
}
    

The functionality of the program depends on the user's input. Below is an example; the red text is user input.

Enter the first string: carrot
Enter the second string: lettuce

The strings you entered weren't the same!
    

Loops

Repetition is often required in programs. First we make a so-called while-true-break loop, which we run until the user inputs the string password. The statement while(true) begins the loop, which will then be repeated until it runs into the keyword break.

        Scanner reader = new Scanner(System.in);

        while (true) {
            System.out.print("Enter password: ");
            String password = reader.nextLine();

            if (password.equals("password")) {
                break;
            }
        }

        System.out.println("Thanks!");
    
Enter password: carrot
Enter password: password
Thanks!
    

You can also pass a comparison to a while loop instead of the boolean true. Below, the user input is printed so that there are stars above and below it.

        Scanner reader = new Scanner(System.in);

        System.out.print("Enter string: ");
        String characterString = reader.nextLine();

        int starNumber = 0;
        while (starNumber < characterString.length()) {
            System.out.print("*");
            starNumber = starNumber + 1;
        }
        System.out.println();

        System.out.println(characterString);

        starNumber = 0;
        while (starNumber < characterString.length()) {
            System.out.print("*");
            starNumber = starNumber + 1;
        }
        System.out.println();
    
Enter string: carrot
******
carrot
******
    

The example above should make you feel a little bad inside. The bad feelings are hopefully because you see that the example violates the rules learned in the programming basics. The example has unneccessary repetition which should be removed with the help of methods.

In addition to the while-loop we also have two versions of the for-loop at our disposal. The newer for-loop is used for going through lists.

        ArrayList<String> greetings = new ArrayList<String>();
        greetings.add("Hei");
        greetings.add("Hallo");
        greetings.add("Hi");

        for (String greet: greetings) {
            System.out.println(greet);
        }
    
Hei
Hallo
Hi
    

The more traditional for-loop is used in situations similar to where you would use a while-loop. It can, for example, be used to go through arrays. In the following example all values in the array values will be multiplied by two and then finally printed using the newer for-loop.

        int[] values = new int[] {1, 2, 3, 4, 5, 6};

        for (int i = 0; i < values.length; i++) {
            values[i] = values[i] * 2;
        }

        for (int value: values) {
            System.out.println(value);
        }
    
2
4
6
8
10
12
    

The traditional for-loop is very useful in cases where we go through indices one at a time. The loop below will go through the characters of a character string one by one, and prints the character string Hip! every time we encounter the character a.

        String characterString = "saippuakauppias";
        for (int i = 0; i < characterString.length(); i++) {
            if (characterString.charAt(i) == 'a') {
                System.out.println("Hip!");
            }
        }
    
Hip!
Hip!
Hip!
Hip!
    

Methods

Methods are a way of chopping up the functionality of a program into smaller entities. All Java programs start their execution from the main program method, which is defined with the statement public static void main(String[] args). This statement defines a static method - that is a method which belongs to the class - which receives a character string array as its parameter.

The program defines methods to abstract the functionalities of the program. When programming, one should try to achieve a situation in which the program can be looked at from a higher level, in such a case the main method consists of calls to a group of self-defined, well-named methods. The methods then specify the functionality of the program and perhaps are based on calls to other methods.

Methods that are defined using the keyword static belong to the class that holds the method, and work as so-called support methods. The methods that are defined without the keyword static belong to the instances - the objects - created from the class and can modify the state of that individual object.

A method always has a visibility modifier (public, visible to 'everyone', or private, only visible within its class), a return type (in the main method this is void, which returns nothing) and a name. In the following code we create a method which belongs to a class, public static void print(String characterString, int times). This method prints a character string the defined amount of times. This time we use the method System.out.print, which works just like System.out.println, but doesn't print a linebreak.

        public static void print(String characterString, int times) {
            for (int i = 0; i < times; i++) {
                System.out.print(characterString);
            }
        }
    

The method above prints the character string it receives as a parameter an amount of times equal to the integer - which was also passed in as a parameter.

In the section on loops we noticed that the code had some nasty copy-paste stuff in it. With the help of methods, we can move the printing of stars to a separate method. We create a method public static void printStars(int times), which prints the amount of stars it receives as a parameter. The method uses a for loop instead of a while.

        public static void printStars(int times) {
            for (int i = 0; i < times; i++) {
                System.out.print("*");
            }
            System.out.println();
        }
    

When making use of a method, our previous (and hideous) example now looks like the following.

        Scanner reader = new Scanner(System.in);

        System.out.print("Enter characterString: ");
        String characterString = reader.nextLine();

        printStars(characterString.length());
        System.out.println(characterString);
        printStars(characterString.length());

    

Class

Methods can abstract a program up to a certain point, but as the program becomes larger it's sensible to chop down the program even further into smaller and more logical entities. With the help of classes, we can define higher level concepts of a program and functionalities related to those concepts. Every Java program requires a class in order to work, so the Hello World! example wouldn't work without the class definition. A class is defined with the keywords public class nameOfTheClass.

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
    

In a program, classes are used to define concepts and functionalities related to those concepts. Objects can be created from a class and are the embodiments of that class. Every object that belongs to a certain class has the same structure, but the variables belonging to each objects can be different. The methods of objects handle the state of the object, that is, the variables of the object.

Let's inspect the class Book below; the class has the object variables name (String) and publishingYear (integer).

        public class Book {
            private String name;
            private int publishingYear;

            public Book(String name, int publishingYear) {
                this.name = name;
                this.publishingYear = publishingYear;
            }

            public String getName() {
                return this.name;
            }

            public int getPublishingYear() {
                return this.publishingYear;
            }
        }
    

The definition in the beginning, public class Book, tells the name of the class. This is followed by the definitions of object variables. Object variables are variables which for each of the objects created from the class are their own -- the object variables of one object are unrelated to the state of the same variables of another object. It's usually appropriate to hide the object variables from the users of the class, to define the visibility modifier private for them. If the visibility modifier is set to public, the user of the object will be able to directly access the object variables.

Objects are created from a class with a constructor. A constructor is a method that initializes an object (creates the variables belonging to the object) and executes the commands that are within the constructor. The constructor is always named the same as the class that has the constructor in it. In the constructor public Book(String name, int publishingYear) a new object is created from the class Book and its variables are set to the values that were passed in as parameters.

Two methods that handle the information in the object are also defined for the class above. The method public String getName() returns the name of the object in question. The method public int getPublishingYear() returns the publishing year of the object in question.

Object

Objects are created with the help of the constructor that is defined within a class. In the program code the costructor is called with the new command, which returns a reference to the new object. Objects are instances created from classes. Let's inspect a program that creates two different books, after which it prints the values returned by the getName methods belonging to the objects.

        Book senseAndSensibility = new Book("Sense and Sensibility", 1811);
        Book prideAndPrejudice = new Book("Pride and Prejudice", 1813);

        System.out.println(senseAndSensibility.getName());
        System.out.println(prideAndPrejudice.getName());
    
Sense and Sensibility
Pride and Prejudice
    

So, each object has its own internal state. The state is formed from object variables that belong to the object. Object variables can be both primitive type variables and reference type variables. If reference type variables belong to the objects, it is possible that other objects also refer to the same referenced objects! Let's visualize this with the bank example, in which there are accounts and persons.

        public class Account {
            private String accountID;
            private int balanceAsCents;

            public Account(String accountID) {
                this.accountID = accountID;
                this.balanceAsCents = 0;
            }

            public void deposit(int sum) {
                this.balanceAsCents += sum;
            }

            public int getBalanceAsCents() {
                return this.balanceAsCents;
            }

            // .. other methods related to an account
        }
    
        import java.util.ArrayList;

        public class Person {
            private String name;
            private ArrayList<Account> accounts;

            public Person(String name) {
                this.name = name;
                this.accounts = new ArrayList<Account>();
            }

            public void addAccount(Account account) {
                this.accounts.add(account);
            }

            public int moneyTotal() {
                int total = 0;
                for (Account account: this.accounts) {
                    total += account.getBalanceAsCents();
                }

                return total;
            }

            // ... other methods related to a person
        }
    

Each object created from the Person class has its own name and its own list of accounts. Next, let's create two persons and two accounts. One of the accounts is owned by only one person and the other one is shared.

        Person matti = new Person("Matti");
        Person maija = new Person("Maija");

        Account salaryAccount = new Account("NORD-LOL");
        Account householdAccount = new Account("SAM-LOL");

        matti.addAccount(salaryAccount);
        matti.addAccount(householdAccount);
        maija.addAccount(householdAccount);

        System.out.println("Money on Matti's accounts: " + matti.moneyTotal());
        System.out.println("Money on Maija's accounts: " + maija.moneyTotal());
        System.out.println();

        salaryAccount.deposit(150000);

        System.out.println("Money on Matti's accounts: " + matti.moneyTotal());
        System.out.println("Money on Maija's accounts: " + maija.moneyTotal());
        System.out.println();

        householdAccount.deposit(10000);

        System.out.println("Money on Matti's accounts: " + matti.moneyTotal());
        System.out.println("Money on Maija's accounts: " + maija.moneyTotal());
        System.out.println();
    
Money on Matti's accounts: 0
Money on Maija's accounts: 0

Money on Matti's accounts: 150000
Money on Maija's accounts: 0

Money on Matti's accounts: 160000
Money on Maija's accounts: 10000
    

Initially, the accounts of both persons are empty. When money is added to the salaryAccount - which matti has a reference to - the amount of money on Matti's accounts grows. When money is added to the householdAccount the amount of money each person has grows. This is because both Matti and Maija have "access" to the householdAccount, so in each of the persons' object variable accounts, there's a reference to the householdAccount.

The structure of a program

A program should be clear and easy to understand for both the original writer and others. The most important aspects of a clear program are class structure and good naming conventions. Each class should have a single, clearly defined responsibility. Methods are used to reduce repetition and to create a structure for the internal functionality of the class. A method should also have a clear responsibility to ensure it stays short and simple. Methods that do many things should be divided into smaller helper methods, which are called by the original method. A good programmer writes code that can be understood even weeks after it was originally written.

Good, understandable code uses descriptive naming of variables, methods and classes, and consistent indentation. Let's look at the example below, a small program for buying and selling goods. Even though the only thing available is carrots, with no bookkeeping, the user interface could be extended to use a storage class to keep track of items.

public class UserInterface {
    private Scanner reader;

    public UserInterface(Scanner reader) {
        this.reader = reader;
    }

    public void start() {
        while (true) {
            String command = reader.nextLine();

            if (command.equals("end")) {
                break;
            } else if (command.equals("buy")) {
                String line = null;
                while(true) {
                    System.out.print("What to buy: ");
                    line = reader.nextLine();
                    if(line.equals("carrot")) {
                        break;
                    } else {
                        System.out.println("Item not found!");
                    }
                }

                System.out.println("Bought!");
            } else if (command.equals("sell")) {
                String line = null;
                while(true) {
                    System.out.print("What to sell: ");
                    line = reader.nextLine();
                    if(line.equals("carrot")) {
                        break;
                    } else {
                        System.out.println("Item not found!");
                    }
                }

                System.out.println("Sold!");
            }
        }
    }
}
    

This example has numerous problems. The first problem is the long start method. It can be shortened by moving most of the command handling to a separate method.

public class UserInterface {
    private Scanner reader;

    public UserInterface(Scanner reader) {
        this.reader = reader;
    }

    public void start() {
        while (true) {
            String command = reader.nextLine();

            if (command.equals("end")) {
                break;
            } else {
                handleCommand(command);
            }
        }
    }

    public void handleCommand(String command) {
        if (command.equals("buy")) {
            String line = null;
            while(true) {
                System.out.print("What to buy: ");
                line = reader.nextLine();
                if(line.equals("carrot")) {
                    break;
                } else {
                    System.out.println("Item not found!");
                }
            }

            System.out.println("Bought!");
        } else if (command.equals("sell")) {
            String line = null;
            while(true) {
                System.out.print("What to sell: ");
                line = reader.nextLine();
                if(line.equals("carrot")) {
                    break;
                } else {
                    System.out.println("Item not found!");
                }
            }

            System.out.println("Sold!");
        }
    }
}
    

handleCommand still has some repetition for reading the user input. Both buying and selling first print a character string with the question, then take input from the user. If the input is incorrect (other than "carrot"), "Item not found!" is printed. We will create a new method, public String readInput(String question), to handle this. Note that if the program used some other object to keep track of inventory, we would compare user input to the inventory's contents instead.

public class UserInterface {
    private Scanner reader;

    public UserInterface(Scanner reader) {
        this.reader = reader;
    }

    public void start() {
        while (true) {
            String command = reader.nextLine();

            if (command.equals("end")) {
                break;
            } else {
                handleCommand(command);
            }
        }
    }

    public void handleCommand(String command) {
        if (command.equals("buy")) {
            String input = readInput("What to buy: ");
            System.out.println("Bought!");
        } else if (command.equals("sell")) {
            String input = readInput("What to sell: ");
            System.out.println("Sold!");
        }
    }

    public String readInput(String question) {
        while (true) {
            System.out.print(question);
            String line = reader.nextLine();

            if (line.equals("carrot")) {
                return line;
            } else {
                System.out.println("Item not found!");
            }
        }
    }
}
    

The program is now divided into appropriate parts. There are still a few things, other than implementing more methods, we can do to improve readability. The start method has an if branch that ends in break, which exits the loop. We can remove the unnecessary else branch, simply moving the handleCommand method to be called after the if statement. The program still works exactly as before, but the method is now shorter and easier to read. A similar situation exists in the readInput method, so we will clean it up too.

public class UserInterface {
    private Scanner reader;

    public UserInterface(Scanner reader) {
        this.reader = reader;
    }

    public void start() {
        while (true) {
            String command = reader.nextLine();

            if (command.equals("end")) {
                break;
            }

            handleCommand(command);
        }
    }

    public void handleCommand(String command) {
        if (command.equals("buy")) {
            String input = readInput("What to buy: ");
            System.out.println("Bought!");
        } else if (command.equals("sell")) {
            String input = readInput("What to sell: ");
            System.out.println("Sold!");
        }
    }

    public String readInput(String question) {
        while (true) {
            System.out.print(question);
            String line = reader.nextLine();

            if (line.equals("carrot")) {
                return line;
            }

            System.out.println("Item not found!");
        }
    }
}
    

Dividing a program into smaller parts, like we did above, is called refactoring. It does not change how the program works, but the internal structure is changed to be more clear and easier to maintain. The current version is clearer than the original, but it can be improved further. For example, handleCommand can be further divided into two different methods, one for handling buying and the other for selling.

public class UserInterface {
    private Scanner reader;

    public UserInterface(Scanner reader) {
        this.reader = reader;
    }

    public void start() {
        while (true) {
            String command = reader.nextLine();

            if (command.equals("end")) {
                break;
            }

            handleCommand(command);
        }
    }

    public void handleCommand(String command) {
        if (command.equals("buy")) {
            commandBuy();
        } else if (command.equals("sell")) {
            commandSell();
        }
    }

    public void commandBuy() {
        String input = readInput("What to buy: ");
        System.out.println("Bought!");
    }

    public void commandSell() {
        String input = readInput("What to sell: ");
        System.out.println("Sold!");
    }

    public String readInput(String question) {
        while (true) {
            System.out.print(question);
            String line = reader.nextLine();

            if (line.equals("carrot")) {
                return line;
            } else {
                System.out.println("Item not found!");
            }
        }
    }
}
    

The program now has a clear structure with descriptively named methods. Every method is short and has a small task to handle. Note that refactoring the code did not add any new functionality, it merely changed the way the program works internally.

Programming and the importance of practicing

As far as we know, nobody has yet learned programming by listening to lectures. To develop the skill required in programming, it is essential to practice both what you have learned earlier and things that are new to you. Programming can be compared to speaking languages or playing an instrument, both of which can only be learned by doing. Master violinists are probably not good at playing only because they practice a lot. Playing an instrument is fun, which makes one more motivated to practice. The same applies to programming.

As Linus Torvalds said, "Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program".

Dr. Luukkainen has written a list of instructions for new programmers to follow when learning to program. Follow this advice to become a great programmer!

Visibility

Until now, we have been using two different keywords to define the visibility of methods and instance variables. public makes the method or instance variable visible and accessable to everyone. Methods and constructors are usually marked as public, so that they can be called from outside the class.

Declaring a method or instance variable private hides it from the outside, making it only accessible from inside the same class.

public class Book {
    private String name;
    private String contents;

    public Book(String name, String contents) {
        this.name = name;
        this.contents = contents;
    }

    public String getName() {
        return this.name;
    }

    public String getContents() {
        return this.contents;
    }

    // ...
}
    

The instance variables in the Book class above can only be accessed with the public methods getName and getContents. Fields declared as private are only accessible in code inside the class. Methods can also be declared as private, which prevents them from being called outside the class.

Now it's time to start practicing!

Smileys

Create the support method private static void printWithSmileys(String characterString) for the class Smileys which comes with the assignment template. The method is to print the given character string surrounded with smileys. Use the character string :) as the smiley.

printWithSmileys("\\:D/");
        
:):):):):)
:) \:D/ :)
:):):):):)
        

Note, that the character string must have \\ so we can print the symbol \.

Note! if the length of the character string is an odd number, add an extra space on the right side of the given character string.

printWithSmileys("\\:D/");
printWithSmileys("87.");
        
:):):):):)
:) \:D/ :)
:):):):):)
:):):):):)
:) 87.  :)
:):):):):)
        

It's a good idea to first think how many smileys should be printed for a character string of a certain length. The length of a character string can be found out with the method length which belongs to it. A loop is helpful for printing the top and bottom smiley rows, the middle row can be handled with a normal print command. You can check if a length is an odd number with the help of a remainder characterString.length() % 2 == 1.

Character String Changer

In this assignment we create a character string changer, which consists of two classes. The class Changer turns a single character to another one. The Changer holds a number of Changes and changes character strings with the help of Change objects it holds.

Change-class

Create a class Change, that has the following functionalities:

  • constructor public Change(char fromCharacter, char toCharacter) that creates an object that makes changes from character fromCharacter to toCharacter
  • method public String change(String characterString) returns the changed version of the given character string

The class is used in the following way:

  String word = "carrot";
  Change change1 = new Change('a', 'b');
  word = change1.change(word);

  System.out.println(word);

  Change Change2 = new Change('r', 'x');
  word = Change2.change(word);

  System.out.println(word);
        

The example above would print:

  cbrrot
  cbxxot
        

Tip: you can handle replacing characters in two ways, either with the help of a method in the class String (look for it yourself!) or by going through the character string character by character while forming the changed character string.

If you don't use the ready-made method of String, it is good to remember that even though you compare character strings with the command equals you compare single characters with the == operator:

  String word = "carrot";

  String replacedA = "";
  for ( int i=0; i < word.length(); i++) {
     char character = word.charAt(i);
     if ( character == 'a' ) {
        replacedA += '*'
     }  else {
        replacedA += character;
     }
  }

  System.out.println(replacedA);  // prints c*rrot
        

Changer-class

Create the class Changer, with the following functions:

  • constructor public Changer() creates a new changer
  • method public void addChange(Change change) adds a new Change to the Changer
  • method public String change(String characterString) executes all added Changes for the character string in the order of their adding and returns the changed character string

The class is used in the following way:

  Changer scandiesAway = new Changer();
  scandiesAway.addChange(new Change('ä', 'a'));
  scandiesAway.addChange(new Change('ö', 'o'));
  System.out.println(scandiesAway.change("ääliö älä lyö, ööliä läikkyy"));
        

The above example would print:

  aalio ala lyo, oolia laikkyy
        

Tip: It's a good idea to store the Changes to a list object variable of Changer (in the same fashion as on the basics course we stored players to a team, phone numbers to a phone book or books to a library, for example) A Changer is executed so that the changes are done to the character string one at a time as in the following example:

    ArrayList<Change> changes = new ArrayList<Change>();

    changes.add( new Change('a', 'b') );
    changes.add( new Change('k', 'x') );
    changes.add( new Change('o', 'å') );

    String word = "carrot";

    for (Change Change : changes) {
        word = Change.change(word);
    }

    System.out.println(word);  // print pårxxbnb
        

REMINDER when you add an ArrayList, a Scanner or a Random, Java doesn't recognize the class unless you "import" it by adding the following lines to the beginning:

import java.util.ArrayList;    // imports ArrayList
import java.util.*;            // imports all tools from java.util, including ArrayList, Scanner ja Random
        

Calculator

In this assignment, we make a simple calculator, similar to the one made in the material of programming basics' week 1. This time however, we pay attention to the structure of the program. Especially we will make the main-method (the main program) very light. The main program method doesn't actually do anything else than just start the program:

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        calculator.start();
    }
}
        

What the main program here does is it just creates the object that implements the actual application logic and then starts it. This is the proper way of creating programs and from now on we'll try to achieve this structure.

Reader

In order to communicate with the user, the calculator needs a Scanner-object. As we've seen, reading integers with a Scanner is a little laborious. We now create a separate class Reader that encapsulates a Scanner-object.

Implement the class Reader and add the following methods to it

  • public String readString()
  • public int readInteger()

Within the Reader there should be a Scanner-object as an instance variable, which the methods use in the old familiar way we know from programming basics. Remember that when reading integers, it's good to first read the entire line and then turn that in to an integer. Here we can utilize the method parseInt of the Integer-class.

Application body

The calculator works like this:

command: sum
value1: 4
value2: 6
sum of the values 10

command: product
value1: 3
value2: 2
product of the values 6

command: end
        

Implement the class Calculator to take care of the application logic of your program, and for that class a method public void start() which looks exactly like this:

    public void start() {
        while (true) {
            System.out.print("command: ");
            String command = reader.readString();
            if (command.equals("end")) {
                break;
            }

            if (command.equals("sum")) {
                sum();
            } else if (command.equals("difference")) {
                difference();
            } else if (command.equals("product")) {
                product();
            }
        }

        statistics();
    }
        

The calculator has the operations sum, difference, product.

Finish the bodies for the methods sum, difference, product and stasistics. All of them are to be of the type private void which means that the methods are available only for internal use in the calculator.

Add an instance variable of the type Reader for the calculator and create the reader in the constructor. The calculator may not have a separate Scanner-type variable!

Implementation of the application logic

Now implement the methods sum, difference and product so that they work according to the example above. In the example, first a command is asked from the user and then two values. The desired operation is then executed and the value of the operation is printed. Notice that asking the user for the values happens within the methods sum, difference and product! The methods use the Reader-object to ask the values, so the body of the methods is as follows:

    private void sum() {
       System.out.print("value1: ");
       int value1 = // read the value using the Reader-object
       System.out.print("value2: ");
       int value2 = // read the value using the Reader-object
       // print the value according to the example above
    }
        

Statistics

After the while-loop in the start-method, the method statistics is called. The method is meant to print the amount of operations done with the Calculator-object:

command: sum
value1: 4
value2: 6
sum of the values 10

command: product
luku1: 3
luku2: 2
product of the values 6

command: end
Calculations done 2
        

Implement the method private void statistics(), and make the required changes to the code of the Calculator-class in order to collect the statistics.

Note: if an invalid command is given to the program (something other than sum, difference, product or end), the calculator will not react to the command in any way, but instead continues by asking the next command. Statistics is not to count an invalid command as a completed calculation.

command: integral
command: difference
value1: 3
value2: 2
difference of the values 1

command: end
Calculations done 1
        

Bonus assignment (not tested): Reading the user input is repeated in the same way in all three operation implementing methods. Remove the repetition from your code with the help of a support method. The method can return the two values asked from the user in an array, for example.

Primitive- and reference-type variables

Java is a strongly typed language, what this means is that all of its variables have a type. The types of the variables can be divided in to two categories: primitive-type and reference-type variables. Both types of variables have their own "slot", which holds the information belonging to them. Primitive-type variables hold the concrete value in their slot, while the reference-type variables hold a reference to a concrete object.

Primitive-type variables

The value of a primitive type variable is saved in a slot created for the variable. Each primitive-type variable has its own slot and its own value. A variable's slot is created when it is introduced (int number;, for example). A value is set to a slot with the assignment operator =. Below is an example of the introduction of a primitive-type int (integer) variable and setting of its value in the same expression.

int number = 42;
    

Primtive type variables, among others, are int, double, char, boolean and the more rarely used short, float, byte and long. Another primitive-type is void, but it doesn't have its own slot or value. The void-type is used when we want to express that a method doesn't return a value.

Next we introduce two primitive-type variables and set values to them.

int five = 5;
int six = 6;
    

The primitive-type variables introduced above are named five and six. When introducing the variable five the value 5 is set to the slot that was created for it (int five = 5;). When introducing the variable six the value 6 is set to the slot that was created for it (int six = 6;). The variables five and six are both of the type int, or integers.

Primitive-type variables can be visualized as boxes that both have the values belonging to them saved in to them:

Next lets inspect how the values of primitive-type variables get copied.

int five = 5;
int six = 6;

five = six; // the variable 'five' now holds the value 6 - the value that was in the variable 'six'.
six = 64; // the variable 'six' now holds the value 64

// the variable 'five' still holds the value 6
    

Above we introduce the variables five and six and we set values to them. After this the value held in the slot of the variable six is copied to the slot of the variable five (five = six;). If the value of the variable six is changed after this point the value in the variable five remains unaffected: the value of the variable five is in its own slot and is not related to the value in the slot of the variable six in any way. The end situation as a picture:

Primitive type variable as a method parameter and return value

When a primitive type variable is passed to a method as a parameter, the method parameter is set to the value in the given variable's slot. In practice, the method parameters also have their own slots to which the value is copied, like in an assignment expression. Let us consider the following method addToValue(int value, int amount).

public int addToValue(int value, int amount) {
    return value + amount;
}
    

The method addToValue is given two parameters: value and amount. The method returns a new value, which is the sum of the given parameters. Let us investigate how the method is called.

int myValue = 10;
myValue = addToValue(myValue, 15);
// the variable 'myValue' now holds the value 25
    

In the example, addToValue is called using the variable myValue and the value 15. These are copied to the method parameters value, which will hold the value 10 (the contents of myValue), and amount, which wil hold the value 15. The method returns the sum of value and amount, which is equal to 10 + 15 = 25.

Note! In the previous example, the value of the variable myValue is changed only because it is assigned the return value of addToValue (myValue = addToValue(myValue, 15);). If the call to addToValue were as follows, the value of the variable myValue would remain unchanged.

int myValue = 10;
addToValue(myValue, 15);
// the variable 'myValue' still holds the value 10
    

Minimum and maximum values

Each primitive data type can represent a specific range of values limited by its minimum and maximum value, which are the smallest and largest values representable by the type. This is because a predefined data size is used for the internal represetantion of the type in Java (and most other programming languages).

The minimum and maximum values for a few Java primitive types are:

Data typeDescriptionMinimum valueMax value
intInteger-2 147 483 648 (Integer.MIN_VALUE)2 147 483 647 (Integer.MAX_VALUE)
longLong interger-9 223 372 036 854 775 808 (Long.MIN_VALUE)9 223 372 036 854 775 807 (Long.MAX_VALUE)
booleanTruth valuetrue or false
doubleFloating pointDouble.MIN_VALUEDouble.MAX_VALUE

Rounding errors

When using floating point data types, it is important to keep in mind that floating point types are always an approximation of the actual value. Because floating point types use a predefined data size to represent the value similarly to all other primitive data types, we may observe quite surprising rounding errors. For example, consider the following case.

double a = 0.39;
double b = 0.35;
System.out.println(a - b);
    

The example prints the value 0.040000000000000036. Programming languages usually include tools to more accurately handle floating point numbers. In Java, for example, the class BigDecimal can be used to store infinitely long floating point numbers.

When comparing floating point numbers, rounding errors are usually taken into account by comparing the distance between the values. For example, with the variables in the previous example, the expression a - b == 0.04 does not produce the expected result due to a rounding error.

double a = 0.39;
double b = 0.35;

if((a - b) == 0.04) {
    System.out.println("Successful comparison!");
} else {
    System.out.println("Failed comparison!");
}
    
Failed comparison!
    

One method to calculate the distance between two values is as follows. The helper function Math.abs returns the absolute value of the value passed to it.

double a = 0.39;
double b = 0.35;

double distance = 0.04 - (a - b);

if(Math.abs(distance) < 0.0001) {
    System.out.println("Successful comparison!");
} else {
    System.out.println("Failed comparison!");
}
    
Successful comparison!
    

Reference-Type Variables

Reference-type variables memorize the information which has been assigned to them "on the other end of the line". Reference-type variables contain a reference to the location where the information is stored. Differently from primitive-type variables, reference-type variable do not have a limited scope because their value or information is stored at the referenced location. Another substantial difference between primitive-type and reference-type variables is that various different reference-type variables can point to the same object.

Let us have a look at two reference-type variables. In the following examples we make use of the class Calculator:

public class Calculator {
    private int value;

    public Calculator(int originalValue) { // Contructor
        this.value = originalValue;
    }

    public void increaseValue() {
        this.value = this.value + 1;
    }

    public int getValue() {
        return value;
   }
}
    

Main:

Calculator bonusCalculator = new Calculator(5);
Calculator axeCalculator = new Calculator(6);
    

In the examples we first create a reference-type variable called bonusCalculator. The new operator tells that we define storage space for the information to be assigned to the variable, then we execute the code which follows the new operator, and we return a reference to the object that has been so created. The reference which is returned is assigned to the bonusCalculator variable through the = equal sign. The same thing happens with the variable called axeCalculator. If we want to think about it with pictures, we can imagine a reference-type variable as it were a box, the variable itself, with a line or an arrow, which starts at the box and points to an object. In fact, the variable does not contain the object, but it points to the object information.

Next, let us have a look at how a reference-type object is duplicated.

Calculator bonusCalculator = new Calculator(5);
Calculator axeCalculator = new Calculator(6);

bonusCalculator = axeCalculator; // the reference contained by the variable axeCalculator is copied to the variable bonusCalculator
                                 // that is to say, a reference to a Calculator-type object which received the value 6 in its constructor
                                 // is copied to the variable bonusCalculator
    

When we copy a reference-type variable (see above bonusCalculator = axeCalculator;), the reference to the variable duplicates as well. In this case, a reference to the axeCalculator variable slot is copied to the bonusCalculator variable slot. Now, both the objects point to the same place!

Let us continue with the example above and let us set a new reference to the variable axeCalculator; this new reference will point to a new object created by the command new Calculator(10).

Calculator bonusCalculator = new Calculator(5);
Calculator axeCalculator = new Calculator(6);

bonusCalculator = axeCalculator; // the reference contained by the variable axeCalculator is copied to the variable bonusCalculator
                                 // that is to say, a reference to a Calculator-type object which received the value 6 in its constructor
                                 // is copied to the variable

axeCalculator = new Calculator(10); // a new reference is assigned to the axeCalculator variable
                                    // which points to the object created by the command new Calculator(10)

// the bonusCalculator variable still contains a reference to the Calculator object which received value 6 in its parameter
    

In these examples, we do the same operations which were shown in the assignment example in the primitive-type variables section. In the very last example, we copied the reference of reference-type variables, whereas in the primitive-type variables section we copied the value of primitive-type variables. In both cases, we copy the contents of a slot: the primitive-type variable slot contains a value, whereas the reference-type variable slot contains a reference.

At the end of the previous example no variable points to the Calculator object which received value 5 in its constructor. Java's garbage collection deletes such useless objects from time to time. Our final situation looks like the following:

Let us have a look to a third example still, and let us focus on an essential difference between primitive-type and reference-type variables.

Calculator bonusCalculator = new Calculator(5);
Calculator axeCalculator = new Calculator(6);

bonusCalculator = axeCalculator; // the reference contained by the variable axeCalculator is copied to the variable bonusCalculator
                                 // that is to say, a reference to a Calculator-type object which received the value 6 in its constructor
                                 // is copied to the variable

axeCalculator.increaseValue(); // we increase by one the value of the object referenced by axeCalculator

System.out.println(bonusCalculator.getValue());
System.out.println(axeCalculator.getValue());
    
7
7
    

Both bonusCalculatorand axeCalculator point to the same object, after we have run the command bonusCalculator = axeCalculator;, and therefore, now they both have the same value 7, even though we have increased only one of them.

The situation might be clear if we look at the following picture. The method axeCalculator.increaseValue() increases by one the value variable of the object pointing to the axeCalculator variable. Because bonusCalculator points to the same object, the method bonusCalculator.getValue() returns the same value which was increased by the method axeCalculator.increaseValue().

In the following example, three reference-type variables all point to the same Calculator object.

Calculator bonus = new Calculator(5);
Calculator ihq = bonus;
Calculator lennon = bonus;
    

In the example, we create only one Calculator object, but all the three Calculator variables point to that same one. Therefore, bonus, ihq, and lennon method calls all modify the same object. To tell it once again: when reference-type variables are copied, their references also duplicate. The same concept in a picture:

Let's use this example to focus on duplication once more.

Calculator bonus = new Calculator(5);
Calculator ihq = bonus;
Calculator lennon = bonus;

lennon = new Calculator(3);
    

The modification of the lennon variable contents – that is to say the change of reference – does not affect the references of either bonus or ihq. When we assign a value to a variable, we only change the contents of that variable's own slot. The same concept in a picture:

A Reference-Type Variables and Method Parameters

When a reference-type variable is given to a method as its parameter, we create a method parameter which is the copy of the reference of a variable. In other words, we copy the reference to the parameter's own slot. Differently from what happens with original-type variables, we copy the reference and not their value. In fact, we can modify the object behind the reference even from within the method. Let us take the method public void addToCalculator(Calculator calculator, int amount).

public void addToCalculator(Calculator calculator, int amount) {
    for (int i = 0; i < amount; i++) {
        calculator.increaseValue();
    }
}
    

We give two parameters to the method addToCalculator – a reference-type value and an original-type variable. The contents of both variable slots are copied to method parameter slots. The reference-type parameter calculator receives a copy of a reference, whereas the original-type parameter amount receives the copy of value. The method will call the increaseValue() method of the Calculator-type parameter, and it will do it as many times as the value of the amount variable. Let us analyze the method call a little more deeply.

int times = 10;

Calculator bonus = new Calculator(10);
addToCalculator(bonus, times);
// the bonus variable value is now 20
    

In the example we call the addToCalculator method, whose given variables are bonus and times. This means that the reference of the reference-type variable bonus and the value of the original-type variable times (which is 10) are copied as parameters whose names are calculator and amount, respectively. The method executes the increaseValue() method of the calculator variable a number of times which equals the value of amount. See the following picture:

The method contains variables which are completely separated from the main program!

As far as the reference-type variable is concerned, a reference duplicates and it is given to the method, and the variable inside the method will still point to the same object. As far as the original-type variable is concerned, a value is copied, and the variable inside the method will have its completely independent value.

The method recognises the calculator which the bonus variable points to, and the alterations made by the method have a direct impact on the object. The situation is different with original-type variables, and the method only receives a copy of the value of the times variable. In fact, it is not possible to modify the value of original-type variables directly within a method.

A method which returns a reference-type variable

When a method returns a reference-type variable, it returns the reference to an object located elsewhere. Once the reference is returned by a method, it can be assigned to a variable in the same way as a normal assignment would happen, through the equal sign (=). Let us have a look at the method public Calculator createCalculator(int startValue), which creates a new reference-type variable.

public Calculator createCalculator(int startValue) {
    return new Calculator(startValue);
}
    

The creteCalculator method creates an object and returns its newCalculator reference. By calling the method, we always create a new object. In the following example we create two different Calculator-type objects.

Calculator bonus  = createCalculator(10);
Calculator lennon = createCalculator(10);
    

The method createCalculator always creates a new Calculator-type object. With the first call, Calculator bonus = createCalculator(10); we assign the method return reference to the bonus variable. With the second call, we create another reference and we assign it to the lennon variable. The variables bonus and lennon do not contain the same reference because the method creates a new object in both cases, and it returns the reference to that particular object.

Static and Non-Static

Let's further investigate a topic we introduced in the 30th section of Introduction to Programming. The static or non-static nature of a variable or of a method depends on their scope. Static methods are always related to their class, whereas non-static methods can modify the variables of the object itself.

Static, Class Libraries and Final

The methods which receive the definition static are not related to objects but to classes. it is possible to define class-specific variables by adding the word static to their name. For instance, Integer.MAX_VALUE, Long.MIN_VALUE and Double.MAX_VALUE are all static methods. Static methods are called via their class name, for instance ClassName.variable or ClassName.method().

We call class library a class which contains common-use methods and variables. For instance, Java Math class is a class library. It provides the Math.PI variable, inter alia. Often, creating your own class libraries can prove useful. Helsinki Regional Transport Authority (Finnish: Helsingin Seudun Liikenne, HSL) could use a class library to keep its ticket prices at its fingertips.

public class HslPrices {
    public static final double SINGLETICKET_ADULT = 2.50;
    public static final double TRAMTICKET_ADULT = 2.50;
}
    

The keyword final in the variable definition tells that once we assign a value to a variable, we can not assign a new one to it. Final-type variables are constant, and they always have to have a value. For instance, the class variable which tells the greatest integer, Integer.MAX_VALUE, is a constant class variable.

Once we have the class presented above, HslPrices, all the programs which need the single or tram-ticket price can have access to it through the class HslPrices. With the next example, we present the class Person, which has the method enoughMoneyForSingleTicket(), which makes use of the ticket price found in the class HslPrices.

public class Person {
    private String name;
    private double money;
    // more object variables

    // constructor

    public boolean enoughMoneyForSingleTicket() {
        if(this.money >= HslPrices.SINGLETICKET_ADULT) {
            return true;
        }

        return false;
    }

    // the other methods regarding the class Person
}
    

The method public boolean enoughMoneyForSingleTicket() compares the object variable money of class Person to the static variable SINGLETICKET_ADULT of class HslPrices. The method enoughMoneyForSingleTicket() can be called only through an object reference. For instance:

Person matti = new Person();

if (matti.enoughMoneyForSingleTicket()) {
    System.out.println("I'll buy a ticket.");
} else {
    System.out.println("Fare dodging, yeah!");
}
    

Note the naming convention! All constants, i.e. all variable which are provided with the definition final, are written with CAPITAL_LETTERS_AND_UNDERLINE_CHARACTERS.

Static methods function analogously. For instance, the class HslPrices could encapsulate the variables and only provide accessors. We call accessors the methods which allow us to either read a variable value or to assign them a new one.

public class HslPrices {
    public static final double SINGLETICKET_ADULT = 2.50;
    public static final double TRAMTICKET_ADULT = 2.50;

  public static double getSingleTicketPrice() {   // Accessor
    return SINGLETICKET_ADULT;
  }

  public static double getTramTicketPrice() {   // Accessor
    return TRAMTICKET_ADULT;
  }
}
    

In such cases, when we code a class such as Person, we can't call the variable straight, but we have to get it through the method getSingleTicketPrice().

public class Peson {
    private String name;
    private double money;
    // other object variables

    // constructor

    public boolean enoughMoneyForSingleTicket() {
        if(this.money >= HslPrices.giveSingleTicketPrice()) {
            return true;
        }

        return false;
    }

    // other methods regarding the class Person
}
    

Even though Java allows for static variable use, we do not usually require it. Often, using static methods causes problems with the program structure, because static variables are as inconvenient as global variables. The only static variables we use in this course are constant, i.e. final!

Non-Static

Non-static methods and variables are related to objects. The object variables, or attributes, are defined at the beginning of the class. When an object is created with the new operator, we allocate storage space for all its object variables. The variable values are personal of the object, which means that every object receives personal variable values. Let us focus again on the class Person, which has the object variable name and money.

public class Person {
  private String name;
  private double money;

  // other details
}
    

When we create a new instance of class Person, we also initialize its variables. If we do not initialize the reference-type variable name, it receives value null. Let us add the constructor and a couple of methods to our class Person.

public class Person {
  private String name;
  private double money;

    // constructor
    public Person(String name, double money) {
        this.name = name;
        this.money = money;
    }

    public String getName() {
        return this.name;
    }

    public double getMoney() {
        return this.money;
    }

    public void addMoney(double amount) {
        if(amount > 0) {
          this.money += amount;
        }
    }

    public boolean enoughMoneyForSigleTicket() {
        if(this.money >= HslPrices.getSingleTicketPrice()) {
            return true;
        }

        return false;
    }
}
    

The constructor Person(String name, double money) creates a new Person object, and it returns its reference. The method getName() returns the reference to a name object, and the getMoney() method returns the original-type variable money. The method addMoney(double amount) receives as parameter an amount of money, and it adds it to the money object variable if the parameter's value is greater than 0.

Object methods are called through their object reference. The following code example creates a new Person object, increases its money, and prints its name, at the end. Note that the method calls follow the pattern objectName.methodName()

Person matti = new Person("Matti", 5.0);
matti.addMoney(5);

if (matti.enoughMoneyForSingleTicket()) {
    System.out.println("I'll buy a single ticket.");
} else {
    System.out.println("Fare dodging, yeah!");
}
    

The example prints "I'll buy a single ticket."

Class Methods

Non-static class methods can be also called without specifying the object which indicates the class. In the following example, the toString() method points to the class Person, which calls the object method getName().

public class Person {
    // earlier written content

    public String toString() {
        return this.getName();
    }
}
    

The toString() method calls the class method getName(), which belongs to the object in question. The this prefix emphasizes that the call refers precisely to this object.

Non-static methods can also call static methods, that is the class-specific ones. On the other hand, static methods can not call non-static methods without a reference to the object itself, which is essential to retrieve the object information.

A Variable within a Method

The variables which are defined inside a method are auxiliary variables used during the method execution, and they are not to be confused with object variables. The example below shows how a local variable is created inside a method. The index variable exists and is accessible only during the method execution.

public class ... {
    ...

    public static void printTable(String[] table) {
        int index = 0;

        while(index < table.length) {
            System.out.println(table[index]);
            index++;
        }
    }
}
    

In the printTable() method, we create the auxiliary variable index which we use to parse the table. The variable index exists only during the method execution.

Thing, Suitcase, and Container

In these exercises, we create the classes Thing, Suitcase, and Container, and we train to use objects which contain other objects.

Class Thing

Create the class Thing whose objects can represent different kinds of things. The information to store are the thing's name and weight (kg).

Add the following methods to your class:

  • A construsctor, which is given the thing's name and weight as parameter
  • public String getName(), which returns the thing's name
  • public int getWeight(), which returns the thing's weight
  • public String toString(), which returns a string in the form "name (weight kg)"

Below, you find an example of how to use the class:

public class Main {
    public static void main(String[] args) {
        Thing book = new Thing("Happiness in Three Steps", 2);
        Thing mobile = new Thing("Nokia 3210", 1);

        System.out.println("Book name: " + book.getName());
        System.out.println("Book weight: " + book.getWeight());

        System.out.println("Book: " + book);
        System.out.println("Mobile: " + mobile);
    }
}
        

The program output should look like the following:

Book name: Happiness in Three Steps
Book weight: 2
Book: Happiness in Three Steps (2 kg)
Mobile: Nokia 3210 (1 kg)
        

Class Suitcase

Create the class Suitcase. Suitcase has things and a maximum weight limit, which defines the greatest total allowed weight of the things contained within the Suitcase object.

Add the following methods to your class:

  • A constructor, which is given a maximum weight limit
  • public void addThing(Thing thing), which adds the thing in the parameter to your suitcase. The method does not return any value.
  • public String toString(), which returns a string in the form "x things (y kg)"

The things are saved into an ArrayList object:

ArrayList<Thing> things = new ArrayList<Thing>();
            

The class Suitcase has to make sure the thing's weight does not cause the total weight to exceed the maximum weight limit. The method addThing should not add a new thing if the total weight happens to exceed the maximum weight limit.

Below, you find an example of how the class can be used:

public class Main {
    public static void main(String[] args) {
        Thing book = new Thing("Happiness in three steps", 2);
        Thing mobile = new Thing("Nokia 3210", 1);
        Thing brick = new Thing("Brick", 4);

        Suitcase suitcase = new Suitcase(5);
        System.out.println(suitcase);

        suitcase.addThing(book);
        System.out.println(suitcase);

        suitcase.addThing(mobile);
        System.out.println(suitcase);

        suitcase.addThing(brick);
        System.out.println(suitcase);
    }
}
            

The program output should look like the following:

0 things (0 kg)
1 things (2 kg)
2 things (3 kg)
2 things (3 kg)
            

Language Check

"0 things" or "1 things" is not really proper English – it would be better to say "empty" or "1 thing". Implement this change in the class Suitcase.

Now, the output of the previous program should look like the following:

empty (0 kg)
1 thing (2 kg)
2 things (3 kg)
2 things (3 kg)
            

Every Thing

Add the following methods to Suitcase:

  • printThings, which prints out all the things inside the suitcase
  • totalWeight, which returns the total weight of the things in your suitcase

Below, there is an example of how the class can be used:

public class Main {
    public static void main(String[] args) {
        Thing book = new Thing("Happiness in Three Steps", 2);
        Thing mobile = new Thing("Nokia 3210", 1);
        Thing brick = new Thing("Brick", 4);

        Suitcase suitcase = new Suitcase(10);
        suitcase.addThing(book);
        suitcase.addThing(mobile);
        suitcase.addThing(brick);

        System.out.println("Your suitcase contains the following things:");
        suitcase.printThings();
        System.out.println("Total weight: " + suitcase.totalWeight() + " kg");
    }
}
            

The program output should now look like the following:

Your suitcase contains the following things:
Happiness in Three Steps (2 kg)
Nokia 3210 (1 kg)
Brick (4 kg)
Total weight: 7 kg
            

Modify your class also so that you use only two object variables. One contains the maximum weight, the other is a list with the things in your suitcase.

The heaviest Thing

Now, add the method heaviestThing to your class Suitcase, which returns the thing which weighs the most. If there are more than one thing with the same weight, the method can return either one. The method has to return an object reference. If the suitcase is empty, the method returns null.

Here is an usage example of the class:

public class Main {
    public static void main(String[] args) {
        Thing book = new Thing("Happiness in Three Steps", 2);
        Thing mobile = new Thing("Nokia 3210", 1);
        Thing brick = new Thing("Brick", 4);

        Suitcase suitcase = new Suitcase(10);
        suitcase.addThing(book);
        suitcase.addThing(mobile);
        suitcase.addThing(brick);

        Thing heaviest = suitcase.heaviestThing();
        System.out.println("The heaviest thing: " + heaviest);
    }
}
            

The program output should look like the following:

The heaviest thing: Brick (4 kg)
            

Container

Create the class Container, which has the following methods:

  • a constructor which is given the maximum weight limit
  • public void addSuitcase(Suitcase suitcase), which adds the suitcase as a parameter to the container
  • public String toString() which returns a string in the form "x suitcases (y kg)"

Store the suitcase with a suitable ArrayList construction.

The class Container has to make sure the thing's total weight does not overcome the maximum weight limitation. The method addSuitcase should not add a new suitcase if the total weight happens to exceed the maximum weight limit.

Below, there is an example of how the class can be used:

public class Main {
    public static void main(String[] args) {
        Thing book = new Thing("Happiness in Three Steps", 2);
        Thing mobile = new Thing("Nokia 3210", 1);
        Thing brick = new Thing("Brick", 4);

        Suitcase tomsCase = new Suitcase(10);
        tomsCase.addThing(book);
        tomsCase.addThing(mobile);

        Suitcase georgesCase = new Suitcase(10);
        georgesCase.addThing(brick);

        Container container = new Container(1000);
        container.addSuitcase(tomsCase);
        container.addSuitcase(georgesCase);

        System.out.println(container);
    }
}
            

The program output should look like the following:

2 suitcases (7 kg)
            

The Container Contents

Add the method public void printThings() to your Container; the method prints out all the things inside the container's suitcases.

Below is an example of how the class can be used:

public class Main {
    public static void main(String[] args) {
        Thing book = new Thing("Happiness in Three Steps", 2);
        Thing mobile = new Thing("Nokia 3210", 1);
        Thing brick = new Thing("Brick", 4);

        Suitcase tomsCase = new Suitcase(10);
        tomsCase.addThing(book);
        tomsCase.addThing(mobile);

        Suitcase georgesCase = new Suitcase(10);
        georgesCase.addThing(brick);

        Container container = new Container(1000);
        container.addSuitcase(tomsCase);
        container.addSuitcase(georgesCase);

        System.out.println("There are the following things in the container suitcases:");
        container.printThings();
    }
}
            

The program output should look like the following:

There are the following things in the container suitcases:
Happiness in Three Steps (2 kg)
Nokia 3210 (1 kg)
Brick (4 kg)
            

A Lot of Bricks

Let's check that our container works fine and we can still not exceed the maximum weight limit. In the Main class, create the method public static void addSuitcasesFullOfBricks(Container container), which adds 100 suitcases into the container it receives as parameter; there is one brick in each suitcase. The bricks weight will then increase by one each time until the weight becomes 100 kg.

The program body is the following:

public class Main {
    public static void main(String[] Container) {
        Container container = new Container(1000);
        addSuitcasesFullOfBricks(container);
        System.out.println(container);
    }

    public static void addSuitcasesFullOfBricks(Container container) {
        // adding 100 suitcases with one brick in each
    }
}
            

The program output should look like the following:

44 suitcases (990 kg)
            

HashMap

HashMap is one of Java's most useful data structures. The idea behind HashMap is we define an index for an object key - a unique value, for instance a social security number, a student number, or a phone number. We call hashing the process of changing a key into an index, or simply to define an index. The hashing happens thanks to a particular function which makes sure that we get always the same index with a known key.

Adding and retrieving items based on the keys allows for a particularly quick search process. Instead of parsing the table items one by one (in the worst case we would have to go through all the items), and instead of looking for a value with a binary search (in which case we would have to go through a number of items which would depend on the logarithm of the table size), we can look at only one table index and check whether a value is mapped to that index.

HashMap uses the Object class hashCode() method to find a key value. Every HashMap subclass will inherit the hashCode() method. However, we will not go deep into HashMap workings in this course. We will return to inheritance in week 10.

Java's HashMap class encapsulates - or hides - the way it works, and it returns made-up methods ready to use.

When we create a HashMap we need two type parameters, a type for the key variable, and a type for the stored object. The following example uses a String-type object as key, and a String-type object as the stored object.

HashMap<String, String> numbers = new HashMap<String, String>();
numbers.put("One", "Yksi");
numbers.put("Two", "Kaksi");

String translation = numbers.get("One");
System.out.println(translation);

System.out.println(numbers.get("Two"));
System.out.println(numbers.get("Three"));
System.out.println(numbers.get("Yksi"));
        
Yksi
Kaksi
null
null
        

In the example, we create a HashMap where both the key and the stored object are strings. We add information to the HashMap with the put() method, which receives the references to the key and to the stored object as parameter. The method get() returns either the reference to the key given as parameter or a null value in case the key was not found.

Each key is mapped to one value, within the HashMap. If we store a new value with an already existing key, the old value is lost.

HashMap<String, String> numbers = new HashMap<String, String>();
numbers.put("One", "Yksi");
numbers.put("Two", "Kaksi");
numbers.put("One", "Uno");

String translation = numbers.get("One");
System.out.println(translation);

System.out.println(numbers.get("Two"));
System.out.println(numbers.get("Three"));
System.out.println(numbers.get("Yksi"));
        

Because the key "One" is assigned a new value, the print output of the example is like the following.

Uno
Kaksi
null
null
        

Nicknames

Create a HashMap<String,String> object in the main method. Store the following people's names and nicknames into the HashMap, the name being the key and the nickname its value. Use only lower case letters.

  • matti's nickname is mage
  • mikael's nickname is mixu
  • arto's nickname is arppa

Then, retrieve mikael's nickname and print it.

The tests require you write lower case names.

Book Search through HashMap

Let us go deeper into HashMap workings with the help of the following example. Books can be retrieved based on their name, which acts as book key. If we find a book for the given name, we obtain the respective reference, as well as the book details. Let us create the example class Book, which has a name and the book contents as object variables.

public class Book {
    private String name;
    private String contents;
    private int publishingYear;

    public Book(String name, int publishingYear, String contents) {
        this.name = name;
        this.publishingYear = publishingYear;
        this.contents = contents;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPublishingYear() {
        return this.publishingYear;
    }

    public void setPublishingYear(int publishingYear) {
        this.publishingYear = publishingYear;
    }

    public String getContents() {
        return this.contents;
    }

    public void setContents(String contents) {
        this.contents = contents;
    }

    public String toString() {
        return "Name: " + this.name + " (" + this.publishingYear + ")\n"
                + "Contents: " + this.contents;
    }
}
        

In the following example, we create a HashMap which makes use of the book name - a String-type object - and stores the references which point to Book-objects.

HashMap<String, Book> bookCollection = new HashMap<String, Book>();
        

The HashMap above has a String object as key. Let us extend our example so that we would add two books to our book collection, "Sense and Sensibility" and "Pride and Prejudice".

Book senseAndSensibility = new Book("Sense and Sensibility", 1811, "...");
Book prideAndPrejudice = new Book("Pride and Prejudice", 1813, "....");

HashMap<String, Book> bookCollection = new HashMap<String, Book>();
bookCollection.put(senseAndSensibility.getName(), senseAndSensibility);
librabookCollectionry.put(prideAndPrejudice.getName(), prideAndPrejudice);
        

Books can be retrieved from the book collection based on their name. A search for the book "Persuasion" does not return a corresponding entry, in which case the HashMap returns a null reference. However, the book "Pride and Prejudice" was found.

Book book = bookCollection.get("Persuasion");
System.out.println(book);
System.out.println();
book = bookCollection.get("Pride and Prejudice");
System.out.println(book);
        
null

Name: Pride and Prejudice (1813)
Contents: ...
        

HashMaps are useful when we know the key to use for our search. Keys are always unique, and it is not possible to store more than one object together with one key alone. The object which we store can still be a list or another HashMap, of course!

Library

The problem with the book collection above is that we must remember the correct book name when we search for it, character by character. Java built-in String class provides us the tools for this. The toLowerCase() method turns a string's characters to lower case, and the trim() method deletes the white spaces at the beginning and at the end of the string. Computer users tend to write white spaces at the beginning or end of a text, involuntarily.

String text = "Pride and Prejudice ";
text = text.toLowerCase(); // the text is now "pride and prejudice "
text = text.trim() // the text is now "pride and prejudice"
        

Let us create the the class Library, which encapsulates a HashMap containing books, and allows for book search regardless of its spelling. Let us add the methods addBook(Book book) and removeBook(String bookName) to our Library class. It's already clear that we would need various different methods to clean a string. Therefore, we can create a separate method called private String stringCleaner(String string).

public class Library {
    private HashMap<String, Book> collection;

    public Library() {
        this.collection = new HashMap<String, Book>();
    }

    public void addBook(Book book) {
        String name = stringCleaner(book.getName());

        if(this.collection.containsKey(name)) {
            System.out.println("The book is already in the library!");
        } else {
            collection.put(name, book);
        }
    }

    public void removeBook(String bookName) {
        bookName = stringCleaner(bookName);

        if(this.collection.containsKey(bookName)) {
            this.collection.remove(bookName);
        } else {
            System.out.println("The book was not found, you can't remove it!");
        }
    }

    private String stringCleaner(String string) {
        if (string == null) {
            return "";
        }

        string = string.toLowerCase();
        return string.trim();
    }
}
        

We implement our search functionality so that we can retrieve a book using a hash algorithm based on the book name.

    public Book getBook(String bookName) {
        bookName = stringCleaner(bookName);
        return this.collection.get(bookName);
    }
        

The method above returns the wanted book when this is found, otherwise it returns a null value. We can also also go through all the collection keys one by one, and look for the beginning characters of the book's name. In this way, we would actually fail to capitalise on HashMap performance speed because, in the worst case, we would need to go through all the book names. Search based on the beginning characters of a string is possible through the keySet() method. The keySet() method returns a set of keys, which can be parsed with the for each loop.

    public Book getBookUsingItsBeginningCharacters(String beginning) {
        beginning = stringCleaner(beginning);

        for (String key: this.collection.keySet()) {
            if (key.startsWith(beginning)) {
                return this.collection.get(key);
            }
        }

        return null;
    }
        

Let's leave the method above out of our library for now. Our library is still lacking an essential feature concerning book addition. Let us create the method public ArrayList<Book> bookList(), which returns a list of the books in our library. The method bookList() makes use of the values() method, which is provided by HashList. The values() method returns a set of our library books, which can be given as parameter to the constructor of an ArrayList class.

public class Library {
    private HashMap<String, Book> collection;

    public Library() {
        this.collection = new HashMap<String, Book>();
    }

    public Book getBook(String bookName) {
        bookName = stringCleaner(bookName);
        return this.collection.get(bookName);
    }

    public void addBook(Book kirja) {
        String name = stringCleaner(book.getName());

        if(this.collection.containsKey(name)) {
            System.out.println("The book is already in the library!");
        } else {
            this.collection.put(name, book);
        }
    }

    public void removeBook(String bookName) {
        bookName = stringCleaner(bookName);

        if(this.collection.containsKey(bookName)) {
            this.collection.remove(bookName);
        } else {
            System.out.println("The book was not found, you can't remove it!");
        }
    }

    public ArrayList<Book> bookList() {
        return new ArrayList<Book>(this.collection.values());
    }

    private String stringCleaner(String string) {
        if (string == null) {
            return "";
        }

        string = string.toLowerCase();
        return string.trim();
    }
}
        

Among the programming principles, there is the so called DRY principle (Don't Repeat Yourself), according to which we try to avoid having code repeat in different places. Turning a string to lower case, and its trimming - removing white spaces from the beginning and the end of a string - would have ocurred several different places without the stringCleaner() method. We might hardly notice we are repeating the same code as we are writing. It is only afterwards we may see the repeated code has snuck in there. That the repetition happens is not in itself bad, however. The most important thing is that we clean our code as soon as we notice the need.

Original-Type Variables in a HashMap

Both HashMap keys and stored objects are reference-type variables. If we want to use an original-type variable as key or stored value, we can use their reference-type equivalent. Some are introduced below.

Original-typeReference-type equivalent
intInteger
doubleDouble
charCharacter

In fact, Java automatically encapsulates original-type values and translates them into reference-type values when needed. Even though the number 1 is an original-type variable, it can be used as an Integer key directly in the following way.

HashMap<Integer, String> table = new HashMap<Integer, String>();
table.put(1, "Be!");
            

In Java, the automatic translation of original-type variables into reference-type ones is called auto-boxing, i.e. allocation into a slot. The same process also works in the opposite way. We can create a method which returns a HashMap containing an Integer. In the following example, the automatic translation happens inside the method addTwitch.

public class TwitchRegister {
    private HashMap<String, Integer> twitched;

    public NumberBookkeeping() {
        this.twitched = new HashMap<String, Integer>();
    }

    public void addTwitch(String name, int number) {
        this.twitched.put(name, number);
    }

    public int lastTwitch(String name) {
        this.twitched.get(name);
    }
}
            

Even though the HashMap contains Integer objects, Java can also translate certain reference-type variables into their original-type equivalent. For instance, Integer objects can be translated into int values, if needed. However, this can be misleading! If we try to translate a null reference into a number, we receive the java.lang.reflect.InvocationTargetException error. When we make use of this automatic translation, we have to be sure that the value we want to translate is not null. The above lastTwitch method must be fixed in the following way.

    public int lastTwitch(String name) {
        if(this.twitched.containsKey(name) {
            return this.twitched.get(name);
        }

        return 0;
    }
            

Promissory Note

Create the class PromissoryNote with the following functionality:

  • the constructor public PromissoryNote() creates a new promissory note
  • the method public void setLoan(String toWhom, double value) which stores the information about loans to specific people.
  • the method public double howMuchIsTheDebt(String whose) which returns the entity of the debt held by the parameter person

The class can be used in the following way:

  PromissoryNote mattisNote = new PromissoryNote();
  mattisNote.setLoan("Arto", 51.5);
  mattisNote.setLoan("Mikael", 30);

  System.out.println(mattisNote.howMuchIsTheDebt("Arto"));
  System.out.println(mattisNote.howMuchIsTheDebt("Joel"));
                

The example above would print:

51.5
0
                

Be careful in a situation where you ask for the debt of a person who hasn't got debts. Go back to the final example of section 36.3, if you need!

Attention! The promissory note does not need to take into account old loans. When you set a new debt to a person who has an old one, the old one is canceled.

  PromissoryNote mattisNote = new PromissoryNote();
  mattisNote.setLoan("Arto", 51.5);
  mattisNote.setLoan("Arto", 10.5);

  System.out.println(mattisNote.howMuchIsTheDebt("Arto"));
                
10.5
                

Dictionary

In this exercise, we implement a dictionary which can be used to retrieve the English translation of Finnish words. We implement our dictionary using the HashMap data structure.

Class Dictionary

Create a class called Dictionary. The class has the following methods:

  • public String translate(String word), returning the translation of its parameter. If the word is unknown, it returns null.
  • public void add(String word, String translation), adding a new translation to the dictionary

Implement the class Dictionary so that it contained only one object variable, a HashMap data structure.

Test your Dictionary:

    Dictionary dictionary = new Dictionary();
    dictionary.add("apina", "monkey");
    dictionary.add("banaani", "banana");
    dictionary.add("cembalo", "harpsichord");

    System.out.println(dictionary.translate("apina"));
    System.out.println(dictionary.translate("porkkana"));
                
monkey
null
                

Amount of Words

Add the method public int amountOfWords(), which returns the amount of words in the dictionary.

    Dictionary dictionary = new Dictionary();
    dictionary.add("apina", "monkey");
    dictionary.add("banaani", "banana");
    System.out.println(dictionary.amountOfWords());

    dictionary.add("cembalo", "harpsichord");
    System.out.println(dictionary.amountOfWords());
                
2
3
                

Listing All Words

Add the method public ArrayList<String> translationList() to your dictionary, returning strings which stand for a content list of your dictionary in the form key = value.

    Dictionary dictionary = new Dictionary();
    dictionary.add("apina", "monkey");
    dictionary.add("banaani", "banana");
    dictionary.add("cembalo", "harpsichord");

    ArrayList<String> translations = dictionary.translationList();
    for(String translation: translations) {
        System.out.println(translation);
    }
                
banaani = banana
apina = monkey
cembalo = harpsichord
                

Hint: you can go through all HashMap keys using the method keySet in the following way:

    HashMap<String, String> wordPairs = new HashMap<String, String>();

    wordPairs.put("monkey", "animal");
    wordPairs.put("South", "compass point");
    wordPairs.put("sauerkraut", "food");

    for ( String key : wordPairs.keySet() ) {
        System.out.print( key + " " );
    }

    // prints: monkey South sauerkraut
                

The Beginning of a Text User Interface

In this exercise, we also train creating a text user interface. Create the class TextUserInterface, with the following methods:

  • the constructor public TextUserInterface(Scanner reader, Dictionary dictionary)
  • public void start(), which starts the interface.

The text user interface stores into two object variables the reader and dictionary it has received as constructor parameters. You don't need other object variables. The user input must be read using the reader object received as constructor parameter! The translations also have to be stored into the dicitonary object received as constructor parameter. The text user interface must not create new objects itself!

Attention: This means The text user interface must not create a scanner itself but it must use the scanner received as parameter to read the user input!

At the beginning, in the text user interface must only have the command quit, to quit the text user interface. If the user inputs something else, we print "Unknown statement".

    Scanner reader = new Scanner(System.in);
    Dictionary dict = new Dictionary();

    TextUserInterface ui = new TextUserInterface(reader, dict);
    ui.start();
                
Statement:
  quit - quit the text user interface

Statement: help
Unknown statement

Statement: quit
Cheers!
                

Adding and Translating Words

Add the methods add and translate to your text user interface. The command add asks for a word pair from the user and adds them to the dictionary. The command translate asks a word from the user and it prints the translation.

    Scanner reader = new Scanner(System.in);
    Dictionary dict = new Dictionary();

    TextUserInterface ui = new TextUserInterface(reader, dict);
    ui.start();
                
Statements:
  add - adds a word pair to the dictionary
  translate - asks a word and prints its translation
  quit - quits the text user interface

Statement: add
In Finnish: porkkana
Translation: carrot

Statement: translate
Give a word: porkkana
Translation: carrot

Statement: quit
Cheers!
                

Towards Automatic Tests

Testing a program manually is a hopeless burden. It is possible to automate inputs by setting up a string as a Scanner object parameter. The example below shows how it is possible to test automatically the program above.

    String input = "translate\n" + "monkey\n"  +
                   "translate\n" + "cheese\n" +
                   "add\n"  + "cheese\n" + "juusto\n" +
                   "translate\n" + "cheese\n" +
                   "quit\n";

    Scanner reader = new Scanner(input);
    Dictionary dictionary = new Dictionary();

    TextUserInterface ui = new TextUserInterface(reader, dictionary);
    ui.start();
            

The print output contains only the program output, and not the user commands.

Commands:
  add - adds a word couple to the dictionary
  translate - asks for a word and prints its translation
  quit - stops the user interface

Command: Give word: Unknown word!

Command: Give word: Unknown word!

Command: In Finnish: Translation:
Command: Give word: Translation: juusto

Command: Cheers!
            

Giving a string to a Scanner class is a way to replace the String inputs given through the keyboard. The contents of the String variable input "simulates" the user input. \n denotes a line break. Each single part of the input variable which ends with a line break corresponds to one nextLine() input.

It is easy to change the text input, and we can add new words to our dictionary in the following way:

    String input = "add\n"  + "cheese\n" + "juusto\n" +
                   "add\n"  + "bier\n" + "olut\n" +
                   "add\n"  + "book\n" + "kirja\n" +
                   "add\n"  + "computer\n" + "tietokone\n" +
                   "add\n"  + "auto\n" + "car\n" +
                   "quit\n";
            

If you want to test again your program manually, change the Scanner object constructor parameter into System.in, i.e system input stream.

The program functionality must be checked from the output pane, still. The result can still be confusing at the beginning, because the automatic input does not appear in the output pane at all.

The final goal will be to also automate the testing the program's functionality, so that both testing the program and analising its output text would happen successfully in one click.

Java API

The Java programming language we use in our course is made of three things. The first is the program syntax and semantics: the way we define variables, the control flow, the variable and class structure, and their functionality. The second is JVM, i.e. Java Virtual Machine, used for running our programs. Our Java programs are translated into a bytecode, which can be run on whatever computer has JVM. We haven't dealt with program translation because the program environment does it on our behalf. Sometimes, if the program environtment does not work as expected we may have to choose clean & build, which deletes the old source code and translates our program again. The third is API (Application Programming Interface), that is to say the program interface or standard library.

API is a set of built-in classes specific of the programming language, which is provided to users for their own projects. For instance the casses ArrayList, Arrays, Collections, and String are all part of Java's build-in API. A description of the API of Java 7 can be found at the address http://docs.oracle.com/javase/7/docs/api/. On the left side of the page we find a description of Java's built-in classes. If you look for the ArrayList class, you find a link to http://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html, which shows the stucture, constructors, and methods of the class.

NetBeans is able to show a class API, if needed. If you write a class name and add the relative import sentence, you can right click on the class name and and choose Show Javadoc. This opens the class API description in your browser.

Airport

Every week, you will find one or more larger exercises, where you can design freely the program structure, the appearance of the user interface and the requred commands are predefined. The first exercise which you can design freely in Advanced Programming is Airport.

Attention: you can create only one Scanner object so that your tests would work well. Also, do not use static variables, the tests execute your program many different times, and the static variable values left from the previous execution would possibly disturb them!

In the airport exercises we create an application to manage an airport, with its airplanes and flights. As far as the planes are concerned, we always know their ID and capacity. As for the flights, we know the flight plane, the know the departure airport code (for instance HEL) and the destination airport code (for instance BAL).

There can be various different flights and planes. The same plane can also go on various different flights (various different routes). The application must offers two different panels. First, the airport worker inputs the flight and plane information to the system.

When the user exits the panel, the user can use the flight service. The flight service has three actions: printing planes, printing flights, and printing user airplane information. In addition to this, the user can exit the application by choosing x. If the user inputs an invalid command, the command is asked again.

Airport panel
--------------------

Choose operation:
[1] Add airplane
[2] Add flight
[x] Exit
> 1
Give plane ID: HA-LOL
Give plane capacity: 42
Choose operation:
[1] Add airplane
[2] Add flight
[x] Exit
> 1
Give plane ID: G-OWAC
Give plane capacity: 101
Choose operation:
[1] Add airplane
[2] Add flight
[x] Exit
> 2
Give plane ID: HA-LOL
Give departure airport code: HEL
Give destination airport code: BAL
Choose operation:
[1] Add airplane
[2] Add flight
[x] Exit
> 2
Give plane ID: G-OWAC
Give departure airport code: JFK
Give destination airport code: BAL
Choose operation:
[1] Add airplane
[2] Add flight
[x] Exit
> 2
Give plane ID: HA-LOL
Give departure airport code: BAL
Give destination airport code: HEL
Choose operation:
[1] Add airplane
[2] Add flight
[x] Exit
> x

Flight service
------------

Choose operation:
[1] Print planes
[2] Print flights
[3] Print flight info
[x] Quit
> 1
G-OWAC (101 ppl)
HA-LOL (42 ppl)
Choose action:
[1] Print planes
[2] Print flights
[3] Print flight info
[x] Quit
> 2
HA-LOL (42 ppl) (HEL-BAL)
HA-LOL (42 ppl) (BAL-HEL)
G-OWAC (101 ppl) (JFK-BAL)

Choose operation:
[1] Print planes
[2] Print flights
[3] Print flight info
[x] Quit
> 3
Give plane ID: G-OWAC
G-OWAC (101 ppl)

Choose operation:
[1] Print planes
[2] Print flights
[3] Print flight info
[x] Quit
> x
                

Attention: for the tests, it is essential that the user interface works exactly as displayed above. In fact, it is a good idea to copy-paste the menus printed by the above program into your code exactly. The tests do not require that your program should be prepared to deal with invalid inputs. This exercise is worth three single excercise points.

The program must start by executing the main method in the exercise layout.

Still another remark: in order to make your tests work, your program has to create only one Scanner object. Also, avoid using static variables: the tests execute your program many different times, and the static variable values left from the previous execution would possibly disturb them!

Week2

Object

In our course, we have been using frequently the method public String toString() when we wanted to print an object in the shape of a string. Calling the method without setting it up properly does usually cause an error. We can have a look at the class Book, which does not contain the method public String toString() yet, and see what happens when the program uses the method System.out.println() and tries to print an object of Book class.

public class Book {
    private String name;
    private int publishingYear;

    public Book(String name, int publishingYear) {
        this.name = name;
        this.publishingYear = publishingYear;
    }

    public String getName() {
        return this.name;
    }

    public int getPublishingYear() {
        return this.publishingYear;
    }
}
            
Book objectBook = new Book("Object book", 2000);
System.out.println(objectBook);
            

if we take an object of Book class and use it as the parameter of the method System.out.println(), our program does not print an error message. Our program does not crash, and instead of reading an error message, we notice an interesting print output. The print output contains the name of the class, Book, plus an indefinite String which follows a @ character. Notice that when we call System.out.println(objectBook) Java calls System.out.println(objectBook.toString()), in fact, but this does not cause an error.

The explanation is related to the way Java classes are built. Each Java class automatically inherits the Object class, which contains a set of methods that are useful to each Java class. Heritage means that our class has access to the features and functions defined in the inherited class. Among the others, the class Object contains the method toString, which is inherited by the classes we create.

The toString method inherited from the object class is not usually the one we'd want. That's why we will want to replace it with one we make personally. Let us add the method public String toString() to our Book class. This method will replace the toString method inherited from the Object class.

public class Book {
    private String name;
    private int publishingYear;

    public Book(String name, int publishingYear) {
        this.name = name;
        this.publishingYear = publishingYear;
    }

    public String getName() {
        return this.name;
    }

    public int getPublishingYear() {
        return this.publishingYear;
    }

    @Override
    public String toString() {
        return this.name + " (" + this.publishingYear + ")";
    }
}
            

If now we create an object instance, and we set it into the print method, we notice that the toString method of the Book class produces a string.

Book objectBook = new Book("Object book", 2000);
System.out.println(objectBook);
            
Object book (2000)
            

Above the toString method of class Book we see the @Override annotation. We use annotations to give guidelines to both the translator and the reader about how to relate to the methods. The @Override annotation tells that the following method replaces the one defined inside the inherited class. If we don't add an annotation to the method we replace, the translator gives us a warning, however avoiding writing annotations is not a mistake.

There are also other useful methods we inherit from the Object class. Let us now get acquainted with the methods equals and hashCode.

Equals Method

The equals method is used to compare two objects. The method is particularly used when we compare two String objects.

Scanner reader = new Scanner(System.in);

System.out.print("Write password: ");
String password = reader.nextLine();

if (password.equals("password")) {
    System.out.println("Right!");
} else {
    System.out.println("Wrong!");
}
            
Write password: mightycarrot
Wrong!
            

The equals method is defined in the Object class, and it makes sure that both the parameter object and the compared object have the same reference. In other words, by default the method makes sure that we are dealing with one unique object. If the reference is the same, the method returns true, otherwise false. The following example should clarify the concept. The class Book doesn't implement its own equals method, and therefore it uses the one created by the Object class.

Book objectBook = new Book("Objectbook", 2000);
Book anotherObjectBook = objectBook;

if (objectBook.equals(objectBook)) {
    System.out.println("The books were the same");
} else {
    System.out.println("The books were not the same");
}

// Now we create an object with the same contents, which is however a different, independent object
anotherObjectBook = new Book("Objectbook", 2000);

if (objectBook.equals(anotherObjectBook)) {
    System.out.println("The books were the same");
} else {
    System.out.println("The books were not the same");
}
            

Print output:

The books were the same
The books were not the same
            

Even if the internal structure of both Book objects (i.e. the object variable values) is exactly the same, only the first comparison prints "The books were the same". This depends on the fact that only in the first case also the references were the same, i.e. we were comparing an object with itself. In the second example, we had two different objects even though they both had the same values.

When we use the equals method to compare strings, it works as we want it to: it identifies two strings as equal if the have the same contents even though they are two different objects. In fact, the default equals method is replaced with a new implementation in the String class.

We want that book comparison happened against name and year. We replace the equals method in the Object class with an implementation in the Book class. The equals method has to make sure whether the object is the same as the one received as parameter. First, we define a method according to which all the objects are the same.

    public boolean equals(Object object) {
        return true;
    }
            

Our method is a little too optimistic, so let us change its functionality slightly. Let us define that the objects are not the same if the parameter object is null or if the the two object types are different. We can find out the type of an object with the method getClass() (which is denifed in the oject class). Otherwise, we expect that the objects are the same.

    public boolean equals(Object object) {
        if (object == null) {
            return false;
        }

        if (this.getClass() != object.getClass()) {
            return false;
        }

        return true;
    }
            

The equalsmethod finds out the class difference of two objects, but it is not able to distinguish two similar objects from each other. In order to compare our object with the object we received as parameter, and whose reference is Object type, we have to change the type of the Object reference. The reference type can be changed if and only if the object type is really such as we are converting it into. Type casting happens by specifying the desired class within brackets on the right side of the assignment sentence:

    WantedType variable = (WantedType) oldVariable;
            

Type casting is possible because we know two objects are the same type. If they are different type, the above getClass method returns false. Let us change the Object parameter received with the equals method into Book type, and let us identify two different books against their publishing year. The books are otherwise the same.

    public boolean equals(Object object) {
        if (object == null) {
            return false;
        }

        if (getClass() != object.getClass()) {
            return false;
        }

        Book compared = (Book) object;

        if(this.publishingYear != compared.getPublishingYear()) {
            return false;
        }

        return true;
    }
            

Now, our comparison method is able to distinguish books against their publishing year. Wa want to check still that our book names are the same, and our own book name is not null.

    public boolean equals(Object object) {
        if (object == null) {
            return false;
        }

        if (getClass() != object.getClass()) {
            return false;
        }

        Book compared = (Book) object;

        if (this.publishingYear != compared.getPublishingYear()) {
            return false;
        }

        if (this.name == null || !this.name.equals(compared.getName())) {
            return false;
        }

        return true;
    }
            

Excellent, we have got a method for comparison which works, finally! Below is our Book class as it looks like at the moment.

public class Book {
    private String name;
    private int publishingYear;

    public Book(String name, int publishingYear) {
        this.name = name;
        this.publishingYear = publishingYear;
    }

    public String getName() {
        return this.name;
    }

    public int getPublishingYear() {
        return this.publishingYear;
    }

    @Override
    public String toString() {
        return this.name + " (" + this.publishingYear + ")";
    }

    @Override
    public boolean equals(Object object) {
        if (object == null) {
            return false;
        }

        if (getClass() != object.getClass()) {
            return false;
        }

        Book compared = (Book) object;

        if (this.publishingYear != compared.getPublishingYear()) {
            return false;
        }

        if (this.name == null || !this.name.equals(compared.getName())) {
            return false;
        }

        return true;
    }
}
            

Now, our book comparison returns true, if the book contents are the same.

Book objectBook = new Book("Objectbook", 2000);
Book anotherObjectBook = new Book("Objectbook", 2000);

if (objectBook.equals(anotherObjectBook)) {
    System.out.println("The books are the same");
} else {
    System.out.println("The books are not the same");
}
            
The books are the same
            

Equals and ArrayList

Various different Java made-up methods make use of the equals method to implement their search functionality. For instance, the contains mehod of class ArrayList compares objects through the equals method. Let us continue to use the Book class we defied for our examples. If our objects do not implement the equals method, we can't use the contains method, for instance. Try out the code below in two different book classes. The first class implements the equals method, the other does not.

ArrayList<Book> books = new ArrayList<Book>();
Book objectBook = new Book("Objectbook", 2000);
books.add(objectBook);

if (books.contains(objectBook)) {
    System.out.println("The object book was found.");
}

objectBook = new Book("Objectbook", 2000);

if (!books.contains(objectBook)) {
    System.out.println("The object book was not found.");
}
            

HashCode Method

The hashCode method takes an object and returns a numeric value, i.e. a hash value. We need numeric values for instance when we use and object as HashMap keys. So far, we have been using only String and Integer objects as HashMap keys, and their hashCode method is implemented by default. Let us make an example where it is not so: let us continue with our book examples and let us start to take note of our books on loan. We want to implement our bookkeeping through Hashmap. The key is the book, and the book's value is a string, which tells the loaner's name:

        HashMap<Book, String> loaners = new HashMap<Book, String>();

        Book objectbook = new Book("Objectbook", 2000);
        loaners.put( objectbook, "Pekka" );
        loaners.put( new Book("Test Driven Development",1999), "Arto" );

        System.out.println( loaners.get( objectbook ) );
        System.out.println( loaners.get( new Book("Objectbook", 2000) );
        System.out.println( loaners.get( new Book("Test Driven Development", 1999) );
            

Print output:

Pekka
null
null
            

We can find the loaner by searching against the same object which was given as HashMap key with the put method. However, if our search item is the same book but a different object, we are not able to find its loaner and we are retured with a null reference. This is again due to the default implementation of the hashCode method of Object class. The default implementation creates an index based on the reference; this means that different objects with the same content receive different hashCode method outputs, and therefore it is not possible to find the right place of the object in the HashMap.

To be sure the HashMap worked in the way we want - i.e. it returned the loaner when the key is an object with the right content (not necessarily the same object as the original value) - the class which works as key must overwrite both the equals method and the hashCode method. The method must be overwritten in such a way, so that it would assign the same numeric value to all objects which have the same content. Some objects with different content may eventually be assigned the same hashCode; however, different content objects should be assigned the same hashCode as rarely as possible, if we want our HashMap to be efficient.

Previously, we have successfully used String objects as HashMap keys, and we can therefore say that the String class has a hashCode implementation which works as expected. Let us delegate the calculation to the String object.

    public int hashCode() {
        return this.name.hashCode();
    }
            

The solution above is quite good; but if name is null, we are thrown a NullPointerException. We can fix this by setting the condition: if the value of the name variable is is null, return value 7. Seven is a value chosen casually, thirteen could have done as well.

    public int hashCode() {
        if (this.name == null) {
            return 7;
        }

        return this.name.hashCode();
    }
            

We can still improve the hashCode method by taking into consideration the book publishing year, in our calculations:

    public int hashCode() {
        if (this.name == null) {
            return 7;
        }

        return this.publishingYear + this.name.hashCode();
    }
            

An additional remark: the output of the hashCode method of HashMap key objects tells us their value slot in the hash construction, i.e. their index in the HashMap. You may now be wondering: "doesn't this lead to a situation where more than one object ends up with the same index in the HashMap?". The answer is yes and no. Even if the hashCode method gave the same value to two different objects, HashMaps are built in such way that various different obejcts may have the same index. In order to distinguish objects with the same index, the key objects of the HashMap must have implemented the equals method. You will find more information about Hashmap implementation in the course Data Structures and Algorithms.

The final Book class now.

public class Book {

    private String name;
    private int publishingYear;

    public Book(String name, int publishingYear) {
        this.name = name;
        this.publishingYear = publishingYear;
    }

    public String getName() {
        return this.name;
    }

    public int getPublishingYear() {
        return this.publishingYear;
    }

    @Override
    public String toString() {
        return this.name + " (" + this.publishingYear + ")";
    }

    @Override
    public boolean equals(Object object) {
        if (object == null) {
            return false;
        }

        if (getClass() != object.getClass()) {
            return false;
        }

        Book compared = (Book) object;

        if (this.publishingYear != compared.getPublishingYear()) {
            return false;
        }

        if (this.name == null || !this.name.equals(compared.getName())) {
            return false;
        }

        return true;
    }

    public int hashCode() {
        if (this.name == null) {
            return 7;
        }

        return this.publishingYear + this.name.hashCode();
    }
}
            

Let us sum up everything again: in order to use a class as HashMap key, we have to define

  • The equals method in a way that objects with the same content will return true when compared, whereas different-content objects shall return false
  • The hashCode method in a way that it assigns the same value to all the objects whose content is regarded as similar

The equals and hashCode methods of our Book class fulfill these two conditions. Now, the problem we faced before is solved, and we can find out the book loaners:

        HashMap<Book, String> loaners = new HashMap<Book, String>();

        Book objectbook = new Book("Objectbook", 2000);
        loaners.put( objectbook, "Pekka" );
        loaners.put( new Book("Test Driven Development",1999), "Arto" );

        System.out.println( loaners.get( objectbook ) );
        System.out.println( loaners.get( new Book("Objectbook", 2000) );
        System.out.println( loaners.get( new Book("Test Driven Development", 1999) );
            

Print output:

Pekka
Pekka
Arto
            

NetBeans allows for the automatic creation of the equals and hashCode methods. From the menu Source -> Insert Code, you can choose equals() and hashCode(). After this, NetBeans asks which object variables the methods shall use.

Car Registration Centre

Registration Plate Equals and HashCode

European registration plates are composed of two parts: the country ID -- one or two letters long -- and possibly a regitration code specific for the country, which in turn is composed of numbers and letters. Registaration plates are defined using the following class:

public class RegistrationPlate {
    // ATTENTION: the object variable types are final, meaning that their value cannot be changed!
    private final String regCode;
    private final String country;

    public RegistrationPlate(String regCode, String country) {
       this.regCode = regCode;
       this.country = country;
    }

    public String toString(){
        return country+ " "+regCode;
    }
}
                

We want to store the registration plates into say ArrayLists, using a HashMap as key. As mentioned before, it means we have to implement the methods equals and hashCode in their class, otherwise they can't work as we want.

Suggestion: take the equals and hashCode models from the Book example above. The registration plate hashCode can be created say combining the hashCodes of the country ID and of the registration code.

Example program:

    public static void main(String[] args) {
        RegistrationPlate reg1 = new RegistrationPlate("FI", "ABC-123");
        RegistrationPlate reg2 = new RegistrationPlate("FI", "UXE-465");
        RegistrationPlate reg3 = new RegistrationPlate("D", "B WQ-431");

        ArrayList<RegistrationPlate> finnish = new ArrayList<RegistrationPlate>();
        finnish.add(reg1);
        finnish.add(reg2);

        RegistrationPlate new = new RegistrationPlate("FI", "ABC-123");
        if (!finnish.contains(new)) {
            finnish.add(new);
        }
        System.out.println("Finnish: " + finnish);
        // if the equals method hasn't been overwritten, the same registration plate is repeated in the list

        HashMap<RegistrationPlate, String> owners = new HashMap<RegistrationPlate, String>();
        owners.put(reg1, "Arto");
        owners.put(reg3, "Jürgen");

        System.out.println("owners:");
        System.out.println(owners.get(new RegistrationPlate("FI", "ABC-123")));
        System.out.println(owners.get(new RegistrationPlate("D", "B WQ-431")));
        // if the hashCode hasn't been overwritten, the owners are not found
    }
                

If equals hashCode have been implemented well, the output should look like this:

Finnish: [FI ABC-123, FI UXE-465]
owners:
Arto
Jürgen

                

The Owner, Based of the Registration Plate

Implement the class VehicleRegister which has the following methods:

  • public boolean add(RegistrationPlate plate, String owner), which adds the parameter owner of the car which corresponds to the parameter registration plate. The method returns true if the car had no owner; if the car had an owner already, the method returns false and it doesn't do anything
  • public String get(RegistrationPlate plate), which returns the car owner which corresponds to the parameter register number. If the car was not registered, it returns null
  • public boolean delete(RegistrationPlate plate), which delete the information connected to the parameter registration plate. The method returns true if the information was deleted, and false if there was no information connetted to the parameter in the register.

Attention: the vehicle register has to store the owner information into a HashMap<RegistrationPlate, String> owners object variable!

More for the Vehicle Register

Add still the following methods to your VehicleRegister:

  • public void printRegistrationPlates(), which prints out all the registration plates stored
  • public void printOwners(), which prints all the car owners stored. Each owner's name has to be printed only once, even though they had more than one car

Interface

Interface is an instrument we have to define the functionality our classes should have. Interfaces are defined as normal Java classes, but instead of the definition "public class ...", we write "public interface ...". The interfaces influence class behaviour by defining the method names and return values, but they do not contain method implementation. The access modifier is not specified, because it is always public. Let us have a look at the interface Readable, which defines whether an object can be read.

public interface Readable {
    String read();
}
            

The interface Readable defines the method read(), which returns a string object. The classes which implement an interface decide in which way the methods defined in the interface have to be implemented, in the end. A class implements an interface by adding the keyword implements between the class and the interface name. Below, we create the class SMS which implements Readable interface.

public class SMS implements Readable {
    private String sender;
    private String content;

    public SMS(String sender, String content) {
        this.sender = sender;
        this.content = content;
    }

    public String getSender() {
        return this.sender;
    }

    public String read() {
        return this.content;
    }
}
            

Because the class SMS implements the interface Readable (public class SMS implements Readable), the class SMS must implement the method public String read(). The implementations of methods defined in the interface must always have public access.

An interface is a behavioural agreement. In order to implement the behaviour, the class must implement the methods defined by the interface. The programmer of a class which implements an interface has to define what the behaviour will be like. Implementing an interface means to agree that the class will offer all the actions defined by the interface, i.e. the behaviour defined by the interface. A class which implements an interface but does not implement some of the interface methods can not exist.

Let us implement another class which implements the Readable interface, in addition to our SMS class. The class EBook is the electronic implementation of a book, and it contains the book name and page number. The EBook reads one page at time, and the public String read() method always returns the string of the following page.

public class EBook implements Readable {
    private String name;
    private ArrayList<String> pages;
    private int pageNumber;

    public EBook(String name, ArrayList<String> pages) {
        this.name = name;
        this.pages = pages;
        this.pageNumber = 0;
    }

    public String getName() {
        return this.name;
    }

    public int howManyPages() {
        return this.pages.size();
    }

    public String read() {
        String page = this.pages.get(this.pageNumber);
        nextPage();
        return page;
    }

    private void nextPage() {
        this.pageNumber = this.pageNumber + 1;
        if(this.pageNumber % this.pages.size() == 0) {
            this.pageNumber = 0;
        }
    }
}
            

Classes which implement interfaces generate objects as well as normal classes, and they can be used as ArrayList types too.

    SMS message = new SMS("ope", "Awesome stuff!");
    System.out.println(message.read());

    ArrayList<SMS> messages = new ArrayList<SMS>();
    messages.add(new SMS("unknown number", "I hid the body.");
            
Awesome stuff!
    ArrayList<String> pages = new ArrayList<String>();
    pages.add("Split your method into short clear chunks.");
    pages.add("Devide the user interface logic from the application logic.");
    pages.add("At first, always code only a small program which solves only a part of the problem.");
    pages.add("Practice makes perfect. Make up your own fun project.");

    EBook book = new EBook("Programming Hints.", pages);
    for(int page = 0; page < book.howManyPages(); page++) {
        System.out.println(book.read());
    }
            

Split your method into short clear chunks.
Divide the user interface logic from the application logic.
At first, always code only a small program which solves only a part of the problem.
Practice makes perfect. Make up your own fun project.
            

NationalService

In the exercise layout, you find the premade interface NationalService, which contains the following operations:

  • the method int getDaysLeft() which returns the number of days left on service
  • the method void work(), which reduces the working days by one. The working days number can not become negative.
public interface NationalService {
    int getDaysLeft();
    void work();
}
                

CivilService

Create the class CivilService which implements your NationalService interface. The class constructor is without parameter. The class has the object variable daysLeft which is initialised in the constructor receiving the value 362.

MilitaryService

Create the class MilitaryService which implements your NationalService interface. The class constructor has one parameter, defining the days of service (int daysLeft).

An Interface as Variable Type

When we create a new variable we always specify its type. There are two types of variable types: primitive-type variables (int, double, ...) and reference-type (all objects). As far as reference-type variables are concerned, their class has also been their type, so far.

    String string = "string-object";
    SMS message = new SMS("teacher", "Something crazy is going to happen");
            

The type of an object can be different from its class. For instance, if a class implements the interface Readable, its type is Readable, too. For instance, since the class SMS implements the interface Readable, it has two types: SMS and Readable.

    SMS message = new SMS("teacher", "Awesome stuff!");
    Readable readable = new SMS("teacher", "The SMS is Readable!");
            
    ArrayList<String> pages = new ArrayList<String>();
    pages.add("A method can call itself.");

    Readable book = new EBook("Recursion Principles", pages);
    for(int page = 0; page < ((EBook)book).howManyPages(); page++) {
        System.out.println(book.read());
    }
            

Because an interface can be used as type, it is possible to create a list containing interface-type objects.

    ArrayList<Readable> numberList = new ArrayList<Readable>();

    numberList.add(new SMS("teacher", "never been programming before..."));
    numberList.add(new SMS("teacher", "gonna love it i think!"));
    numberList.add(new SMS("teacher", "give me something more challenging! :)"));
    numberList.add(new SMS("teacher", "you think i can do it?"));
    numberList.add(new SMS("teacher", "up here we send several messages each day"));

    for (Readable readable: numberList) {
        System.out.println(readable.read());
    }
            

The EBook class implements the interface Readable. However, notice that even though the type of the class EBook is an interface, EBook is not the type of all the classes which implement the Readable interface. It is possible to assign an EBook object to a Readable variable, but the assignment does not work in the opposite way without a particular type change.

    Readable readable = new Readable("teacher", "The SMS is Readable!"); // works
    SMS message = readable; // not possible

    SMS transformedMessage = (Message) Readable; // works
            

Type casting works if and only if the variable's type is really what we try to change it into. Type casting is not usually a best practice; one of the only cases where that is legitimate is in connection with the equals method.

An Interface as Method Parameter

The real use of interfaces becomes clear when we use them for the type of a method parameter. Because interfaces can be used as variable type, they can be used in method calls as parameter type. For instance, the below method print of class Printer receives a Readable variable.

public class Printer {
    public void print(Readable readable) {
        System.out.println(readable.read());
    }
}
          

The real value of the print method of class Printer is that its parameter can be whatever class instance which implements our Readable interface. When we call the method of an object, the method will work regardless of the class of this object, as long as the object implements Readable.

    SMS message = new SMS("teacher", "Huhhuh, this printer is able to print them, actually!");
    ArrayList<String> pages = new ArrayList<String>();
    pages.add("{3, 5} are the numbers in common between {1, 3, 5} and {2, 3, 4, 5}.");

    EBook book = new EBook("Introduction to University Mathematics.", pages);

    Printer printer = new Printer();
    printer.print(SMS);
    printer.print(book);
          
Wow, this printer is able to print them, actually!
{3, 5} are the numbers in common between {1, 3, 5} and {2, 3, 4, 5}.
          

Let us implement another numberList class, where we can add interesting readable stuff. The class has an ArrayList instance as object variable where we save things to read. We add items to our number list through the add method which receives a Readable variable as parameter.

public class NumberList {
    private ArrayList<Readable> readables;

    public NumberList() {
        this.readables = new ArrayList<Readable>();
    }

    public void add(Readable readable) {
        this.readables.add(readable);
    }

    public int howManyReadables() {
        return this.readables.size();
    }
}
          

Number lists are usually readable, so we can implement the Readable interface to the NumberList class. The number list read method reads all the objects of the readables list, and it adds them one by one to a string which is returned by the read() method.

public class NumberList implements Readable {
    private ArrayList<Readable> readables;

    public NumberList() {
        this.readables = new ArrayList<Readable>();
    }

    public void add(Readable readable) {
        this.readables.add(readable);
    }

    public int howManyReadables() {
        return this.readables.size();
    }

    public String read() {
        String read = "";
        for(Readable readable: this.readables) {
            read += readable.read() + "\n";
        }

        this.readables.clear();
        return read;
    }
}
          
    NumberList myList = new NumberList();
    joelList.add(new SMS("matti", "have you already written the tests?"));
    joelList.add(new SMS("matti", "did you have a look at the submissions?"));

    System.out.println("Joel has " + joelList.howManyReadables() + " messages to read");
          
Joel has got 2 messages to read
          

Because the type of NumerList is Readable, we can add NumerList objects to our number list, too. In the example below, Joel has a lot of messages to read, luckily Mikael deals with it and reads the messages on behalf of Joel.

    NumberList joelList = new NumberList();
    for (int i = 0; i < 1000; i++) {
        joelList.add(new SMS("matti", "have you already written the tests?"));
    }

    System.out.println("Joel has " + joelList.howManyReadables() + " messages to read");
    System.out.println("Let's delegate some reading to Mikael");

    NumberList mikaelList = new NumberList();
    mikaelList.add(joelList);
    mikaelList.read();

    System.out.println();
    System.out.println("Joel has " + joelList.howManyReadables() + " messages to read");
          
Joel has 1000 messages to read
Let's delegate some reading to Mikael

Joel has 0 messages to read
          

The read method which is called in connection to Mikael's list parses all the Readable objects contained in the list, and calls their read method. At the end of each read method call the list is cleared. In other words, Joel's number list is cleared as soon as Mikael reads it.

At this point, there are a lot of references; it would be good to draw down the objects and try to grasp how the read method call connected to mikaelList works!

Boxes and Things

ToBeStored

We need storage boxes when we move to a new apartment. The boxes are used to store different things. All the things which are stored in the boxes have to implement the following interface:

public interface ToBeStored {
    double weight();
}
            

Add the interface to your program. New interfaces are added almost in the same way as classes: you choose new Java interface instead of new Java class.

Create two classes which implement the interface Book and CD. Book receives its writer (String), name (String), and weight (double), all as parameter. CD's parameter contains its artist (String), title (String), and publishing year (int). All CDs weigh 0.1 kg.

Remember that the classes also have to implement the interface ToBeStored. The classes have to work in the following way:

    public static void main(String[] args) {
        Book book1 = new Book("Fedor Dostojevski", "Crime and Punishment", 2);
        Book book2 = new Book("Robert Martin", "Clean Code", 1);
        Book book3 = new Book("Kent Beck", "Test Driven Development", 0.5);

        CD cd1 = new CD("Pink Floyd", "Dark Side of the Moon", 1973);
        CD cd2 = new CD("Wigwam", "Nuclear Nightclub", 1975);
        CD cd3 = new CD("Rendezvous Park", "Closer to Being Here", 2012);

        System.out.println(book1);
        System.out.println(book2);
        System.out.println(book3);
        System.out.println(cd1);
        System.out.println(cd2);
        System.out.println(cd3);
    }
            

Print output:

Fedor Dostojevski: Crime and Punishment
Robert Martin: Clean Code
Kent Beck: Test Driven Development
Pink Floyd: Dark Side of the Moon (1973)
Wigwam: Nuclear Nightclub (1975)
Rendezvous Park: Closer to Being Here (2012)
            

Attention! The weight is not reported here.

Box

Create the class box, which has to store Things that implement the interface ToBeStored. The box receives as constructor the maximum weight, expressed in kilograms. The box can't be added more things than its maximum capacity allows for. The weight of the things contained in the box can never exceed the box maximum capacity.

The following example clarifies the box use:

    public static void main(String[] args) {
        Box box = new Box(10);

        box.add( new Book("Fedor Dostojevski", "Crime and Punishment", 2) ) ;
        box.add( new Book("Robert Martin", "Clean Code", 1) );
        box.add( new Book("Kent Beck", "Test Driven Development", 0.7) );

        box.add( new CD("Pink Floyd", "Dark Side of the Moon", 1973) );
        box.add( new CD("Wigwam", "Nuclear Nightclub", 1975) );
        box.add( new CD("Rendezvous Park", "Closer to Being Here", 2012) );

        System.out.println( box );
    }
            

Printing:

Box: 6 things, total weight 4.0 kg
            

Note: because the weights are represented as double, the rounding can cause small mistakes. You don't need to care about it when you do the exercise.

Box weight

If you created the object variable double weght in your box which records the weight of your things, replace it now with a method which calculates the weight:

public class Box {
    //...

    public double weight() {
        double weight = 0;
        // it calculates the total weight of the things which had been stored
        return weight;
    }
}
            

When you need the box weight -- if you have to add a new thing, for instance -- you can simply call the method which calculates it.

In fact, the method could work well even though it retured the value of an object variable. However, we train a situation where you don't need to maintain the object variable explicitly, but you can calculate it when you need it. With the following exercise, the information stored in a box object variable would not necessarily work properly, however. Why?

Boxes are Stored too!

Implementing the interface ToBeStored requires that the class has the method double weight(). In fact, we just added this method to Box. Boxes can be stored!

Boxes are objects where we can store object which implement the interface ToBeStored. Boxes also implement this interface. This means that you can also put boxes inside your boxes!

Try this out: create a couple of boxes in your program, put things into the boxes and put smaller boxes into the bigger ones. Try also what happens when you put a box into itself. Why does it happen?

An Interface as Method Return Value

As well as any other variable type, an interface can also be used as method return value. Below you find Factory, which can be used to produce different objects that implement the interface Item. In the beginning, Factory produces books and disks at random.

   public class Factory {
      public Factory(){
          // Attention: it is not necessary to write an empty constructor if there are no other constructors in the class.
      // In such cases, Java creates a default constructor, i.e a constructor without parameter
      }

       public Item produceNew(){
           Random random = new Random();
           int num = random.nextInt(4);
           if ( num==0 ) {
               return new CD("Pink Floyd", "Dark Side of the Moon", 1973);
           } else if ( num==1 ) {
               return new CD("Wigwam", "Nuclear Nightclub", 1975);
           } else if ( num==2 ) {
               return new Book("Robert Martin", "Clean Code", 1 );
           } else {
               return new Book("Kent Beck", "Test Driven Development", 0.7);
           }
       }
   }
        

It is possible to use our Factory without knowing precisely what kind of classes are present in it, as long as they all implement Item. Below you find the class Packer which can be used to get a boxful of items. The Packer knows the factory which produces its Items:

   public class Packer {
       private Factory factory;

       public Packer(){
            factory = new Factory();
       }

       public Box giveABoxful() {
            Box box = new Box(100);

            for ( int i=0; i < 10; i++ ) {
                Item newItem = factory.produceNew();
                box.add(newItem);
            }

            return box;
       }
   }
        

Because the packer doesn't know the classes which implement the Item interface, it is possble to add new classes which implement the interface without having to modify the packer. Below, we create a new class which implements our Item interface - ChocolateBar. Our Factory was modified to produce chocolate bars in addition to books and CDs. The class Packer works fine with the extended factory version, without having to change it.

   public class ChocolateBar implements Item {
      // we don't need a constructor because Java is able to generate a default one!

      public double weight(){
         return 0.2;
      }
   }

   public class Factory {
      // we don't need a constructor because Java is able to generate a default one!

       public Item produceNew(){
           Random random = new Random();
           int num = random.nextInt(5);
           if ( num==0 ) {
               return new CD("Pink Floyd", "Dark Side of the Moon", 1973);
           } else if ( num==1 ) {
               return new CD("Wigwam", "Nuclear Nightclub", 1975);
           } else if ( num==2 ) {
               return new Book("Robert Martin", "Clean Code", 1 );
           } else if ( num==3 ) {
               return new Book("Kent Beck", "Test Driven Development", 0.7);
           } else {
               return new ChocolateBar();
           }
       }
   }
        

Using interfaces while programming permits us to reduce the number of dependences among our classes. In our example, Packer is not dependent on the classes which implement Item interface, it is only dependent on the interface itself. This allows us to add classes wihout having to change the class Packer, as long as they implement our interface. We can even add classes that implement the interface to the methods which make use of our packer without compromising the process. In fact, less dependences make it easy to extend a program.

Made-Up Interfaces

Java API offers a sensible number of made-up interfaces. Below, we get to know some of Java's most used interfaces: List, Map, Set and Collection.

List

The List interface defines lists basic functionality. Because the class ArrayList implements the List interface, it can also be initialized through the List interface.

List<String> strings = new ArrayList<String>();
strings.add("A String object within an ArrayList object!");
        

As we notice from the List interface Java API, there are a lot of classes which implement the interface List. A list construction which is familiar to hakers like us is the linked list. A linked list can be used through the List interface in the same way as the objects created from ArrayList.

List<String> strings = new LinkedList<String>();
strings.add("A string object within a LinkedList object!");
        

Both implementations of the List interface work in the same way, in the user point of view. In fact, the interface abstracts their internal functionality. ArrayList and linkedList internal construction is evidently different, anyway. ArrayList saves the objects into a table, and the search is quick with a specific index. Differently, LinkedList builds up a list where each item has a reference to the following item. When we search for an item in a linked list, we have to go through all the list items till we reach the index.

When it comes to bigger lists, we can point out more than evident performance differences. LinkedList's strength is that adding new items is always fast. Differently, behind ArrayList there is a table which grows as it fills up. Increasing the size of the table means creating a new one and copying there the information of the old. However, searching against an index is extremely fast with an ArrayList, whereas we have to go thourgh all the list elements one by one before reaching the one we want, with a LinkedList. More information about data structures such as ArrayList and LinkedList internal implementation comes with the course Data structures and algorithms.

In our programming course you will rather want to choose ArrayList, in fact. Programming to interface is worth of it, anyway: implement your program so that you'll use data structures via interfaces.

Map

The Map Interface defines HashMap basic fuctionality. Because HashMaps implement the Map interface, it is possible to initialize them trough the Map interface.

Map<String, String> translations = new HashMap<String, String>();
translations.put("gambatte", "tsemppiä");
translations.put("hai", "kyllä");
        

You get HashMap keys thourgh the method keySet.

Map<String, String> translations = new HashMap<String, String>();
translations.put("gambatte", "good luck");
translations.put("hai", "yes");

for(String key: translations.keySet()) {
    System.out.println(key + ": " + translations.get(key));
}
        
gambatte: good luck
hai: yes
        

The keySet method returns a set made of keys which implement Set interface. The set which implement the Set interface can be parsed with a for-each loop. HashMap values are retrieved through the values method, which returns a set of values which implement the Collection interface. We should now focus on Set and Collection interfaces.

Set

The Set interface defines the functionality of Java's sets. Java's sets always contain 0 or 1 element of a certain type. Among the others, HashSet is one of the classes which implement the Set interface. We can parse a key set through a for-each loop, in the following way

Set<String> set = new HashSet<String>();
set.add("one");
set.add("one");
set.add("two");

for (String key: set) {
    System.out.println(key);
}
        
one
two
        

Notice that HashSet is not concerned on the order of its keys.

Collection

The Collection interface defines the functionality of collections. Among the others, Java's lists and sets are collections -- that is, List and Set interfaces implement the Collection interface. Collection interface provides methods to check object existence (the contains method) and to check the collection size (size method). We can parse any class which implements the Collection interface with a for-each loop.

We now create a HashMap and parse first its keys, and then its values.

Map<String, String> translations = new HashMap<String, String>();
translations.put("gambatte", "good luck");
translations.put("hai", "yes");

Set<String> keys = translations.keySet();
Collection<String> keySet = keys;

System.out.println("Keys:");
for(String key: keySet) {
    System.out.println(key);
}

System.out.println();
System.out.println("Values:");
Collection<String> values = translations.values();
for(String value: values) {
    System.out.println(value);
}
        
Keys:
gambatte
hai

Values:
yes
good luck
        

The following example would have produced the same output, too.

Map<String, String> translations = new HashMap<String, String>();
translations.put("gambatte", "good luck");
translations.put("hai", "yes");

System.out.println("Keys:");
for(String key: translations.keySet()) {
    System.out.println(key);
}

System.out.println();
System.out.println("Values:");
for(String value: translations.values()) {
    System.out.println(value);
}
        

In the following exercise we build an online shop, and we train to use classes through their interfaces.

Online Shop

Next, we create some programming components which are useful to manage an online shop.

Storehouse

Create the class Storehouse with the following methods:

  • public void addProduct(String product, int price, int stock), adding to the storehouse a product whose price and number of stocks are the parameter values
  • public int price(String product) returns the price of the parameter product; if the product is not available in the storehouse, the method returns -99

Inside the storehouse, the prices (and soon also the stocks) of the products have to be stored into a Map<String, Integer> variable! The type of the object so created can be HashMap, but you should use the interface Map for the variable type (see 40.4.2)

The next example clarifies storehouse use:

        Storehouse store = new Storehouse();
        store.addProduct("milk", 3, 10);
        store.addProduct("coffee", 5, 7);

        System.out.println("prices:");
        System.out.println("milk:  " + store.price("milk"));
        System.out.println("coffee:  " + store.price("coffee"));
        System.out.println("sugar: " + store.price("sugar"));
            

Prints:

prices:
milk:  3
coffee:  5
sugar: -99
            

Product Stock

Store product stocks in a similar Map<String, Integer> variable as the one you used for their prices. Fill the Storehouse with the following methods:

  • public int stock(String product) returns the stock of the parameter product.
  • public boolean take(String product) decreases the stock of the parameter product by one, and it returns true if the object was available in the storehouse. If the product was not in the storehouse, the method returns false, the product stock cannot go below zero.

An example of how to use the storehouse now:

        Storehouse store = new Storehouse();
        store.addProduct("coffee", 5, 1);

        System.out.println("stocks:");
        System.out.println("coffee:  " + store.stock("coffee"));
        System.out.println("sugar: " + store.stock("sugar"));

        System.out.println("we take a coffee " + store.take("coffee"));
        System.out.println("we take a coffee " + store.take("coffee"));
        System.out.println("we take sugar " + store.take("sugar"));

        System.out.println("stocks:");
        System.out.println("coffee:  " + store.stock("coffee"));
        System.out.println("sugar: " + store.stock("sugar"));
            

Prints:

stocks:
coffee:  1
sugar: 0
we take coffee true
we take coffee false
we take sugar false
stocks:
coffee:  0
sugar: 0
            

Listing the Products

Let's add another method to our storehouse:

  • public Set<String> products() returns a name set of the products contained in the storehouse

The method can be implemented easily. Using the Map's method keySet, you can get the storehouse products by asking for their prices or stocks.

An example of how to use the storehose now:

        Storehouse store = new Storehouse();
        store.addProduct("milk", 3, 10);
        store.addProduct("coffee", 5, 6);
        store.addProduct("buttermilk", 2, 20);
        store.addProduct("jogurt", 2, 20);

        System.out.println("products:");
        for (String product : store.products()) {
            System.out.println(product);
        }
            

Prints:

products:
buttermilk
jogurt
coffee
milk
            

Purchase

We add purchases to our shopping basket. With purchase we mean a specific number of a specific product. For instance, we can put into our shopping basket either a purchase corresponding to one bread, or a purchase corresponding to 24 coffees.

Create the class Purchase with the following functionality:

  • a constructor public Purchase(String product, int amount, int unitPrice), which creates a purchase corresponding the parameter product. The product unit amount of purchase is clarified by the parameter amount, and the third parameter is the unit price
  • public int price(), which returns the purchase price. This is obtained by raising the unit amount by the unit price
  • public void increaseAmount() increases by one the purchase unit amount
  • public String toString() returns the purchase in a string form like the following

An example of how to use a purchase

        Purchase purchase = new Purchase("milk", 4, 2);
        System.out.println( "the total price of a purchase containing four milks is " + purchase.price() );
        System.out.println( purchase );
        purchase.increaseAmount();
        System.out.println( purchase );
            

Prints:

the total price of a purchase containing four milks is 8
milk: 4
milk: 5
            

Note: toString follows the pattern product: amount, but the price is not reported!

Shopping Basket

Finally, we can implement our shopping basket class!

The shopping basket stores its products as Purchase objects. The shopping basket has to have an object variable whose type is either Map<String, Purchase> or List<Purchase>. Do not create any other object variable for your shopping basket in addition to the Map or List needed to store purchases.

Attention: if you store a Purchase object in a Map helping variable, it will be useful to use the Map method values() in this and in the following exercise; with it, it's easy to go through all the stored Purchase objects.

Let's create a constructor without parameter for our shopping basket, as well as the following methods:

  • public void add(String product, int price) adds a purchase to the shopping basket; the purchase is defined by the parameter product, and its price is the second parameter.
  • public int price() returns the shopping basket total price

Example code of using basket

        ShoppingBasket basket = new ShoppingBasket();
        basket.add("milk", 3);
        basket.add("buttermilk", 2);
        basket.add("cheese", 5);
        System.out.println("basket price: " + basket.price());
        basket.add("computer", 899);
        System.out.println("basket price: " + basket.price());
            

Prints:

basket price: 10
basket price: 909
            

Printing out the Shopping Basket

Let's create the method public void print() for our shopping basket. This prints out the Purchase objects which are contained by the basket. The printing order is not important. The output of the shopping basket in the previous example would be:

butter: 1
cheese: 1
computer: 1
milk: 1
            

Note that the number stands for the unit amount of the products, not for their price!

Only One Purchase Object for One Product

Let's update our Shopping Basket; if the basket already contains the product which we add, we don't create a new Purchase object, but we update the Purchase object corresponding to the existing product by calling its method increaseAmount().

Example:

        ShoppingBasket basket = new ShoppingBasket();
        basket.add("milk", 3);
        basket.print();
        System.out.println("basket price: " + basket.price() +"\n");

        basket.add("buttermilk", 2);
        basket.print();
        System.out.println("basket price: " + basket.price() +"\n");

        basket.add("milk", 3);
        basket.print();
        System.out.println("basket price: " + basket.price() +"\n");

        basket.add("milk", 3);
        basket.print();
        System.out.println("basket price: " + basket.price() +"\n");
            

Prints:

milk: 1
basket price: 3

buttermilk: 1
milk: 1
basket price: 5

buttermilk: 1
milk: 2
basket price: 8

buttermilk: 1
milk: 3
basket price: 11
            

This means that first, we add milk and buttermilk, creating new Purchase objects for them. When we add more milk to the basket, we don't create a new purchase object for the milk, but we update the unit amount of the purchase object representing the milk we already have in the basket.

Shop

Now, all the parts of our online shop are ready. Our online shop has a storage house, which contains all products. We have got a shopping basket to manage all our customers. Whenever a customer chooses a purchase, we add it to the shopping basket if the product is available in our storage house. Meanwhile, the storage stocks are reduced by one.

Below, you find a ready-made code body for your online shop. Create the class Shop to your project, and copy the code below into it.

import java.util.Scanner;

public class Shop {

    private Storehouse store;
    private Scanner reader;

    public Shop(Storehouse store, Scanner reader) {
        this.store = store;
        this.reader = reader;
    }

    // the method to deal with a customer in the shop
    public void manage(String customer) {
        ShoppingBasket basket = new ShoppingBasket();
        System.out.println("Welcome to our shop " + customer);
        System.out.println("below is our sale offer:");

        for (String product : store.products()) {
            System.out.println( product );
        }

        while (true) {
            System.out.print("what do you want to buy (press enter to pay):");
            String product = reader.nextLine();
            if (product.isEmpty()) {
                break;
            }

            // here, you write the code to add a product to the shopping basket, if the storehouse is not empty
            // and decreases the storehouse stocks
            // do not touch the rest of the code!

        }

        System.out.println("your purchases are:");
        basket.print();
        System.out.println("basket price: " + basket.price());
    }
}
            

The following main program fills the shop storehouse and manages the customer Pekka:

    Storehouse store = new Storehouse();
    store.addProduct("coffee", 5, 10);
    store.addProduct("milk", 3, 20);
    store.addProduct("milkbutter", 2, 55);
    store.addProduct("bread", 7, 8);

    Shop shop = new Shop(store, new Scanner(System.in));
    shop.manage("Pekka");
            

The shop is almost ready. There are comments in the method public void manage(String customer), showing the part that you should implement. In that point, implement the code to check whether the object the customer wants is available in the storehouse. If so, reduce the storehouse stocks by one unit, and add the product to the shopping basket.

Now you have done something! verkkokauppa.com!

Generics

We speak about Generics in connection to the way classes can conserve objects of genric type. Generics is based on the generic type parameter which is used when we define a class, and which helps us to define the types that have to be chosen when an object is created. A class generics can be defined by setting up the number of type parameters we want. This number is written after the class name and between the greater-than and less-than signs. We now implement our own generic class Slot which be assigned whatever object.

public class Slot<T> {
    private T key;

    public void setValue(T key) {
        this.key = key;
    }

    public T getValue() {
        return key;
    }
}
        

The definition public class Slot<T> tells us that we have to give a type parameter to the constructor of the class Slot. After the constructor call the object variables have to be the same type as what established with the call. We now create a slot which memorizes strings.

    Slot<String> string = new Slot<String>();
    string.setValue(":)");

    System.out.println(string.getValue());
        
:)
        

If we change the type parameter we can create different kinds of Slot ojects, whose purpose is to memorize objects. For instance, we can memorize an integer in the following way:

    Slot<Integer> num = new Slot<Integer>();
    num.setValue(5);

    System.out.println(slot.getValue());
        
5
        

An important part of Java data structures are programmed to be generic. For instance, ArrayList receives one parameter, HashMap two.

    List<String> string = new ArrayList<String>();
    Map<String, String> keyCouples = new HashMap<String, String>();
        

In the future, when you see the type ArrayList<String>, for instance, you know that its internal structure makes use of a generic type parameter.

The Interface which Makes Use of Generics: Comparable

In addition to normal interfaces, Java has interfaces which make use of generics. The internal value types of generic interfaces are defined in the same way as for generic classes. Let us have a look at Java made-up Comparable interface. The Comparable interface defines the compareTo method, which returns the place of this object, in relation to the parameter object (a negative number, 0, or a positive number). If this object is placed before the parameter object in the comparison order, the method returns a negative value, whereas it returns a positive value if it is placed after the parameter object. If the objects are placed at the same place in the comparison order, the method returns 0. With comparison order we mean the object order of magnitude defined by the programmer, i.e. the object order, when they are sorted with the sort method.

One of the advantages of the Comparable interface is that it allows us to sort a list of Comparable type keys by using the standard library method Collections.sort, for instance. Collections.sort uses the compareTo method of a key list to define in which order these keys should be. We call Natural Ordering this ordering technique which makes use of the compareTo method.

We create the class ClubMember, which depicts the young people and children who belong to the club. The members have to eat in order of height, so the club members will implement the interface Comparable. The interface Comparable also takes as type parameter the class which it is compared to. As type parameter, we use the ClubMember class.

public class ClubMember implements Comparable<ClubMember> {
    private String name;
    private int height;

    public ClubMember(String name, int height) {
        this.name = name;
        this.height = height;
    }

    public String getName() {
        return this.name;
    }

    public int getHeigth() {
        return this.height;
    }

    @Override
    public String toString() {
        return this.getName() + " (" + this.getHeigth() + ")";
    }

    @Override
    public int compareTo(ClubMember clubMember) {
        if(this.heigth == clubMember.getHeight()) {
            return 0;
        } else if (this.height > clubMember.getHeight()) {
            return 1;
        } else {
            return -1;
        }
    }
}
        

The interface requires the method compareTo, which returns an integer that tells us the comparison order. Our compareTo() method has to return a negative number if this object is smaller than its parameter object, or zero, if the two members are equally tall. Therefore, we can implement the above compareTo method, in the following way:

    @Override
    public int compareTo(ClubMember clubMember) {
        return this.height - clubMember.getHeight();
    }
        

Sorting club members is easy, now.

    List<ClubMember> clubMembers = new ArrayList<ClubMember>();
    clubMembers.add(new ClubMember("mikael", 182));
    clubMembers.add(new ClubMember("matti", 187));
    clubMembers.add(new ClubMember("joel", 184));

    System.out.println(clubMembers);
    Collections.sort(clubMembers);
    System.out.println(clubMembers);
        
[mikael (182), matti (187), joel (184)]
[mikael (182), joel (184), matti (187)]
        

If we want to sort the members in descending order, we only have to switch the variable order in our compareTo method.

Rich First, Poor Last

You find the pre-made class Person. People have got name and salary information. Make Person implement the Comparable interface, so that the compareTo method would sort the people according to their salary -- rich first, poor last.

Students Sorted by Name

You find the pre-made class Student. Students have got a name. Make Student implement the COmparable interface, so that the compareTo method would sort the students in alphabetic order.

Tip: student names are Strings, the class String is Comparable itself. You can use the String's compareTo method when you implement your Student class. String.compareTo gives a different value to characters according to their case; because of this, String has also got the method compareToIgnoreCase which, in fact, ignores the case while comparing. You can use either of them, when you sort your students.

Sorting Cards

Together with the exercise layout, you find a class whose objects represent playing cards. Cards have got a value and a suit. Card values are 2, 3, ..., 10, J, Q, K and A, and the suits are Spades, Hearts, Diamonds and Clubs. Value and suit are however shown as integers in the objects. Cards have also got a toString method, which is used to print the card value and suit in a "friendly way".

Four constants -- that is public static final variables -- are defined in the class, so that the user didn't need to handle card's suits as numbers:

public class Card {
    public static final int SPADES  = 0;
    public static final int DIAMONDS  = 1;
    public static final int HEARTS = 2;
    public static final int CLUBS   = 3;

    // ...
}
            

Now, instead of writing the number 1, we can use the constant Card.DIAMONDS in our program. In the following example, we create three cards and print them:

  Card first = new Card(2, Card.DIAMONDS);
  Card second = new Card(14, Card.CLUBS);
  Card third = new Card(12, Card.HEARTS);

  System.out.println(first);
  System.out.println(second);
  System.out.println(third);
            

Prints:

2 of Diamonds
A of Clubs
Q of Hearts
            

Note: using constants as shown above is not the best way deal with things. Later on in the course we learn a better way to show suits!

Comparable Cards

Make your Cards class Comparable. Implement the compareTo method so that cards would be sorted in ascending order according to their value. If the value of two classes have got the same values, we compare them against their suit in ascending order: spades first, diamonds second, hearts third, and clubs last.

The smallest card would then be the two spades and the greatest would be the clubs ace.

Hand

Next, let's create the class Hand which represents the player hand set of cards. Create the following method to the hand:

  • public void add(Card card) adds a card to the hand
  • public void print() prints the cards in the hand following the below example pattern
  Hand hand = new Hand();

  hand.add( new Card(2, Card.SPADES) );
  hand.add( new Card(14, Card.CLUBS) );
  hand.add( new Card(12, Card.HEARTS) );
  hand.add( new Card(2, Card.CLUBS) );

  hand.print();
            

Prints:

2 of Spades
A of Clubs
Q of Hearts
2 of Clubs
            

Store the hand cards into an ArrayList.

Sorting the Hand

Create the method public void sort() for your hand, which sorts the cards in the hand. After being sorted, the cards are printed in order:

  Hand hand = new Hand();

  hand.add( new Card(2, Card.SPADES) );
  hand.add( new Card(14, Card.CLUBS) );
  hand.add( new Card(12, Card.HEARTS) );
  hand.add( new Card(2, Card.CLUBS) );

  hand.sort();

  hand.print();
            

Prints:

2 of Spades
2 of Clubs
Q of Hearts
A of Clubs
            

Comparing Hands

In one card game, the most valuable hand, where the sum of the cards value is the biggest. Modify the class Hand so that it could be compared according to this criterion: make it implement the interface Comparable<Hand>.

Below, you find an example of a program where we compare hands:

  Hand hand1 = new Hand();

  hand1.add( new Card(2, Card.SPADES) );
  hand1.add( new Card(14, Card.CLUBS) );
  hand1.add( new Card(12, Card.HEARTS) );
  hand1.add( new Card(2, Card.CLUBS) );

  Hand hand2 = new Hand();

  hand2.add( new Card(11, Card.DIAMONDS) );
  hand2.add( new Card(11, Card.CLUBS) );
  hand2.add( new Card(11, Card.HEARTS) );

  int comparison = hand1.compareTo(hand2);

  if ( comparison < 0 ) {
    System.out.println("the most valuable hand contains the cards");
    hand2.print();
  } else if ( comparison > 0 ){
    System.out.println("the most valuable hand contains the cards");
    hand1.print();
  } else {
    System.out.println("the hands are equally valuable");
  }
            

Prints:

the most valuable hand contains the cards
J of Diamonds
J of Spades
J of Hearts
            

Sorting the Cards against Different Criteria

What about if we wanted to sort cards in a slightly different way, sometimes; for instance, what about if we wanted to have all same-suit cards in a raw? The class can have only one compareTo method, which means that we have to find out other ways to sort cards against different orders.

If you want to sort your cards in optional orders, you can make use of different classes which execute the comparison. These classes have to implement the interface Comparator<Card>. The object which determines the sorting order compares two cards it receives as parameter. There is only one method, a compare(Card card1, Card card2) method which has to return a negative value if card1 is before card2, a positive value if card2 is before card1, and 0 otherwise.

The idea is creating a specific comparison class for each sorting order; for instance, a class which places same suit cards together in a row:

import java.util.Comparator;

public class SortAgainstSuit implements Comparator<Card> {
    public int compare(Card card1, Card card2) {
        return card1.getSuit()-card2.getSuit();
    }
}
            

Sorting against suit works in the same way as the card method compareTo thought for suits, that is spades first, diamonds second, hearts third, clubs last.

Sorting is still possible through the Collections' sort method. The method now receives as second parameter an object of the class that determines the sorting order:

  ArrayList<Card> cards = new ArrayList<Card>();

  cards.add( new Card(3, Card.CLUBS) );
  cards.add( new Card(2, Card.DIAMONDS) );
  cards.add( new Card(14, Card.CLUBS) );
  cards.add( new Card(12, Card.HEARTS) );
  cards.add( new Card(2, Card.CLUBS) );

  SortAgainstSuit suitSorter = new SortAgainstSuit();
  Collections.sort(cards, suitSorter );

  for (Card c : cards) {
    System.out.println( c );
  }
          

Prints:

2 of Diamonds
Q of Hearts
3 of Clubs
A of Clubs
2 of Clubs
          

The sorting object can also be created directly together with the sort call:

  Collections.sort(cards, new SortAgainstSuit() );
          

Further information about comparator classes in here.

Create now the class SortAgainstSuitAndValue which iplements the Comparator interface and sorts cards as it is done in the example above, plus same suit cards are also sorted according to their value.

Sort Against Suit

Add the method public void sortAgainstSuit() to the class Hand; when the method is called the hand's cards are sorted according to the comparator SortAgainstSuitAndValue. After sorting them, the cards are printed in order:

  Hand hand = new Hand();

  hand.add( new Card(12, Card.HEARTS) );
  hand.add( new Card(4, Card.CLUBS) );
  hand.add( new Card(2, Card.DIAMONDS) );
  hand.add( new Card(14, Card.CLUBS) );
  hand.add( new Card(7, Card.HEARTS) );
  hand.add( new Card(2, Card.CLUBS) );

  hand.sortAgainstSuit();

  hand.print();
          

Prints:

2 of Diamonds
7 of Hearts
Q of Hearts
2 of Clubs
4 of Clubs
A of Clubs
          

Collections

The class library Collections is Java's general-purpose library for collection classes. As we can see, Collections provides methods to sort objects either through the interface Comparable or Comparator. In addition to sorting, we can use this class library to retrieve the minimum and maximum values (through the methods min and max, respectively), retrieve a specific value (binarySearch method), or reverse the list (reverse method).

Search

The Collections class library provides a made-up binary search functionality. The method binarySearch() returns the index of our searched key, if this is found. If the key is not found, the search algorithm returns a negative value. The method binarySearch() makes use of the Comparable interface to retieve objects. If the object's compareTo() method returns the value 0, i.e. if it is the same object, the key is considered found.

Our ClubMember class compares people's heights in its compareTo() method, i.e. we look for club members whose height is the same while we parse our list.

    List<ClubMember> clubMembers = new ArrayList<ClubMember>();
    clubMembers.add(new ClubMember("mikael", 182));
    clubMembers.add(new ClubMember("matti", 187));
    clubMembers.add(new ClubMember("joel", 184));

    Collections.sort(clubMembers);

    ClubMember wanted = new ClubMember("Name", 180);
    int index = Collections.binarySearch(clubMembers, wanted);
    if (index >= 0) {
        System.out.println("A person who is 180 centimiters tall was found at index " + index);
        System.out.println("name: " + clubMembers.get(index).getName());
    }

    wanted = new ClubMember("Name", 187);
    int index = Collections.binarySearch(clubMembers, wanted);
    if (index >= 0) {
        System.out.println("A person who is 187 centimiters tall was found at index " + index);
        System.out.println("name: " + clubMembers.get(index).getName());
    }
      

The print output is the following:

A person who is 187 centimiters tall was found at index 2
name: matti
      

Notice that we also called the method Collections.sort(), in our example. This is because binary search cannot be done if our table or list are not already sorted up.

Ski Jumping

Once again, you can train to build the program structure yourself; the appearance of the user interface and its functionality are pre-defined.

Note: you can create only one Scanner object lest the tests fail. Also, no not use static variables: the tests execute your program many different times, and the static variable values left from the previous execution would possibly disturb them!

Ski jumping is a beloved sport for Finns; they attempt to land as far as possible down the hill below, in the most stylish way. In this exercise, you create a simulator for a ski jumping tournament.

First, the simulator asks the user for the jumper names. If the user inputs an empty string (i.e. presses enter), we move to the jumping phase. In the jumping phase, the jumpers jump one by one in reverse order according to their points. The jumper with the less points always jumps first, then the ones with more points, till the person with the most points.

The total points of a jumper are calculated by adding up the points from their jumps. Jump points are decided in relation to the jump length (use a random integer between 60-120) and judge decision. Five judges vote for each jump (a vote is a random number between 10-20). The judge decision takes into consideration only three judge votes: the smallest and the greatest votes are not taken into account. For instance, if Mikael jumps 61 meters and the judge votes are 11, 12, 13, 14, and 15, the total points for the jump are 100.

There are as many rounds as the user wants. When the user wants to quit, we print the tournament results. The tournament results include the jumpers, the jumper total points, and the lengths of the jumps. The final results are sorted against the jumper total points, and the jumper who received the most points is the first.

Among the other things, you will need the method Collections.sort and Collections.reverse. First, you should start to wonder what kind of classes and objects there could be in your program. Also, it would be good to arrive to a situation where your user interface is the only class with printing statements.

Kumpula ski jumping week

Write the names of the participants one at a time; an empty string brings you to the jumping phase.
  Participant name: Mikael
  Participant name: Mika
  Participant name:

The tournament begins!

Write "jump" to jump; otherwise you quit: jump

Round 1

Jumping order:
  1. Mikael (0 points)
  2. Mika (0 points)

Results of round 1
  Mikael
    length: 95
    judge votes: [15, 11, 10, 14, 14]
  Mika
    length: 112
    judge votes: [14, 12, 18, 18, 17]

Write "jump" to jump; otherwise you quit: jump

Round 2

Jumping order:
  1. Mikael (134 points)
  2. Mika (161 points)

Results of round 2
  Mikael
    length: 96
    judge votes: [20, 19, 15, 13, 18]
  Mika
    length: 61
    judge votes: [12, 11, 15, 17, 11]

Write "jump" to jump; otherwise you quit: jump

Round 3

Jumping order:
  1. Mika (260 points)
  2. Mikael (282 points)

Results of round 3
  Mika
    length: 88
    judge votes: [11, 19, 13, 10, 15]
  Mikael
    length: 63
    judge votes: [12, 19, 19, 12, 12]

Write "jump" to jump; otherwise you quit: quit

Thanks!

Tournament results:
Position    Name
1           Mikael (388 points)
            jump lengths: 95 m, 96 m, 63 m
2           Mika (387 points)
            jump lengths: 112 m, 61 m, 88 m
          

Note1: it is essential that the user interface works exactly as displayed above; for instance, the number of spaces at the beginning of the lines must be right. The space at the beginning of the lines must be made of spaces, the tests do not work if you use tabulatation. Also, it is good that you copy the text which has to be printed by your program and you paste it into your code; you can copy it either from the exercise layout or from the test error messages. The exercise is worth of four separate exercise points.

The program has to start when we execute the main method in the example layout. Also, remember again that you can create only one Scanner object in your exercise.

Week3

Single Responsibility Principle

When we design bigger programs, we often reason about what class has to deal with what task. If we delegate the implementation of the whole program to one class, the result is inevitably chaos. A sector of software design, object-oriented design, includes the Single Responsibility Principle, which we should follow.

The Single Responsibility Principle states that each class should have only one clear role. If the class has one clear role, modifying that role is easy, and only one class will have to be modified. Each class should have only one reason to be modified..

Let us focus on the following class Worker, which has methods to calculate his salary and to report his working hours.

public class Worker {
    // object variables

    // worker's constructor and methods

    public double calculateSalary() {
        // the logic concerning salary count
    }

    public String reportHours() {
        // the logic concerning working hours bookkeeping
    }
}
    

Even if the examples above do not show the concrete implementations, an alarm should go off. Our Worker class has at least three different responsibilities. It represents a worker, it performes the role of a salary calculator, and the role of a working hour bookkeeping system by reporting working hours. The class above should be split into three: one should represent the worker, another should represent the salary calculator, and the third should deal with time bookkeeping.

public class Worker {
    // object variables

    // worker's constructor and methods
}
    
public class SalaryCalculator {
    // object variables

    // methods for salary count

    public double calculateSalary(Person person) {
        // salary calculation logic
    }
}
    
public class TimeBookkeeping {
    // object variables

    // methods concerning time bookkeeping

    public String createHourReport(Person person) {
        // working hours bookeeping logic
    }
}
    

Each variable, each code raw, each method, each class, and each program should have only one responsibility. Often a "better" program stucture is clear to the programmer only once the program is implemented. This is completely acceptable: even more important it is that we always try to change a program to make it clearer. Always refactor -- i.e. always improve your program when it is needed!

Organising Classes into Packages

When we design and implement bigger programs, the number of classes rapidly grows. When the number of classes grows, remembering their functionality and methods becomes more difficult. Giving sensible names to classes helps to remember their funcitonality. In addition to giving sensible names, it is good to split the source code files into packages according to their functionality, use, and other logical reasons. In fact, the packages are but folders we use to organise our source code files. Directories are often called folders, both in windows and colloqually. We will use the term directory, anyway.

Programming environments provide made-up tools for package management. So far, we have been creating classes and interfaces only in the default package of the Source Packages partition. In NetBeans, we can create a new package by clicking on Source Packages, and choosing New -> Java Package.... In the created package, we can create classes in the same way as we do in the default package.

You can read the name of the package that contains a certain class at the beginning of the source code files in the sentence package packageName before the other statements. For instance, the below class Implementation is contained in the package library.

package library;

public class Implementation {

    public static void main(String[] args) {
        System.out.println("Hello packageworld!");
    }
}
    

Packages can contain other packages. For instance, the package definition package library.domain means that the package domain is contained in the package library. By placing packages into other packages, we design the hierachy of classes and interfaces. For instance, all Java's classes are located in packages that are contained in the package java. The package name domain is often used to represent the storage location of the classes which deal with concepts specific for the domain. For instance, the class Book could be stored in the package library.domain because it represents a concept specific of the library.

package library.domain;

public class Book {
    private String name;

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

    public String getName() {
        return this.name;
    }
}
    

We can uses the classes stored in our packages through the import statement. For instance, the class Implementation, which is contained in the package library could make use of a class stored in library.domain through the assignment import library.domain.Book.

package library;

import library.domain.Book;

public class Implementation {

    public static void main(String[] args) {
        Book book = new Book("The ABC of Packages!");
        System.out.println("Hello packageworld: " + book.getName());
    }
}
    
Hello packageworld: The ABC of Packages!
    

The import statements are defined in our source code file after the package statement but before the class statement. There can be many of them -- for instance, when we want to use different classes. Java's made-up classes are usually stored in java package child packages. Hopefully, the statements which appear at the beginning of our classes -- such as import java.util.ArrayList and import java.util.Scanner; -- are starting to look more meaningful now.

From now on, in all our exercises we will use packages. Next, we will create our first packages ourselves.

First Packages

UI Interface

Create the package mooc in your project. We create the functionality of our application inside this package. Add the package ui to your application; at this point, you should have the package mooc.ui. Create a new interface in it, and call it UserInterface.

The interface UserInterface has to determine the method void update().

Text User Interface

Create the class TextUserInterface in the same package; make it implement the interface UserInterface. Implement the method public void update() which is required by the interface UserInterface which TextUserInterface implements: its only duty should be printing the string "Updating the user interface" with a System.out.println method call.

Application Logic

Create now the package mooc.logic, and add the class ApplicationLogic in it. The application logic API should be the following:

  • the constructor public ApplicationLogic(UserInterface ui)
  • . It receives as parameter a class which implements the interface UserInterface. Note: your application logic has to see the interface and therefore to import it; in other words, the line import mooc.ui.UserInterface must appear at the beginning of he file
  • the method public void execute(int howManyTimes)
  • prints the string "The application logic works" as many times as it is defined by its parameter variable. After each "The application logic works" printout, the code has to call the update() method of the object which implements the interface UserInterface and which was assigned to the constructor as its parameter.

You can test your application with the following main class.

import mooc.logic.ApplicationLogic;
import mooc.ui.UserInterface;
import mooc.ui.TextUserInterface;

public class Main {

    public static void main(String[] args) {
        UserInterface ui = new TextUserInterface();
        new ApplicationLogic(ui).execute(3);
    }
}
        

The program output should be the following:

The application logic works
Updating the user interface
The application logic works
Updating the user interface
The application logic works
Updating the user interface
        

A Concrete Directory Construction

All the projects which can be seen are stored in your computer file system. Each project has its own directory (folder) which contains the project directories and files.

The project directory src contains the program source code. If a class package is a library, it is located in the directory library of the project source code directory src. If you are interested in it, it is possible to have a look at the concrete project structure in NetBeans, by going to the Files tab which is next to the Projects tab. If you can't see the Files tab, you can display it by choosing Files from the Window menu.

Application development is usually done through the Projects tab, where NetBeans has hidden the project files which the programmer doesn't have to care about.

Visibility Definitions and Packages

We have already managed to know two visibility definitions. The method and variables with the visibility definition private are visible only inside the class that defines them. They cannot be used outside the class. Differently, the method and variables with visibility definition public are visible for any class.

package library.ui;

public class UserInterface {
    private Scanner reader;

    public UserInterface(Scanner reader) {
        this.reader = reader;
    }

    public void start() {
        printTitle();

        // more functionality
    }

    private void printTitle() {
        System.out.println("***********");
        System.out.println("* LIBRARY *");
        System.out.println("***********");
    }
}
    

The object constructor and start method of the above class UserInterface can be called from whatever program. The method printTitle and the variable reader can be used only inside their class.

When we want to assign package visibility to a variable or a method, we do not need to use any prefix. We can modify the example above assigning package visibility to the method printTitle.

package library.ui;

public class UserInterface {
    private Scanner reader;

    public UserInterface(Scanner reader) {
        this.reader = reader;
    }

    public void start() {
        printTitle();

        // more functionality
    }

    void printTitle() {
        System.out.println("***********");
        System.out.println("* Library *");
        System.out.println("***********");
    }
}
    

Now, the classes inside the same package can use the method printTitle.

package library.ui;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);
        UserInterface userInterface = new UserInterface(reader);

        userInterface.printTitle(); // it works!
    }
}
    

If the class is in a different package, the method printTitle can't be used.

package library;

import java.util.Scanner;
import library.ui.UserInterface;

public class Main {

    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);
        UserInterface userInterface = new UserInterface(reader);

        userInterface.printTitle(); // it doesn't work  !
    }
}
    

Many Interfaces, and Interface Flexibility

Last week we were introduced to interfaces. An interface defines one or more methods which have to be implemented in the class which implements the interface. The interfaces can be stored into packages like any other class. For instance, the interface Identifiable below is located in the package application.domain, and it defines that the classes which implement it have to implement the method public String getID().

package application.domain;

public interface Identifiable {
    String getID();
}
    

The class makes use of the interface through the keyword implements. The class Person, which implements the Idenfifiable interface. The getIDof Person class always returns the person ID.

package application.domain;

public class Person implements Identifiable {
    private String name;
    private String id;

    public Person(String name, String id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public String getPersonID() {
        return this.id;
    }

    @Override
    public String getID() {
        return getPersonID();
    }

    @Override
    public toString(){
        return this.name + " ID: " +this.id;
    }
}
    

An interface strength is that interfaces are also types. All the objects which are created from classes that implement an interface also have that interface's type. This effictively helps us to build our applications.

We create the class Register, which we can use to search for people against their names. In addition to retrieve single people, Register provides a method to retrieve a list with all the people.

public class Register {
    private HashMap<String, Identifiable> registered;

    public Register() {
        this.registered = new HashMap<String, Identifiable>();
    }

    public void add(Identifiable toBeAdded) {
        this.registered.put(toBeAdded.getID(), toBeAdded);
    }

    public Identifiable get(String id) {
        return this.registered.get(id);
    }

    public List<Identifiable> getAll() {
        return new ArrayList<Identifiable>(registered.values());
    }
}
    

Using the register is easy.

Register personnel = new Register();
personnel.add( new Person("Pekka", "221078-123X") );
personnel.add( new Person("Jukka", "110956-326B") );

System.out.println( personnel.get("280283-111A") );

Person found = (Person) register.get("110956-326B");
System.out.println( found.getName() );
    

Because the people are recorded in the register as Identifiable, we have to change back their type if we want to deal with people through those methods which are not defined in the interface. This is what happens in the last two lines.

What about if we wanted an operation which returns the people recorded in our register sorted according to their ID?

One class can implement various different interfaces, and our Person class can implement Comparable in addition to Identifiable. When we implement various different interfaces, we separate them with a comma (public class ... implements FirstInterface, SecondInterface ...). When we implement many interfaces, we have to implement all the methods required by all the interfaces. Below, we implement the interface Comparable in the class Person.

package application.domain;

public class Person implements Identifiable, Comparable<Person> {
    private String name;
    private String id;

    public Person(String name, String id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public String getPersonID() {
        return this.id;
    }

    @Override
    public String getID() {
        return getPersonID();
    }

    @Override
    public int compareTo(Person another) {
        return this.getID().compareTo(another.getID());
    }
}
    

Now, we can add to the register method sortAndGetEverything:

    public List<Identifiable> sortAndGetEverything() {
        ArrayList<Identifiable> all = new ArrayList<Identifiable>(registered.values());
        Collections.sort(all);
        return all;
    }
    

However, we notice that our solution does not work. Because the people are recorded into the register as if their type was Identifiable, Person has to implement the interface Comparable<Identifiable> so that our register could sort people with its method Collections.sort(). This means we have to modify Person's interface:

public class Person implements Identifiable, Comparable<Identifiable> {
    // ...

    @Override
    public int compareTo(Identifiable another) {
        return this.getID().compareTo(another.getID());
    }
}
    

Now our solution works!

Our Register is unaware of the real type of the objects we record. We can use the class Register to record objects of different types than Person, as long as the object class implements the interface Identifiable. For instance, below we use the register to manage shop sales:

public class Sale implements Identifiable {
    private String name;
    private String barcode;
    private int stockBalance;
    private int price;

    public Sale(String name, String barcode) {
        this.name = name;
        this.barcode = barcode;
    }

    public String getID() {
        return barcode;
    }

    // ...
}

Register products = new Register();
products.add( new Product("milk", "11111111") );
products.add( new Product("yogurt", "11111112") );
products.add( new Product("cheese", "11111113") );

System.out.println( products.get("99999999") );

Product product = (Product)products.get("11111112");
product.increaseStock(100);
product.changePrice(23);
    

The class Register is quite universal now that it is not dependent on concrete classes. Whatever class which implements Identifiable is compatible with Register. However, the method sortAndGetEverything can only work if we implement the interface Comparable<Identifiable>.

NetBeans Tips
  • All NetBeans tips can be found here
  • Implement all abstract methods

    Let us suppose that your program contains the interface Interface, and you are building the class Class which implements the interface. It will be annoying to write the declaration raws of all the interface methods.

    However it is possible to ask NetBeans to fill in the method bodies automatically. When you have defined the interface a class should implement, i.e. when you have written

    public class Class implements Interface {
    }
            

    NetBeans paints the class name red. If you go to lamp icon on the left corner of the raw, click, and choose Implement all abstract methods, the method bodies will appear in your code!

  • Clean and Build

    Sometimes, NetBeans may get confused and try to run a code version without noticing all the corrected changes made to it. Usually you notice it because something "strange" happens. Usually, you can fix the problem by using Clean and build operation. The operation is found in the Run menu, and you can execute it also by clicking on the brush and hammer symbol. Clean and build deletes the translated versions of the code and generates a new translation.

Moving

Before moving, you pack your things and put them into boxes trying to keep the number of boxes needed as small as possible. In this exercise we simulate packing things into boxes. Each thing has a volume, and boxes have got a maximum capacity.

Things and Items

The removers will later on move your things to a track (which is not implemented here); therefore, we first implement the interface Thing, which represents all things and boxes.

The Thing interface has to determine the method int getVolume(), which is needed to understand the size of a thing. Implement the interface Thing in the package moving.domain.

Next, implement the class Item in the package moving.domain. The class receives the item name (String) and volume (int) as parameter. The class has to implement the interface Thing.

Add the method public String getName() to Item, and replace the method public String toString() so that it returns strings which follow the pattern "name (volume dm^3)". Item should now work like the following

    Thing item = new Item("toothbrash", 2);
    System.out.println(item);
        
toothbrash (2 dm^3)
        

Comparable Item

When we pack our items into boxes, we want to start in order from the first items. Implement the interface Comparable in the class Item; the item natural order must be ascending against volume. When you have implemented the interface Comparable, the sort method of class Collection has to work in the following way:.

    List<Item> items = new ArrayList<Item>();
    items.add(new Item("passport", 2));
    items.add(new Item("toothbrash", 1));
    items.add(new Item("circular saw", 100));

    Collections.sort(items);
    System.out.println(items);
        
[toothbrash (1 dm^3), passport (2 dm^3), circular saw (100 dm^3)]
        

Moving Box

Implement now the class Box in the package moving.domain. At first, implement the following method for your Box:

  • the constructor public Box(int maximumCapacity)
  • receives the box maximum capacity as parameter;
  • the method public boolean addThing(Thing thing)
  • adds an item which implements the interface Thing to the box. If it does not fit in the box, the method returns false, otherwise true. The box must store the things into a list.

Also, make your Box implement the Thing interface. The method getVolume has to return the current volume of the things inside the box.

Packing Items

Implement the class Packer in the package moving.logic. The constructor of the class Packer is given the parameter int boxesVolume, which determines how big boxes the packer should use.

Afterwards, implement the method public List<Box> packThings(List<Thing> things), which packs things into boxes.

The method should move all the things in the parameter list into boxes, and these boxes should be contained by the list the method returns. You don't need to pay attention to such situations where the things are bigger than the boxes used by the packer. The tests do not check the way the packer makes use of the moving boxes.

The example below shows how our packer should work:

    // the things we want to pack
    List<Thing> things = new ArrayList<Thing>();
    things.add(new Item("passport", 2));
    things.add(new Item("toothbrash", 1));
    things.add(new Item("book", 4));
    things.add(new Item("circular saw", 8));

    // we create a packer which uses boxes whose valume is 10
    Packer packer = new Packer(10);

    // we ask our packer to pack things into boxes
    List<Box> boxes = packer.packThings( things );

    System.out.println("number of boxes: "+boxes.size());

    for (Box box : boxes) {
        System.out.println("  things in the box: "+box.getVolume()+" dm^3");
    }
        

Prints:

number of boxes: 2
  things in the box: 7 dm^3
  things in the box: 8 dm^3
        

The packer has packed the things into two boxes, the first box has the firts three things, whose total volume was 7, and the last thing in the list -- the circular saw, whose volume was 8 -- has gone to the third box. The tests do not set a limit to the number of boxes used by the packer; each thing could have been packed into a different box, and the output would have been:

number of boxes: 4
  things in the box: 2 dm^3
  things in the box: 1 dm^3
  things in the box: 4 dm^3
  things in the box: 8 dm^3
        

Note: to help testing, it would be convinient to create a toString method for the class Box, for instance; this would help printing the content of the box.

Exceptions

Exceptions are such situations where the program executions is different from our expectations. For instance, the program may have called a method of a null reference, in which case the user is thrown a NullPointerException. If we try to retrieve a index outside a table, the user is thrown a IndexOutOfBoundsException. All of them are a type of Exception.

We deal with exception using the block try { } catch (Exception e) { }. The code contained within the brackets which follows the keyword try can possibly go through an exception. The keyword the code within the brackets which follow the keyword catch defines what should happen when the try-code throws an exception. We also define the type of the exception we want to catch (catch (Exception e)).

    try {
        // code which can throw an exception
    } catch (Exception e) {
        // code which is executed in case of exception
    }
    

The parseInt method of class Integer which turns a string into a number can throw a NumberFormatException if its string parameter cannot be turned into a number. Now we implement a program which tries to turn into a number a user input string.

    Scanner reader = new Scanner(System.in);
    System.out.print("Write a number: ");

    int num = Integer.parseInt(reader.nextLine());
    
Write a number: tatti
Exception in thread "..." java.lang.NumberFormatException: For input string: "tatti"
    

The program above throws an exception because the user digits an erroneous number. The program execution ends up with a malfunction, and it cannot continue. We add an exception management statement to our program. The call, which may throw an exception is written into the try block, and the action which takes place in case of exception is written into the catch block.

    Scanner reader = new Scanner(System.in);

    System.out.print("Write a number: ");

    try {
        int num = Integer.parseInt(reader.nextLine());
    } catch (Exception e) {
        System.out.println("You haven't written a proper number.");
    }
    
Write number: 5
    
Write number: oh no!
You haven't written a proper number.
    

In case of exception, we move from the chunk of code defined by the try keyword to the catch chunk. Let's see this by adding a print statement after the Integer.parseInt line in the try chunk.

    Scanner reader = new Scanner(System.in);

    System.out.print("Write a number: ");

    try {
        int num = Integer.parseInt(reader.nextLine());
        System.out.println("Looks good!");
    } catch (Exception e) {
        System.out.println("You haven't written a proper number.");
    }
    
Write a number: 5
Looks good!
    
Write a number: I won't!
you haven't written a proper number.
    

String I won't! is given as parameter to the method Integer.parseInt, which throws an exception if the String parameter can't be changed into a number. Note that the code in the catch chunk is executed only in case of exception -- otherwise the program do not arrive till there.

Let's make something more useful out of our number translator: let's do a method which keeps on asking to type a number till the user does it. The user can return only if they have typed the right number.

public int readNumber(Scanner reader) {
    while (true) {
        System.out.print("Write a number: ");

        try {
            int num = Integer.parseInt(reader.nextLine());
            return num;
        } catch (Exception e) {
            System.out.println("You haven't written a proper number.");
        }
    }
}
    

The method readNumber could work in the following way:

Write a number: I won't!
You haven't written a proper number.
Write a number: Matti has a mushroom on his door.
You haven't written a proper number.
Write a number: 43

    

Throwing Exceptions

Methods and constructors can throw exceptions. So far, there are two kinds of exceptions which can be thrown. There are the ones which have to be handled, and the ones which don't have to be dealt with. When we have to handle the exceptions, we do it either in a try-catch chunk, or throwing them from a method.

In the clock exercise of Introduction to Programming, we explained that we can stop our program of one second, by calling the method Thread.sleep(1000). The method may throw an exception, which we must deal with. In fact, we handle the exception using the try-catch sentence; in the following example we skip the exception, and we leave empty the catch chunk.

    try {
        // we sleep for 1000 milliseconds
        Thread.sleep(1000);
    } catch (Exception e) {
        // In case of exception, we do not do anything.
    }
  

It is also possible to avoid handling the exceptions in a method, and delegate the responsibility to the method caller. We delegate the responsibility of a method by using the statement throws Exception.

    public void sleep(int sec) throws Exception {
        Thread.sleep(sec * 1000);   // now we don't need the try-catch block
    }
  

The sleep method is called in another method. Now, this other method can either handle the exception in a try-catch block or delegate the responsibility forward. Sometimes, we delegate the responsibility of handling an exception, till the very end, and even the main method delegates it:

public class Main {
   public static void main(String[] args) throws Exception {
       // ...
   }
}
  

In such cases, the exception ends up in Java's virtual machine, which interrupts the program in case there is an error which causes the problem.

There are some exceptions which the programmer does not always have to address, such as the NumberFormatException which is thrown by Integer.parseInt. Also the RuntimeExceptions do not always require to be addressed; next week we will go back to why variables can have more than one type.

We can throw an exception ourself from the source code using the throw statement. For instance, if we want to throw an exception which was created in the class NumberFormatException, we could use the statement throw new NumberFormatException().

Another exception which hasn't got to be addressed is IllegalArgumentException. With IllegalArgumentException we know that a method or a constructor has received an illegal value as parameter. For instance, we use the IllegalArgumentException when we want to make sure that a parameter has received particular values. We create the class Grade whose constructor has a integer parameter: the grade.

public class Grade {
    private int grade;

    public Grade(int grade) {
        this.grade = grade;
    }

    public int getGrade() {
        return this.grade;
    }
}
  

Next, we want to validate the value of the constructor parameter of our Grade class. The grades in Finland are from 0 to 5. If the grade is something else, we want to throw an exception. We can add an if statement to our Grade class constructor, which checks whether the grade is outside range 0-5. If so, we throw an IllegalArgumentException telling throw new IllegalArgumentException("The grade has to be between 0-5");.

public class Grade {
    private int grade;

    public Grade(int grade) {
        if (grade < 0 || grade > 5) {
            throw new IllegalArgumentException("The grade has to be between 0-5");
        }
        this.grade = grade;
    }

    public int getGrade() {
        return this.grade;
    }
}
  
    Grade grade = new Grade(3);
    System.out.println(grade.getGrade());

    Grade wrongGrade = new Grade(22);
    // it causes an exception, we don't continue
  
3
Exception in thread "..." java.lang.IllegalArgumentException: The grade has to be between 0-5
  

Method Argument Validation

Let's train method argument validation with the help of the IllegalArgumentException. The excercise layout shows two classes Person and Calculator. Change the class in the following way:

Person Validation

The constructor of Person has to make sure its parameter's Name variable is not null, empty, or longer than 40 characters. The age has also to be between 0-120. If one of the conditions above are not satisfied, the constructor has to throw an IllegalArgumentException.

Calculator Validation

The Calculator methods have to be changed in the following way: the method multiplication has to work only if its parameter is not negative (greater than 0). The method binomialCoefficient has to work only if the parameters are not negative and the size of a subset is smaller than the set's size. If one of the methods receives invalid arguments when they are called, they have to throw a IllegalArgumentException.

Sensors and Temperature Measurement

All the code in our application has to be placed into the package application.

We have got the following interface available for our use:

public interface Sensor {
    boolean isOn();  // returns true if the sensor is on
    void on();       // switches the sensor on
    void off();      // switches the sensor off
    int measure();   // returns the sensor reading if the sensor is on
                     // if the sensor is off, it throws an IllegalStateException
}
      

Constant Sensor

Create the class Constant Sensor which implements the interface Sensor.

The constant sensor is online all the time. The methods on() and off() do not do anything. The constant sensor has a constructor with an int parameter. The measure method call returns the number received as constructor parameter.

For instance:

public static void main(String[] args) {
  ConstantSensor ten = new ConstantSensor(10);
  ConstantSensor minusFive = new ConstantSensor(-5);

  System.out.println( ten.measure() );
  System.out.println( minusFive.measure() );

  System.out.println( ten.isOn() );
  ten.off();
  System.out.println( ten.isOn() );
}
      

Prints:

10
-5
true
true
      

Thermometer

Create the class Thermometer which implements the interface Sensor.

At first, the thermometer is off. When the measure method is called, if the thermometer is on it returns a random number between -30 and 30. If the thermometer is off, it throws an IllegalStateException.

AverageSensor

Create the class AverageSensor which implements the interface Sensor.

An average sensor contains many sensors. In addition to the methods defined by the interface Sensor, the class has the method public void addSensor(Sensor additional) which adds a new sensor to the AverageSensor.

The average sensor is on when all its sensors are on. When the average sensor is switched on, all its sensors have to be switched on if they were not on already. When the average sensor is closed, at least one of its sensors has to be switched off. It's also possible that all its sensors are switched off.

The measure method of our AverageSensor returns the average of the readings of all its sensors (because the return value is int, the readings are rounded down as it is for integer division). If the measure method is called when the average sensor is off, or if the average sensor was not added any sensor, the method throws an IllegalStateException.

Below, you find an example of a sensor program (note that both the Thermometer and the AverageSensor constructors are without parameter):

public static void main(String[] args) {
    Sensor kumpula = new Thermometer();
    kumpula.on();
    System.out.println("the temperature in Kumpula is "+kumpula.measure() + " degrees");

    Sensor kaisaniemi = new Thermometer();
    Sensor helsinkiVantaa = new Thermometer();

    AverageSensor helsinkiArea = new AverageSensor();
    helsinkiArea.addSensor(kumpula);
    helsinkiArea.addSensor(kaisaniemi);
    helsinkiArea.addSensor(helsinkiVantaa);

    helsinkiArea.on();
    System.out.println("the temperature in Helsinki area is "+helsinkiArea.measure() + " degrees");
}

Prints (the printed readings depend on the random temperature readings):

the temperature in Kumpula is -7 degrees
the temperature in Helsinki area is -10 degrees

Note: you'd better use a ConstantSensor object to test your average sensor!

All Readings

Add the method public List<Integer> readings() to your AverageSensor; it returns a list of the reading results of all the measurements executed through your AverageSensor. Below is an example of how the method works:

public static void main(String[] args) {
    Sensor kumpula = new Thermometer();
    Sensor kaisaniemi = new Thermometer();
    Sensor helsinkiVantaa = new Thermometer();

    AverageSensor helsinkiArea = new AverageSensor();
    helsinkiArea.addSensor(kumpula);
    helsinkiArea.addSensor(kaisaniemi);
    helsinkiArea.addSensor(helsinkiVantaa);

    helsinkiArea.on();
    System.out.println("the temperature in Helsinki area is "+helsinkiArea.measure() + " degrees");
    System.out.println("the temperature in Helsinki area is "+helsinkiArea.measure() + " degrees");
    System.out.println("the temperature in Helsinki area is "+helsinkiArea.measure() + " degrees");

    System.out.println("readings: "+helsinkiArea.readings());
}

Prints (again, the printed readings depend on the random temperature readings):

the temperature in Helsinki area is -10 degrees
the temperature in Helsinki area is -4 degrees
the temperature in Helsinki area is -5 degrees

readings: [-10, -4, 5]

Exceptions and Interfaces

Interfaces do not have a method body, but the method definition can be freely chosen when the developer implements the interface. Interfaces can also define the exceptions throw. For instance, the classes which implement the following FileServer can possibly throw an exception in their methods download and save.

public interface FileServer {
    String download(String file) throws Exception;
    void save(String file, String string) throws Exception;
}

If an interface defines the throws Exception attributes for the methods -- i.e. the methods may throw an exception -- the classes which implement the interface must be defined in the same way. However, they do not have to throw an exception, as it becomes clear in the following example.

public class TextServer implements FileServer {

    private Map<String, String> data;

    public TextServer() {
        this.data = new HashMap<String, String>();
    }

    @Override
    public String download(String file) throws Exception {
        return this.data.get(file);
    }

    @Override
    public void save(String file, String string) throws Exception {
        this.data.put(file, stirng);
    }
}

The Exception Information

The catch block tells how we handle an exception, and it tells us what exception we should be prepared for: catch (Exception e). The exception information is saved into the e variable.

    try {
        // the code, which may throw an exception
    } catch (Exception e) {
        // the exception information is saved into the variable e
    }

The class Exception can provide useful methods. For instance, the method printStackTrace() prints a path which tells us where the exception came from. Let's check the following error printed by the method printStackTrace().

Exception in thread "main" java.lang.NullPointerException
  at package.Class.print(Class.java:43)
  at package.Class.main(Class.java:29)

Reading the stack trace happens button up. The lowest is the first call, i.e. the program execution has started from the main() method of class Class. At line 29 of the main method of Class, we called the method print(). Line 43 of the method print caused a NullPointerException. Exception information are extremely important to find out the origin of a problem.

Reading a File

A relevant part of programming is related to stored files, in one way or in another. Let's take the first steps in Java file handling. Java's API provides the class File, whose contents can be read using the already known Scanner class.

If we read the desciption of the File API we notice the File class has the constructor File(String pathname), which creates a new File instance by converting the given pathname string into an abstract pathname. This means the File class constructor can be given the pathname of the file we want to open.

In the NetBeans programming environment, files have got their own tab called Files, which contains all our project files. If we add a file to a project root -- that is to say outside all folders -- we can refer to it by writing only the its name. We create a file object by fiving the file pathname to it as parameter:

    File file = new File("file-name.txt");

System.in input stream is not the only reading source we can give to the constructor of a Scanner class. For instance, the reading source can be a file, in addition to the user keyboard. Scanner provides the same methods to read a keyboard input and a file. In the following example, we open a file and we print all the text contained in the file using the System.out.println statement. At the end, we close the file using the statement close.

        // The file we read
        File file = new File("filename.txt");

        Scanner reader = new Scanner(file);
        while (reader.hasNextLine()) {
            String line = reader.nextLine();
            System.out.println(line);
        }

        reader.close();

The Scanner class constructor public Scanner(File source) (Constructs a new Scanner that produces values scanned from the specified file.) throws a FileNotFoundException when the specified file is not found. The FileNotFoundException is different than RuntimeException, and we have either to handle it or throw it forward. At this point, you only have to know that the programming environment tells you whether you have to handle the exception or not. Let's first create a try-catch block where we handle our file as soon as we open it.

    public void readFile(File f) {
        // the file we read
        Scanner reader = null;

        try {
            reader = new Scanner(f);
        } catch (Exception e) {
            System.out.println("We couldn't read the file. Error: " + e.getMessage());
            return; // we exit the method
        }

        while (reader.hasNextLine()) {
            String line = reader.nextLine();
            System.out.println(line);
        }

        reader.close();
    }

Another option is to delegate the exception handling responsibility to the method caller. We delegate the exception handling responsibility by adding the definition throws ExceptionType to the method. For instance, we can add throws Exception because the type of all exceptions is Exception. When a method has the attribute throws Exception, whatever chunk of code which calls that method knows that it may throw an exception, and it should be prepared for it.

    public void readFile(File f) throws Exception {
        // the file we read
        Scanner reader = new Scanner(f);

        while (reader.hasNextLine()) {
            String line = reader.nextLine();
            System.out.println(line);
        }

        reader.close();
    }

In the example, the method readFile receives a file as parameter, and prints all the file lines. At the end, the reader is closed, and the file is closed with it, too. The attribute throws Exception tells us that the method may throw an exception. Same kind of attributes can be added to all the methods that handle files.

Note that the Scanner object's method nextLine returns a string, but it does not return a new line at the end of it. If you want to read a file and still maintain the new lines, you can add a new line at the end of each line:

    public String readFileString(File f) throws Exception {
        // the file we read
        Scanner reader = new Scanner(f);

        String string = "";

        while (reader.hasNextLine()) {
            String line = reader.nextLine();
            string += line;
            string += "\n";
        }

        reader.close();
        return string;
    }

Because we use the Scanner class to read files, we have all Scanner methods available for use. For instance the method hasNext() returns the boolean value true if the file contains something more to read, and the method next() reads the following word and returns a String object.

The following program creates a Scanner object which opens the file file.txt. Then, it prints every fifth word of the file.

        File f = new File("file.txt");
        Scanner reader = new Scanner(f);

        int whichNumber = 0;
        while (reader.hasNext()) {
            whichNumber++;
            String word = reader.next();

            if (whichNumber % 5 == 0) {
                System.out.println(word);
            }
        }

Below, you find the text contained in the file, followed by the program output.

Exception handling is the process of responding to the occurrence, during computation, of exceptions – anomalous or exceptional events requiring special processing – often changing the normal flow of program execution. ...
process
occurrence,
–
requiring
changing
program

Character Set Issues

When we read a text file (or when we save something into a file), Java has to find out the character set used by the operating system. Knowledge of the character set is required both to save text on the computer harddisk in binary format, and to translate binary data into text.

There have been developed standard character sets, and "UTF-8" is the most common nowadays. UTF-8 character set contains both the alphabet letters of everyday use and more particular characters such as the Japanese kanji characters or the information need to read and save the chess pawns. From a simplified programming angle, we could think a character set both as a character-number hashmap and a number-character hashmap. The character-number hashmap shows what binary number is used to save each character into a file. The number-character hashmap shows how we can translate into characters the values we obtain reading a file.

Almost each operating system producer has also got their own standards. Some support and want to contribute to the use of open source standards, some do not. If you have got problems with the use of Scandinavian characters such as ä and ö (expecially Mac and Windows users), you can tell which character set you want to use when you create a Scanner object. In this course, we always use the the "UTF-8" character set.

You can create a Scanner object which to read a file which uses the UTF-8 character set in the following way:

    File f = new File("examplefile.txt");
    Scanner reader = new Scanner(f, "UTF-8");

Anther thing you can do to set up a character set is using an environment variable. Macintosh and Windows users can set up an the value of the environment variable JAVA_TOOL_OPTIONS to the string -Dfile.encoding=UTF8. In such case, Java always uses UTF-8 characters as a default.

Printer

Create the class Printer, its constructor public Printer(String fileName) which receives a String standing for the file name, and the method public void printLinesWhichContain(String word) which prints the file lines which contain the parameter word (lower and upper case make difference in this excercise; for instance, "test" and "Test" are not the considered the same); the lines are printed in the same order as they are inside the file.

If the argument is an empty String, all of the file is printed.

If the file is not found, the constructor delegates the exception with no need for a try-catch statement; the constructor simply has to be defined in the following way:

public Printer {

   public Printer(String fileName) throws Exception {
      // ...
   }

   // ...
}

The file textFile has been place into the default package of your project to help the tests. When you define the file name of the constructor of Printer, you have to write src/textfile.txt. The file contains an extract of Kalevala, a Finnish epic poem:

Siinä vanha Väinämöinen
katseleikse käänteleikse
Niin tuli kevätkäkönen
näki koivun kasvavaksi
Miksipä on tuo jätetty
koivahainen kaatamatta
Sanoi vanha Väinämöinen

The following example shows what the program should do:

    Printer printer = new Printer("src/textfile.txt");

    printer.printLinesWhichContain("Väinämöinen");
    System.out.println("-----");
    printer.printLinesWhichContain("Frank Zappa");
    System.out.println("-----");
    printer.printLinesWhichContain("");
    System.out.println("-----");

Prints:

Siinä vanha Väinämöinen
Sanoi vanha Väinämöinen
-----
-----
Siinä vanha Väinämöinen
katseleikse käänteleikse
Niin tuli kevätkäkönen
näki koivun kasvavaksi
Miksipä on tuo jätetty
koivahainen kaatamatta
Sanoi vanha Väinämöinen

In the project, you also find the whole Kalevala; the file name is src/kalevala.txt

File Analysis

In this exercise, we create an application to calculate the number of lines and characters.

Number of Lines

Create the class Analysis in the package file; the class has the constructor public Analysis(File file). Create the method public int lines(), which returns the number of lines of the file the constructor received as parameter.

The method cannot be "disposable", that is to say it has to return the right value even though it is called different times in a raw. Note that after you create a Scanner object for a file and read its whole contents using nextLine method calls, you can't use the same scanner to read the file again!

Attention: if the tests report a timeout, it probably means that you haven't been reading the file at all, meaning that the nextLine method calls miss!

Number of Characters

Create the method public int characters() in the class Analysis; the method returns the number of characters of the file the constructor received as parameter.

The method cannot be "disposable", that is to say it has to return the right value even though it is called different times in a raw.

You can decide yourself what to do if the constructor parameter file does not exist.

The file testFile has been place into the test package of your project to help the tests. When you define the file name of the constructor of Analysis, you have to write test/testfile.txt. The file contains the following text:

there are 3 lines, and characters
because line breaks are also
characters

The following example shows what the program should do:

    File file = new File("test/testfile.txt");
    Analysis analysis = new Analysis(file);
    System.out.println("Lines: " + analysis.lines());
    System.out.println("Characters: " + analysis.characters());
Lines: 3
Characters: 74

Word Inspection

Create the class WordInspection, which allows for different kinds of analyses on words. Implement the class in the package wordinspection.

The Institute for the Languages of Finland (Kotimaisten kielten tutkimuskeskus, Kotus) has published online a list of Finnish words. In this exercise we use a modified version of that list, which can be found in the exercise source folder src with the name wordList.txt; the relative path is "src/wordList.txt". Because the word list if quite long, in fact, a shortList.txt was created in the project for the tests; the file can be found following the path "src/shortList.txt".

If you have problems with Scandinavian letters (Mac and Windows users) create your Scanner object assigning it the "UTF-8" character set, in the following way: Scanner reader = new Scanner(file, "UTF-8"); Problems come expecially when the tests are executed.

Word Count

Create the constructor public WordInspection(File file) to your WordInspection class. The constructor creates a new WordInspection object which inspects the given file.

Create the method public int wordCount(), which counts the file words and prints their number. In this part, you don't have to do anything with the words, you should only count how many there are. For this exercise, you can expect there is only one word in each row.

z

Create the method public List<String> wordsContainingZ(), which returns all the file words which contain a z; for instance, jazz and zombie.

Ending l

Create the method public List<String> wordsEndingInL(), which returns all the Finnish words of the file which end in l; such words are, for instance, kannel and sammal.

Attention! If you read the file various different times in your program, you notice that your code contains a lot of copy-paste, so far. It would be useful to think whether it would be possible to read the file in an different place, maybe inside the constructor or as a method, which the constructor calls. In such case, the methods could use a list which was read before and then create a new list which suits their search criteria. In week 6, we will come back again with an ortodox way to eliminate copy-paste.

Palindromes

Create the method public List<String> palindromes(), which returns all the palindrome words of the file. Such words are, for instance, ala and enne.

All Vowels

Create the method public List<String> wordsWhichContainAllVowels(), which returns all the words of the file which contain all Finnish vowels (aeiouyäö). Such words are, for instance, myöhäiselokuva and ympäristönsuojelija.

Hashmaps and Sets

Many Values and One Key

As we remember, we can save only one value per key using HashMap. In the following examples we save people's mobile phone numbers in a HashMap.

  Map<String, String> phoneNumbers = new HashMap<String, String>();

  phoneNumbers.put("Pekka", "040-12348765");

  System.out.println( "Pekka's number: "+ phoneNumbers.get("Pekka") );

  phoneNumbers.put("Pekka", "09-111333");

  System.out.println( "Pekka's number: "+ phoneNumbers.get("Pekka") );

as expected, the output tells us:

Pekka's number: 040-12348765
Pekka's number: 09-111333

What about if we wanted to save various different values per one key, what about if a person had many phone numbers? Can we manage with an HashMap? Of course! For instance, instead of saving Strings as HashMap values we could save ArrayLists, mapping more than one object to one key. Let's change the way we save phone numbers as follows:

  Map<String, ArrayList<String>> phoneNumbers = new HashMap<String, ArrayList<String>>();

Now, a list is mapped to each HashMap key. Even though the new command creates a HashMap, the list which will be saved inside has to be created separately. In the following example, we add two numbers to the HashMap for Pekka, and we print them:

  Map<String, ArrayList<String>> phoneNumbers = new HashMap<String, ArrayList<String>>();

  // We map an empty ArrayList to Pekka
  phoneNumbers.put( "Pekka", new  ArrayList<String>() );

  // we add Pekka's number to the list
  phoneNumbers.get("Pekka").add("040-12348765");

  // and we add a second phone number
  phoneNumbers.get("Pekka").add("09-111333");

  System.out.println( "Pekka's numbers: "+ phoneNumbers.get("Pekka") );

Prints

Pekka's numbers: [040-12348765, 09-111333]

We define the phone number type as Map<String, ArrayList<String>>, that is a Map whose key is a String and whose value is a list containing strings. The concrete implementation -- that is to say the created object -- was a HashMap. We could have defined a variable also in the following way:

Map<String, List<String>> phoneNumbers = new HashMap<String, List<String>>();

Now, the variable type is a Map, whose key is a String and value is a List containing strings. In fact, a List is an interface which defines the List functionality, and ArrayLists implement this interface, for instance. The concrete object is a HashMap.

The values we save into the HashMap are concrete object which implement the interface List<String>, ArrayLists, for instance. Again, we can add values to the HashMap in the following way:

  // first, we map an empty ArrayList to Pekka
  phoneNumbers.put( "Pekka", new  ArrayList<String>() );

  // ...

In the future, instead of using concrete classes (such as HashMap and ArrayList, for instance), we will always try to use their respective interfaces Map and List.

Sets

Differently from lists, in a Set there can be up to one same entry, that is to say the same object can not be contained twice in a set. The similarity between two objects is inspected using the methods equals and hashCode.

One of the classes which implement the Set interface is HashSet. Let's use it to implement the class ExerciseAccounting, which allows us to keep an account of the exercise we do and to print them. Let's suppose the the exercises are always integers.

public class ExerciseAccounting {
    private Set<Integer> doneExercises;

    public ExerciseAccounting() {
        this.doneExercises = new HashSet<Integer>();
    }

    public void add(int exercise) {
        this.doneExercises.add(exercise);
    }

    public void print() {
        for (int exercise: this.doneExercises) {
            System.out.println(exercise);
        }
    }
}
        ExerciseAccounting account = new ExerciseAccounting();
        account.add(1);
        account.add(1);
        account.add(2);
        account.add(3);

        account.print();
1
2
3

The solution above is useful if we don't need information about the exercises done by each different user. We can change the saving logic of the exercises in a way to have them save in relation to each user, using a HashMap. The users are recognized through a unique string (for instance, their student number), and each user has their own set of finished exercises.

public class ExerciseAccounting {
    private Map<String, Set<Integer>> doneExercises;

    public ExerciseAccounting() {
        this.doneExercises = new HashMap<String, Set<Integer>>();
    }

    public void add(String user, int exercise) {
        // note that when we create a new user we have first to map an empty exercise set to it
        if (!this.doneExercises.containsKey(user)) {
            this.doneExercises.put(user, new HashSet<Integer>());
        }

        // first, we retrieve the set containing the user's exercises and then we add an exercise to it
        Set<Integer> finished = this.doneExercises.get(user);
        finished.add(exercise);

        // the previous would have worked out without helping variable in the following way:
        //  this.doneExercises.get(user).add(exercise);
    }

    public void print() {
        for (String user: this.doneExercises.keySet()) {
            System.out.println(user + ": " + this.doneExercises.get(user));
        }
    }
}
        ExerciseAccounting accounting = new ExerciseAccounting();
        accounting.add("Mikael", 3);
        accounting.add("Mikael", 4);
        accounting.add("Mikael", 3);
        accounting.add("Mikael", 3);

        accounting.add("Pekka", 4);
        accounting.add("Pekka", 4);

        accounting.add("Matti", 1);
        accounting.add("Matti", 2);

        accounting.print();
Matti: [1, 2]
Pekka: [4]
Mikael: [3, 4]

Note that the user names are not printed in order, in our example. This depends on the saving process of the HashMap entries, which happens through the value returned by the hashCode method, and does not involve the entry order in any way.

Multiple Entry Dictionary

Let's make an extended version of the dictionary of week 1. Your task is to implement the class PersonalMultipleEntryDictionary, which can save one or more entry for each word translated. The class has to implement the interface in the exercise source, MultipleEntryDictionary, with the following methods:

  • public void add(String word, String entry)
  • , which adds a new entry to a word, maintaining the old ones
  • public Set<String> translate(String word)
  • , which returns a Set object, with all the entries of the word, or a null reference, if the word is not in the dictionary
  • public void remove(String word)
  • , which removes a word and all its entries from the dictionary

As for the ExampleAccounting above, it's good to store the translations into a Map<String, Set<String>> object variable.

The interface code:

package dictionary;

import java.util.Set;

public interface MultipleEntryDictionary {
    void add(String word, String translation);
    Set<String> translate(String word);
    void remove(String word);
}

An example program:

    MultipleEntryDictionary dict = new PersonalMultipleEntryDictionary();
    dict.add("kuusi", "six");
    dict.add("kuusi", "spruce");

    dict.add("pii", "silicon");
    dict.add("pii", "pi");

    System.out.println(dict.translate("kuusi"));
    dict.remove("pii");
    System.out.println(dict.translate("pii"));

Prints:

[six, spruce]
null

Duplicate Remover

Your task is to implement inside the package tools a class PersonalDuplicateRemover, which stores the given characterStrings so that equal characterStrings are removed (a.k.a duplicates). Class also holds a record of the amount of duplicates. Class should implement the given interface DuplicateRemover, which has the following methods:

  • public void add(String characterString)
  • stores a characterString if it's not a duplicate.
  • public int getNumberOfDetectedDuplicates()
  • returns the number of detected duplicates.
  • public Set<String> getUniqueCharacterStrings()
  • returns an object which implements the interface Set<String>. Object should have all unique characterStrings (no duplicates!). If there are no unique characterStrings, method returns an empty set.
  • public void empty()
  • removes stored characterStrings and resets the amount of detected duplicates.

Code of the interface:

package tools;

import java.util.Set;

public interface DuplicateRemover {
    void add(String characterString);
    int getNumberOfDetectedDuplicates();
    Set<String> getUniqueCharacterStrings();
    void empty();
}

Interface can be used like this for example:

    public static void main(String[] args) {
        DuplicateRemover remover = new PersonalDuplicateRemover();
        remover.add("first");
        remover.add("second");
        remover.add("first");

        System.out.println("Current number of duplicates: " +
            remover.getNumberOfDetectedDuplicates());

        remover.add("last");
        remover.add("last");
        remover.add("new");

        System.out.println("Current number of duplicates: " +
            remover.getNumberOfDetectedDuplicates());

        System.out.println("Unique characterStrings: " +
            remover.getUniqueCharacterStrings());

        remover.empty();

        System.out.println("Current number of duplicates: " +
            remover.getNumberOfDetectedDuplicates());

        System.out.println("Unique characterStrings: " +
            remover.getUniqueCharacterStrings());
    }

Code above would print: (order of characterStrings can change, it doesn't matter)

Current number of duplicates: 1
Current number of duplicates: 2
Unique characterStrings: [first, second, last, new]
Current number of duplicates: 0
Unique characterStrings: []

One Object in Many Lists, a Map Construction or a Set

As we remember, object variable are reference-type, which means that the variable does not memorize the object itself, but the reference to the object. Respectively, if we put an object into an ArrayList, for instance, the List does not memorize the object itself but the reference to the object. There is no reason why we should not be able to save an object in various different lists or HashMaps, for instance.

Let's have a look at our library example, which saves books into HashMaps, both based on their writer and ISB number. In addition to this, the library. Moreover, the library has two lists for the books on loan and for the ones that are on the shelves.

public class Book {
    private String ISBN;
    private String writer;
    private String name;
    private int date;
    // ...
}

public class Library {
    private Map<String, Book> ISBNBooks;
    private Map<String, List<String>> writerBooks;
    private List<Book> loanBooks;
    private List<Book> shelfBooks;

    public void addBook(Book newBook){
        ISBNBooks.put(newBook.getIsbn(), newBook);
        writerBooks.get(newBook.getWriter()).add(newBook);
        shelfBooks.add(newBook);
    }

    public Book getBookBasedOnISBN(String isbn){
        return ISBNBooks.get(isbn);
    }

    // ...
}

If an object is listed in different places at the same time (in a list, a set, or a map construction), you have to pay particular attention so to make sure the state of the different collections is consistent. For instance, if we decide to delete a book, it must be deleted from both maps as well as from the two lists which contain the books on loan and on the shelves.

Phone Search

Attention: you can create only one Scanner object so that your tests would work well. Also, do not use static variables, the tests execute your program many different times, and the static variable values left from the previous execution would possibly disturb them!

Let's create an application to manage people phone numbers and addresses.

The exercise can be worth 1-5 points. To receive one point, you should implement the following functionality:

  • 1 adding a phone number to the relative person
  • 2 phone number search by person

to receive two points we also require

  • 3 name search by phone number

to receive three points also

  • 4 adding an address to the relative person
  • 5 personal information search (search for a person's address and phone number)

if you want to receive four points, also implement

  • 6 removing a person's information

and to receive all the points:

  • 7 filtered search by keyword (retrieving a list which must be sorted by name in alphabetic order), the keyword can appear in the name or address

An example of how the program works:

phone search
available operations:
 1 add a number
 2 search for a number
 3 search for a person by phone number
 4 add an address
 5 search for personal information
 6 delete personal information
 7 filtered listing
 x quit

command: 1
whose number: pekka
number: 040-123456

command: 2
whose number: jukka
  not found

command: 2
whose number: pekka
 040-123456

command: 1
whose number: pekka
number: 09-222333

command: 2
whose number: pekka
 040-123456
 09-222333

command: 3
number: 02-444123
 not found

command: 3
number: 09-222333
 pekka

command: 5
whose information: pekka
  address unknown
  phone numbers:
   040-123456
   09-222333

command: 4
whose address: pekka
street: ida ekmanintie
city: helsinki

command: 5
whose information: pekka
  address: ida ekmanintie helsinki
  phone numbers:
   040-123456
   09-222333

command: 4
whose address: jukka
street: korsontie
city: vantaa

command: 5
whose information: jukka
  address: korsontie vantaa
  phone number not found

command: 7
keyword (if empty, all listed): kk

 jukka
  address: korsontie vantaa
  phone number not found

 pekka
  address: ida ekmanintie helsinki
  phone numbers:
   040-123456
   09-222333

command: 7
keyword (if empty, all listed): vantaa

 jukka
  address: korsontie vantaa
  phone number not found

command: 7
keyword (if empty, all listed): seppo
 keyword not found

command: 6
whose information: jukka

command: 5
whose information: jukka
  not found

command: x

Some remarks:

  • Because of the tests, it is essential that the user interface works exactly as in the example above. The application can optionally decide in which way invalid inputs are handled. The tests contain only valid inputs.
  • The program has to start when the main method is executed; you can only create one Scanner object.
  • Do not use static variables, the tests execute your program many different times, and the static variable values left from the previous execution would possibly disturb them!
  • In order to make things easier, we assume the name is a single string; if we want to print our lists sorted by surname in alphabetic order, the name has to be given in the form surname name.
  • A person can have more than one phone number and address. However, these are not necessarily stored.
  • If a person is deleted, no search should retrieve them.
Week4

The Finnish Ringing Centre

In the sixth week of Introduction to Programming, we created an observation database for bird watching. Now we continue in the same vein, and this time, we create a program for the ringing centre to track of the places where the rung birds were observed.

ATTENTION: You may run into a strange error message in this exercise, such as NoSuchMethodError: Bird.equals(BBird;)Z; if this happens, clean and build, i.e. press the brush and hammer icon in NetBeans.

Bird equals and toString

The Finnish Ringing Centre stores the information about the birds who were watched in a specific year in Bird objects:

public class Bird {

    private String name;
    private String latinName;
    private int ringingYear;

    public Bird(String name, String latinName, int ringingYear) {
        this.name = name;
        this.latinName = latinName;
        this.ringingYear = ringingYear;
    }

    @Override
    public String toString() {
        return this.latinName + "(" + this.ringingYear + ")";
    }
}

The idea is implementing the funcionality for the ringing centre to track of the places where rung birds were observed and how many times they were. However, observation places and times are not stored in Bird objects, but in a separate HashMap, whose keys are Bird objects. As we remember from Week 2, in such cases we have to implement the methods equals(Object other) and hashCode() in the class Bird.

Some birds have more than one English name (for instance, the Rose Starling is also known as Rose-Coloured Starling or Rose-Coloured Pastor); however, the Latin name is always unique. Create the methods equals and hashCode- in the class Bird; two Bird objects have to be understood as the same bird if their Latin name and observation year are the same.

Example:

    Bird bird1 = new Bird("Rose Starling", "Sturnus roseus", 2012);
    Bird bird2 = new Bird("Rose-Coloured Starling", "Sturnus roseus", 2012);
    Bird bird3 = new Bird("Hooded Crow", "Corvus corone cornix", 2012);
    Bird bird4 = new Bird("Rose-Coloured Pastor", "Sturnus roseus", 2000);

    System.out.println( bird1.equals(bird2));   // same Latin name and same observation year: they are the same bird
    System.out.println( bird1.equals(bird3));   // different Latin name: they are not the same bird
    System.out.println( bird1.equals(bird4));   // different observation year: not the same bird
    System.out.println( bird1.hashCode()==bird2.hashCode() );

Prints:

true
false
false
true

Ringing Centre

The Ringing Centre has two methods: public void observe(Bird bird, String place), which records the observation and its place among the bird information; and public void observations(Bird bird), which prints all the observations of the parameter bird following the example below. The observation printing order is not important, as far as the tests are concerned.

The Ringing Centre stores the observation places in a Map<Bird, List<String>> object variable. If you need, you can use the exercise from Section 16 as example.

An example of how the Ringing Centre works:

    RingingCentre kumpulaCentre = new RingingCentre();

    kumpulaCentre.observe( new Bird("Rose Starling", "Sturnus roseus", 2012), "Arabia" );
    kumpulaCentre.observe( new Bird("Rose-Coloured Starling", "Sturnus roseus", 2012), "Vallila" );
    kumpulaCentre.observe( new Bird("European Herring Gull", "Larus argentatus", 2008), "Kumpulanmäki" );
    kumpulaCentre.observe( new Bird("Rose Starling", "Sturnus roseus", 2008), "Mannerheimintie" );

    kumpulaCentre.observations( new Bird("Rose-Coloured Starling", "Sturnus roseus", 2012 ) );
    System.out.println("--");
    kumpulaCentre.observations( new Bird("European Herring Gull", "Larus argentatus", 2008 ) );
    System.out.println("--");
    kumpulaCentre.observations( new Bird("European Herring Gull", "Larus argentatus", 1980 ) );

Prints:

Sturnus roseus (2012) observations: 2
Arabia
Vallila
--
Larus argentatus (2008) observations: 1
Kumpulanmäki
--
Larus argentatus (1980) observations: 0

Object Polymorphism

Precedently, we have run into situations where variables had various different types, in addition to their own. For instance, in the section 45, we noticed that all objects are Object-type. If an object is a particular type, we can also represent it as Object-type. For instance, String is also Object-type, and all String variables can be defined using Object.

    String string = "string";
    Object string = "another string";

It is possible to assign a String to an Object-type reference.

    String string = "characterString";
    Object string = string;

The opposite way does not work. Because Object-type variables are not Strings, an Object variable cannot be assigned to a String variable.

    Object string = "another string";
    String string = string; // DOESN'T WORK!

What is the real problem?

Variables have got their own type, and in addition to it they also have got the type of their parent classes and interfaces. The class String derives from the Object class, and therefore String objects are also Object-type. The class Object does not derive from the class String, and therefore Object variables are not automatically String-type. Let's dig deeper into the String class API documentation, expecially the upper part of the HTML page.

The String class API documentation starts with the common heading; this is followed by the class package (java.lang). After the package you find the class name (Class String), and this is followed by the inheritance hierarchy.

java.lang.Object
  java.lang.String

The inheritance hierarchy lists the classes from which a class derives. The inherited classes are listed in hierarchical order, where the class we are analizing is the last one. As far as our String class inheritance hierarchy is concerned, we notice that the String class derives from the class Object. In Java, each class can derive from one class, tops; however, they can inherit features of more than one, undirectly.

You can think inheritance hierarchy as if it was a list of types, which the object has to implement.

The fact that all objects are Object-type helps programming. If we only need the features defined in the Object class in our method, we can use Object as method parameter. Because all objects are also Object-type, a method can be given whatever object as parameter. Let's create the method printManyTimes, which receives an Object variable as parameter, and the number of times this must be printed.

public class Printer {
    ...
    public void printManyTimes(Object object, int times) {
        for (int i = 0; i < times; i++) {
            System.out.println(object.toString());
        }
    }
    ...
}

You can give whaterver object parameter to the method printManyTimes. Within the method printManyTimes, the object has only the method which are defined in the Object class at its disposal, because the method is presented as Object-type inside the mehod.

    Printer printer = new Printer();

    String string = " o ";
    List<String> words = new ArrayList<String>();
    words.add("polymorphism");
    words.add("inheritance");
    words.add("encapsulation");
    words.add("abstraction");

    printer.printManyTimes(string, 2);
    printer.printManyTimes(words, 3);
 o
 o
[polymorphism, inheritance, encapsulation, abstraction]
[polymorphism, inheritance, encapsulation, abstraction]
[polymorphism, inheritance, encapsulation, abstraction]

Let's continue with our String class API inspection. In the description, the inheritance hierarchy is followed by a list of the interfaces which the class implements.

All Implemented Interfaces:
  Serializable, CharSequence, Comparable<String>

The String class implements the interfaces Serializable, CharSequence, and Comparable<String>. An interface is a type, too. According to the description of the String API, we should be able to set the following interfaces as the type of a String object.

    Serializable serializableString = "string";
    CharSequence charSequenceString = "string";
    Comparable<String> comparableString = "string";

Because we can define the parameter type of a method, we can define methods which would accept an object which implements a specific interface. When we define an interface as method parameter, the parameter can be whatever object which implements such interface, the method does not care about the object actual type.

Let's implement our Printer class, and create a method to print the characters of the objects which implement the interface CharSequence. The CharSequence interface also provides methods such as int length(), which returns the String's length, and char charAt(int index), which returns the character at a specific index.

public class Printer {
    ...
    public void printManyTimes(Object object, int times) {
        for (int i = 0; i < times; i++) {
            System.out.println(object.toString());
        }
    }

    public void printCharacters(CharSequence charSequence) {
        for (int i = 0; i < charSequence.length(); i++) {
            System.out.println(charSequence.charAt(i);
        }
    }
    ...
}

Whatever object which implements the CharSequence interface can be assigned to the method printCharacters. For instance, you can give a String or a StringBuilder which is usually more efficient when it comes to string building. The method printCharacters prints each character of the object in its own line.

    Printer printer = new Printer();

    String string = "works";

    printer.printCharacters(string);
w
o
r
k
s

Groups

In this exercise, we make organisms and groups of organisms which move around each other. The position of the organisms is reported by using a bidimensional coordinate system. Each position is defined by two numbers, the x and y coordinates. The x coordinate tells us how far from the "point zero" the position is horizontally, whereas the y coordinate tells u how far the position is vertically. If you have got doubts of what a coordinate system is, you can read more information in Wikipedia, for instance.

Together with the exercise, you find the interface Movable, which represents things that can be moved from one place to another. The interface contains the method void move(int dx, int dy). The parameter dx tells us how much the object moves on the x axis and dy tells us about the movement on the y axis.

Implement the classes Organism and Group, which are both movable. Implement all the functionality inside the package movable.

Implementing Organism

Create the class Organism in the package movable; let Organism implement the interface Movable. Organisms have to know their own position (x and y coordinates). The API of Organism has to be the following:

  • the constructor public Organism(int x, int y)
    ; it receives the x and y initial coordinates of the object
  • public String toString()
    ; it creates and returns a string which represents the object. The form should be the following "x: 3; y: 6". Note that the coordinates are separated by a semicolon (;)
  • public void move(int dx, int dy)
    ; it moves the object as much as it is specified by the arguments. The variable dx contains the x coordinate of the movement, whereas dy contains the y coordinate of the movement. For instance, if the value of the variable dx is 5, the object variable x has to be increased by five

Try our the functionality of Organism using the following code.

     Organism organism = new Organism(20, 30);
     System.out.println(organism);
     organism.move(-10, 5);
     System.out.println(organism);
     organism.move(50, 20);
     System.out.println(organism);
x: 20; y: 30
x: 10; y: 35
x: 60; y: 55

Implementing Group

Create the class Group in the package movable; Group implements the interface Movable. The Group is made of various different objects which implement the interface Movable, and they have to be stored into a list construction, for instance.

The class Group should have the following API.

  • public String toString()
    ; it returns a string which describes the position of the group organisms, each organism is printed in its own line.
  • public void addToGroup(Movable movable)
    ; it adds a new objects which implements the interface Movable to the group.
  • public void move(int dx, int dy)
    ; it moves a group as much as it is defined by the arguments. Note that you will have to move each group organism.

Try out your program functionality with the following code.

    Group group = new Group();
    group.addToGroup(new Organism(73, 56));
    group.addToGroup(new Organism(57, 66));
    group.addToGroup(new Organism(46, 52));
    group.addToGroup(new Organism(19, 107));
    System.out.println(group);
x: 73; y: 56
x: 57; y: 66
x: 46; y: 52
x: 19; y: 107

Inheritance of Class Features

Classes are for the programmer a way to clarify problematic concepts. With each class we create, we add new functionality to the programming language. The functionality is needed to solve the problems we meet, and the solutions are born from the interaction among the objects we create. In object programming, an object is an independent unity which can change through its methods. Objects are used together with each other; each object has its own area of responsibility. For instance, our user interface classes have been making use of Scanner objects, so far.

Each Java's class descends from the class Object, which means that each class we create has all methods which are defined in Object. If we want to change the functionality of the methods defined in Object, we have to Override them and define a new functionality in the created class.

In addition to be possible to inherit the Object class, it is also possible to inherit other classes. If we check Java's ArrayList class API we notice that ArrayList inherits the class AbstractList. The class AbstractList inherits the class AbstractCollection, which descended from the class Object.

java.lang.Object
  java.util.AbstractCollection<E>
      java.util.AbstractList<E>
          java.util.ArrayList<E>

Each class can inherit one class, directly, Indirectly, a class can still inherit all the feauters its parent class. The class ArrayList inherits directly the class AbstractList, and indirectly the classes AbstractCollection and Object. In fact, the class ArrayList has the methods and interfaces of AbstractList, AbstractCollection and Object at its disposal.

The class features are inherited using the keyword extends. The class which inherits is called subclass; the class which is inherited is called superclass. Let's get aquainted with a carmaker system, which handles car components. The basic component of component handling is the class Component which defines the identification number, the producer, and the description.

public class Component {

    private String id;
    private String producer;
    private String description;

    public Component(String id, String producer, String description) {
        this.id = id;
        this.producer = producer;
        this.description = description;
    }

    public String getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }

    public String getProducer() {
        return producer;
    }
}

One car component is its motor. As for all the other components, the motor has also got a producer, an identification number, and a description. In addition, a motor has also got a type: for instance, combustion engine, electric motor, or hybrid. Let's create the class Motor which inherits Component: a motor is a particular case of component.

public class Motor extends Component {

    private String motorType;

    public Motor(String motorType, String id, String producer, String description) {
        super(id, producer, description);
        this.motorType = motorType;
    }

    public String getMotorType() {
        return motorType;
    }
}

The class definition public class Motor extends Component tells us that the class Motor inherits the functionality of Component. In the class Motor, we define the object variable motorType.

The Motor class constructor is interesting. In the first line of the constructor we notice the keyword super, which is used to call the superclass constructor. The call super(id,producer,description) class the constructor public Component(String id, String producer, String description) which is defined in the class Component; in this way the superclass object variables are assigned a value. After doing this, we assign a value to the object variable motorType.

When the class Motor inherits the class Component, in receives all the methods provides by Component. It is possible to create an instance of the class Motor as it is for any other class.

        Motor motor = new Motor("combustion engine", "hz", "volkswagen", "VW GOLF 1L 86-91");
        System.out.println(motor.getMotorType());
        System.out.println(motor.getProducer());
combustion engine
volkswagen

As you notice, the class Motor has the methods defined in Component at its disposal.

Private, Protected and Public

If a method or a variable have got the private field accessibility, it can not be seen by its subclasses, and its subclasses do not have any straight way to access it. In the previous example Motor can't access directly the attributes defined in its superclass Component (id, producer, description). The subclass see naturally everything which has been defined public in its super class. If we want to define superclass variables or methods whose accessibility should be restricted to only its subclasses, we can use the protected field accessability.

Superclass

The superclass constructor is defined by the super keyword. In fact, the call super is similar to the this constructor call. The call is given the values of the type required by the super class constructor parameter.

When we call the constructor, the variables defined in the super class are initialized. In fact, with constructor call happens the same thing as in normal constructor calls. Unless the superclass has a constructor without parameter, in the subclass constructor call there must always be a call for its superclass constructor.

Attention! The super call must always be in the first line!

Calling the Superclass Methods

The method defined in the superclass can always be called using the super prefix, in the same way we call the methods defined in this class through the this prefix. For instance, we can make use of a method which overrives the superclass toString method in the following way:

    @Override
    public String toString() {
        return super.toString() + "\n  And my personal message again!";
    }

Person and their Heirs

Person

Create the package people and the class Person in it; Person works in relation to the following main program:

    public static void main(String[] args) {
        Person pekka = new Person("Pekka Mikkola", "Korsontie Street 1 03100 Vantaa");
        Person esko = new Person("Esko Ukkonen", "Mannerheimintie Street 15 00100 Helsinki");
        System.out.println(pekka);
        System.out.println(esko);
    }

Printing

Pekka Mikkola
  Korsontie Street 1 03100 Vantaa
Esko Ukkonen
  Mannerheimintie Street 15 00100 Helsinki

Student

Create the class Student which inherits the class Person.

Students have 0 credits, at the beginning. As long as a student studies, their credits grow. Create the class, in relation to the following main program:

    public static void main(String[] args) {
        Student olli = new Student("Olli", "Ida Albergintie Street 1 00400 Helsinki");
        System.out.println(olli);
        System.out.println("credits " + olli.credits());
        olli.study();
        System.out.println("credits "+ olli.credits());
    }

Prints:

Olli
  Ida Albergintie Street 1 00400 Helsinki
credits 0
credits 1

toString for Studets

The Student in the previous exercise inherits their toString method from the class Person. Inherited methods can also be overwritten, that is to say replaced with another version. Create now an own version of the toString method for Student; the method has to work as shown below:

    public static void main(String[] args) {
        Student olli = new Student("Olli", "Ida Albergintie Street 1 00400 Helsinki");
        System.out.println( olli );
        olli.study();
        System.out.println( olli );
    }

Prints:

Olli
  Ida Albergintie Street 1 00400 Helsinki
  credits 0
Olli
  Ida Albergintie Street 1 00400 Helsinki
  credits 1

Teacher

Create the class Teacher in the same package. Teacher inherits Person, but they also have a salary, which together with the teacher information in String form.

Check whether the following main program generates the prinout below

    public static void main(String[] args) {
        Teacher pekka = new Teacher("Pekka Mikkola", "Korsontie Street 1 03100 Vantaa", 1200);
        Teacher esko = new Teacher("Esko Ukkonen", "Mannerheimintie 15 Street 00100 Helsinki", 5400);
        System.out.println( pekka );
        System.out.println( esko );

        Student olli = new Student("Olli", "Ida Albergintie 1 Street 00400 Helsinki");
        for ( int i=0; i < 25; i++ ) {
            olli.study();
        }
        System.out.println( olli );
    }

Printing

Pekka Mikkola
  Korsontie Street 1 03100 Vantaa
  salary 1200 euros/month
Esko Ukkonen
  Mannerheimintie Street 15 00100 Helsinki
  salary 5400 euros/month
Olli
  Ida Albergintie Street 1 00400 Helsinki
  credits 25

Everyone in a List

Implement the method public static void printDepartment(List<Person> people) in the Main class, default package. The method prints all the people in the parameter list. When the main method is called, printDepartment should work in the following way.

    public static void printDepartment(List<Person> people) {
       // we print all the people in the department
    }

    public static void main(String[] args) {
        List<Person> people = new ArrayList<Person>();
        people.add( new Teacher("Pekka Mikkola", "Korsontie Street 1 03100 Vantaa", 1200) );
        people.add( new Student("Olli", "Ida Albergintie Street 1 00400 Helsinki") );

        printDepartment(people);
    }
Pekka Mikkola
  Korsontie Street 1 03100 Vantaa
  salary 1200 euros/month
Olli
  Ida Albergintie Street 1 00400 Helsinki
  credits 0

The Object Type defines the Called Method: Polymorphism

The method which can be called is defined through the variable type. For instance, if a Student object reference is saved into a Person variable, the object can use only the methods defined in the Person class:

   Person olli = new Student("Olli", "Ida Albergintie Street 1 00400 Helsinki");
   olli.credits();        // NOT WORKING!
   olli.study();              // NOT WORKING!
   String.out.println( olli );   // olli.toString() IT WORKS!

If the object has many different types, it has available the methods defined by all types. For instance, a Student object has available the methods defined both in the class Person and in Object.

In the previous exercise, we were in the class Student and we replaced the toString method inherited from Person with a new version of it. Suppose we are using an object throw a type which is not its real, what version of the object method would we call, then? For instance, below there are two students whose references are saved into a Person and an Object variables. We call the toString method of both. What version of the method is executed: the one defined in Object, in Person, or in Student?

   Person olli = new Student("Olli", "Ida Albergintie Street 1 00400 Helsinki");
   String.out.println( olli );

   Object liisa = new Student("Liisa", "Väinö Auerin Street 20 00500 Helsinki");
   String.out.println( liisa );

Printing:

Olli
  Ida Albergintie Street 1 00400 Helsinki
  credits 0
Liisa
  Väinö Auerin Street 20 00500 Helsinki
  credits 0

As you see, the execution method is chosen based on its real type, that is the type of the variable which saved the reference!

More generally: The execution method is always chosen based on the object real type, regardless of the variable type which is used. Objects are diverse, which means they can be used through different variable types. The execution method does always depend of the object actual type. This diversity is called polymorphysm.

Another Example: Points

A point laying in a bidimensional coordinate system can be represented with the help of the following class:

public class Point {

    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int manhattanDistanceFromOrigin(){
        return Math.abs(x)+Math.abs(y);
    }

    protected String location(){
        return x + ", " + y;
    }

    @Override
    public String toString() {
        return "("+this.location()+") distance "+this.manhattanDistanceFromOrigin();
    }
}

The location method is not supposed to be used outside its class, and its accessability field is protected, which means only subclasses can access it. For instance, if we use a path fiding algorithm, the Manhattan distance is the distance of two points moving on a strictly horizontal and/or vertical path, along the coordinate system lines.

A coloured point is similar to a point, except that it contains a string which tells us its colour. The class can be created by inheriting Point:

public class ColouredPoint extends Point {

    private String colour;

    public ColouredPoint(int x, int y, String colour) {
        super(x, y);
        this.colour = colour;
    }

    @Override
    public String toString() {
        return super.toString()+" colour: "+colour;
    }
}

The class defines an object variable which saves the colour. The coordinates are saved in the superclass. The string representation must be similar to the one of the point, but it also has to show the colour. The overwritten toString method, calls the superclass toString method, and it adds the point colour to it.

In the following example, we create a list which contains various different points, either normal or coloured. Thanks to polymorphysm, we call the actual toString method of all objects, even though the list knows them as if they were all Point-type:

public class Main {
    public static void main(String[] args) {
        List points = new ArrayList();
        points.add(new Point(4, 8));
        points.add(new ColouredPoint(1, 1, "green"));
        points.add(new ColouredPoint(2, 5, "blue"));
        points.add(new Point(0, 0));

        for (Point point : points) {
            System.out.println(point);
        }
    }
}

Printing:

(4, 8) distance 12
(1, 1) distance 2 colour: green
(2, 5) distance 7 colour: blue
(0, 0) distance 0

We also want a 3D point in our program. Because that is not a coloured point, it shall inherit Point:

public class 3DPoint extends Point {

    private int z;

    public 3DPoint(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    protected String location() {
        return super.location() + ", " + z;    // printing as "x, y, z"
    }

    @Override
    public int manhattanDistanceFromOrigin() {
        // first, we ask the superclass for the distance of x+y
        // and then we add the value of z to it
        return super.manhattanDistanceFromOrigin() + Math.abs(z);
    }

    @Override
    public String toString() {
        return "(" + this.location() + ") distance " + this.manhattanDistanceFromOrigin();
    }
}

A 3D point defines an object variable corresponding to the third coordinate, and it overrides the methods location, manhattanDistanceFromOrigin and toString so that they would take into account the tridimensionality. We can now extend the previous example and add 3D points to our list:

public class Main {

    public static void main(String[] args) {
        List points = new ArrayList();
        points.add(new Point(4, 8));
        points.add(new ColouredPoint(1, 1, "green"));
        points.add(new Point(2, 5, "blue"));
        points.add(new 3DPoint(5, 2, 8));
        points.add(new Point(0, 0));

        for (Point point : points) {
            System.out.println(point);
        }
    }
}

The output meets our expectations

(4, 8) distance 12
(1, 1) distance 2 colour: green
(2, 5) distance 7 colour: blue
(5, 2, 8) distance 15
(0, 0) distance 0

We notice that the tridimensional point toString method is exactly the same as the point's toString. Could we leave the toString method untouched? Of course! A tridimensional point can be reduced to the following:

public class 3DPoint extends Point {

    private int z;

    public 3DPoint(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    protected String distance() {
        return super.distance()+", "+z;
    }

    @Override
    public int manhattanDistanceFromOrigin() {
        return super.manhattanDistanceFromOrigin()+Math.abs(z);
    }
}

What does exactly happen when we call a tridimensional point's toString method? The execution proceeds in the following way:

  1. we look for a toString method in the class 3DPoint; this is not found and we move to its parent class
  2. we look for a toString method in the superclass Point; the method is found and we execute its code
    • the code to execute is return "("+this.location()+") location "+this.manhattanDistanceFromOrigin();
    • first, we execute the method location
    • we look for a location method in the class 3DPoint; the method is found and we executes its code
    • the location method calculates its result by calling the superclass method location
    • next, we look for the definition of the method manhattanDistanceFromOrigin in the class Point3D; the method is found and we excecute its code
    • once again, the method calculates its result by calling its homonym in the superclass

The operating sequence produced by the method call has many steps. The idea is clear, anyway: when we want to execute a method, we first look for its definition in the object real type, and if it is not found, we move to the super class. If the method is not found in the parent class either, we move to the parent parent class, and so on...

When Do We Have to Use Inheritance?

Inheritance is a tool to build and qualify object hierarchy; a subclass is always a special instance of the superclass. If the class we want to create is a special instance of an already existent class, we can create it by inheriting the class which already exists. For instance in our auto components example, motor is a component, but the motor has additional functionality which not all the classe s have.

Through inheritance, a subclass receives the superclass functionality. If a subclass does not need or use the inherited functionality, inheritance is not motivated. The inherited classes inherit the upperclass methods and interfaces, and therefore we can use a subclass for any purpose the superclass was used. It is good to stick to low inheritance hierarchy, because the more complex the inheritance hierarchy is, the more complex maintainance and further development will be. In general, if the hierarchy is higher than two or three, the program structure is usually to improve.

It is good to think about inheritance use. For instance, if Car inherited classes like Component or Motor, that would be wrong. A car contains a motor and components, but a car is not a motor or a component. More generally, we can think that if an object owns or is composed of the other objects, inheritance is wrong.

Devoloping hierachy, you have to make sure the Single Responsibility Principle applies. There must be only one reason to change a class. If you notice that the hierarchy increases a class responsibilities, that class must be divided into various different classes.

An Example of Inheritance Misuse

Let's think about the Customer class, in relation to post service. The class contains the customer personal information, and Order, which inherits the customer personal information and contains the information of the object to order. The class Order has also got the method mailingAddress, which tells the mailing address of the order.

public class Customer {

    private String name;
    private String address;

    public Customer(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}
public class Order extends Customer {

    private String product;
    private String amount;

    public Order(String product, String amount, String name, String address) {
        super(name, address);
        this.product = product;
        this.amount = amount;
    }

    public String getProduct() {
        return product;
    }

    public String getAmount() {
        return amount;
    }

    public String mailingAddress() {
        return this.getName() + "\n" + this.getAddress();
    }
}

The inheritance above is used erroneously. When a class inherits another, the subclass has to be a special instance of the superclass; order is not a special instance of customer. The misuse becomes apparent by breaking the single responsibility principle: the class Order is responsible for both the customer and the order information maintenance.

The problem with the previous solution becomes apparent when we think of what would happen if a customer changes their own address.

With a change of address, we would have to change each Order object of the customer, which is a hint about a bad situation. A better solution would be encapsulating Customer as an object variable of Order. If we think more specifically about the order semantics this becomes clear. An order has a customer. Let's change the class Order so that it contains a reference to Customer.

public class Order {

    private Customer customer;
    private String product;
    private String amount;

    public Tilaus(Customer customer, String product, String amount) {
        this.customer = customer;
        this.product = product;
        this.amount = amount;
    }

    public String getProduct() {
        return product;
    }

    public String getAmount() {
        return amount;
    }

    public String mailingAddress() {
        return this.customer.getName() + "\n" + this.customer.getAddress();
    }
}

The Order class above is better now. The method mailingAddress uses a Customer reference to retrieve the mailing address, instead of inheriting the class Customer. This makes easier both the maintanance and the concrete functionality of our program.

Now, when we modify a customer, we only need to change their information; we don't have to do anything about the orders.

Container

Together with the exercise, you find the class Container, with the following constructor and methods:

  • public Container(double capacity)
    It creates an empty container, whose capacity is given as argument; an improper capacity (<=0) creates a useless container, whose capacity is 0.
  • public double getVolume()
    It returns the volume of product in the container.
  • public double getOriginalCapacity()
    It returns the original capacity of the container, that is to say what the constructor was originally given.
  • public double getCurrentCapacity()
    It returns the actual capacity of the container.
  • public void addToTheContainer(double amount)
    It adds the specified amount of things to the container. If the amount is negative, nothing changes; if a part of that amount fits but not the whole of it, the container is filled up and the left over is thrown away.
  • public double takeFromTheContainer(double amount)
    We take the specified amount form the container, the method returns what we receive. If the specified amount is negative, nothing happens and zero is returned. If we ask for more than what there is in the container, the method returns all the contents.
  • public String toString()
    It returns the state of an object in String form like volume = 64.5, free space 123.5

In this exercise, we create various different containers out of our Container class. Attention! Create all the classes in the package containers.

Product Container, Phase 1

The class Container has control on the operations regarding the amount of a product. Now, we also want that products have their name and handling equipment. Program ProductContainer, a subclass of Container! Let's first implement a single object variable for the name of the contained product, a constructor and a getter for the name:

  • public ProductContainer(String productName, double capacity)
    It creates an empty product container. The product name and the container capacity are given as parameters.
  • public String getName()
    It returns the product name.

Remember in what way the constructor can make use of its upper class constructor in its first line!

Example:

        ProductContainer juice = new ProductContainer("Juice", 1000.0);
        juice.addToTheContainer(1000.0);
        juice.takeFromTheContainer(11.3);
        System.out.println(juice.getName()); // Juice
        System.out.println(juice);           // volume = 988.7, free space 11.3
Juice
volume = 988.7, free space 11.3

Product Container, Phase 2

As you see from the example above, the toString() method inherited by ProductContainer does not know anything about the product name (of course!). Something must be done for it! Let's also add a setter for the product name, at the same time:

  • public void setName(String newName) sets a new name to the product.
  • public String toString() returns the object state in String form, like Juice: volume = 64.5, free space 123.5

The new toString() method could be programmed using the getter inherited from the superclass, retrieving the values of inherited but hidden values field values. However, we have programmed the superclass in a way that it is already able to produce the container situation in String form: why should we bother to program this again? Make use of the inherited toString.

Remember that an overwritten method can still be called in its subclass, where we overwrite it!

Use demonstration:

        ProductContainer juice = new ProductContainer("Juice", 1000.0);
        juice.addToTheContainer(1000.0);
        juice.takeFromTheContainer(11.3);
        System.out.println(juice.getName()); // Juice
        juice.addToTheContainer(1.0);
        System.out.println(juice);           // Juice: volume = 989.7, space 10.299999999999955
Juice
Juice: volume = 989.7, free space 10.299999999999955

Container History

Sometimes, it can be interesting to know in what way the container situation has changed: is the container often rather empty or full, is the fluctuation considerable or not, and so on. Let's provide our ProductContainer class with the ability to record the container history.

Let's start by designing a useful tool.

We could directly implement an ArrayList<Double> object to track our container history in the class ProductConteiner; however, now we create a specific tool for this purpose. The tool has to encapsulate an ArrayList<Double> object.

ContainerHistory public constructor and methods:

  • public ContainerHistory() creates an empty ContainerHistory object.
  • public void add(double situation) adds the parameter situation to the end of the container history.
  • public void reset() it deletes the container history records.
  • public String toString() returns the container history in the form of a String. The String form given by the ArrayList class is fine and doesn't have to be modified.

ContainerHistory.java, Phase 2

Implement analysis methods for your ContainerHistory class:

  • public double maxValue() reutrns the greatest value in the container history. If the history is empty, the method returns 0.
  • public double minValue() reutrns the smallest value in the container history. If the history is empty, the method returns 0.
  • public double average() reutrns the average of the values in the container history. If the history is empty, the method returns 0.

ContainerHistory.java, Phase 3

Implement analysis methods for your ContainerHistory class:

  • public double greatestFluctuation() returns the absolute value of the single greatest fluctuation in the container history (attention: a fluctuation of -5 is greater than 4). If the history is empty or it contains one value, the method returns zero. Absolute value is the distance of a number from zero. For instance the absolute value of -5.5 is 5.5, and the absolute value of 3.2 is 3.2.
  • public double variance() returns the sample variance of the container history values. If the history is empty or it contains only one value, the method returns zero.

You find guidelines to calculate the variance in Wikipedia, in the population and sample variance section. For instance, the average of the numbers 3, 2, 7, and 2 is 3.5, and their sample variance is therefore ((3 - 3.5)² + (2 - 3.5)² + (7 - 3.5)² + (2 - 3.5)²)/(4 - 1) ˜ 5,666667.)

Product Container Recorder, Phase 1

Implement the class ProductContainerRecorder which inherits ProductContainer. In addition to the old methods, the new version provides services for the container history. The hisotry is handled with a ContainerHistory object.

The public constructor and methods:

  • public ProductContainerRecorder(String productName, double capacity, double initialVolume) creates a product container. The product name, capacity, and original volume are given as parameter. Record the original volume both as the stored product original volume and as the first value of the container history.
  • public String history() returns the container history in the following form: [0.0, 119.2, 21.2]. Use the String printout form as it is.

Attention: now we remember only the original volume.

Example:

// the well known way:
ProductContainerRecorder juice = new ProductContainerRecorder("Juice", 1000.0, 1000.0);
juice.takeFromTheContainer(11.3);
System.out.println(juice.getName()); // Juice
juice.addToTheContainer(1.0);
System.out.println(juice);           // Juice: volume = 989.7, free space 10.3
...
// history() does not work properly, yet:
System.out.println(juice.history()); // [1000.0]
   // in fact, we only retrieve the original value which was given to the constructor...
...

Printing:

Juice
Juice: volume = 989.7, free space 10.299999999999955
[1000.0]

Product Container Recorder, Phase 2

It's time to pick up history! The first version of our history knew only the original value. Implement the following methods:

  • public void addToTheContainer(double amount); this works like the method in Container, but the new situation is recorded in the history. Attention: you have to record the product volume in the container after the addition, not the amount which was added!
  • public double takeFromTheContainer(double amount); it works like the method in Container, but the new situation is recorded in the history. Attention: you have to record the product volume in the container after the operation, not the amount which was removed!

Use example:

// the well known way:
ProductContainerRecorder juice = new ProductContainerRecorder("Juice", 1000.0, 1000.0);
juice.takeFromTheContainer(11.3);
System.out.println(juice.getName()); // Juice
juice.addToTheContainer(1.0);
System.out.println(juice);           // Juice: volume = 989.7, free space 10.3
...
// but now we have our history record
System.out.println(juice.history()); // [1000.0, 988.7, 989.7]
...

Printing:

Juice
Juice: volume = 989.7, free space 10.299999999999955
[1000.0, 988.7, 989.7]

Remember how an overwritten method can be used inside the method that overwrites it!

Product Container Recorder, Phase 3

Implement the following method:

  • public void printAnalysis(), which prints the history information regarding the product, following the exercise below:

Use example:

ProductContainerRecorder juice = new ProductContainerRecorder("Juice", 1000.0, 1000.0);
juice.takeFromTheContainer(11.3);
juice.addToTheContainer(1.0);
//System.out.println(juice.history()); // [1000.0, 988.7, 989.7]

juice.printAnalysis();

The method printAnalysis prints:

Product: Juice
History: [1000.0, 988.7, 989.7]
Greatest product amount: 1000.0
Smallest product amount: 988.7
Average: 992.8
Greatest change: 11.299999999999955
Variance: 39.129999999999676

Product Container Recorder, Phase 4

Fill the analysis so that it prints the greatest fluctuation and the history variance.

Inheritance, Interfaces, Both, or None?

Inheritance does not exclude using interfaces, and viceversa. Interfaces are like an agreement on the class implementation, and they allow for the abstraction of the concrete implementation. Changing a class which implements an interface is quite easy.

As with interfaces, when we make use of inheritance, the subclasses are committed to provide all the superclass methods. Because of polymorphism, inheritance works as interfaces do. We can assign a subclass instance to a method which receives its superclass as parameter.

Below, we create a farm simulator, where we simulate the life in a farm. Note that the program does not make us of inheritance, and the interface use is scarce. With programs, we often create a first version which we improve later on. Typically, we don't already understand the scope of the problem when we implement the first version; planning interfaces and inheritance hierarchy may be difficult and it may slow down the work.

Farm Simulator

Dairy farms have got milking animals; they do not handle milk themselves, but milk trucks transport it to dairy factories which process it into a variety of milk products. Each dairy factory is specialised in one product type; for instance, a cheese factory produces cheese, a butter factory produces butter, and a milk factory produces milk.

Let's create a simulator which represents the milk course of life. Implement all the classes in the package farmsimulator.

Bulk Tank

Milk has to be stored in bulk tanks in good conditions. Bulk tanks are produced both with a standard capacity of 2000 litres, and with customer specific capacity. Create the class BulkTank, with the following constructors and methods.

  • public BulkTank()
  • public BulkTank(double capacity)
  • public double getCapacity()
  • public double getVolume()
  • public double howMuchFreeSpace()
  • public void addToTank(double amount) adds to the tank only as much milk as it fits; the additional milk will not be added, and you don't have to worry about a situation where the milk spills over
  • public double getFromTank(double amount) takes the required amount from the tank, or as much as there is left

Also, implement the toString method for your BulkTank. The toString method describes the tank situation by rounding down the litres using the ceil() method of class Math.

Test your bulk tank with the following program chunk:

        BulkTank tank = new BulkTank();
        tank.getFromTank(100);
        tank.addToTank(25);
        tank.getFromTank(5);
        System.out.println(tank);

        tank = new BulkTank(50);
        tank.addToTank(100);
        System.out.println(tank);

The program print output should look like the following:

20.0/2000.0
50.0/50.0

Note that when you call the println() method of the out object of class System, the method receives as paramater an Object variable; in such case, the print output is determined by the overwritten toString() method in BulkTank! We are in front of a case of polymorphism, because the method can work with different types.

Cow

If we want to produce milk, we also need cows. Cows have got names and udders. Udder capacity is a random value between 15 and 40; the class Random can be used to raffle off the numers, for instance, int num = 15 + new Random().nextInt(26);. The class Cow has the following functionality:

  • public Cow() creates a new cow with a random name
  • public Cow(String name) creates a new cow with its given name
  • String getName() returns the cow's name
  • double getCapacity() returns the udder capacity
  • double getAmount() returns the amount on milk available in the cow's udders
  • String toString() returns a String which describes the cow (see the example below)

Cow also implement the following interfaces: Milkable, which describes the cow's faculty for being milked, and Alive, which represents their faculty for being alive.

public interface Milkable {
    public double milk();
}

public interface Alive {
    public void liveHour();
}

When a cow is milked, all their milk provision is taken to be processed. As long as a cow lives, their milk provision increases slowly. In Finland, milking cows produce 25-30 litres of milk every day, on the average. We simulate this by producing 0.7-2 litres every hour.

If a cow is not given a name, they are assigned a random one from the list below.

    private static final String[] NAMES = new String[]{
        "Anu", "Arpa", "Essi", "Heluna", "Hely",
        "Hento", "Hilke", "Hilsu", "Hymy", "Ihq", "Ilme", "Ilo",
        "Jaana", "Jami", "Jatta", "Laku", "Liekki",
        "Mainikki", "Mella", "Mimmi", "Naatti",
        "Nina", "Nyytti", "Papu", "Pullukka", "Pulu",
        "Rima", "Soma", "Sylkki", "Valpu", "Virpi"};

Implement the class Cow, and test whether it works with the following program body.

        Cow cow = new Cow();
        System.out.println(cow);


        Alive livingCow = cow;
        livingCow.liveHour();
        livingCow.liveHour();
        livingCow.liveHour();
        livingCow.liveHour();

        System.out.println(cow);

        Milkable milkingCow = cow;
        milkingCow.milk();

        System.out.println(cow);
        System.out.println("");

        cow = new Cow("Ammu");
        System.out.println(cow);
        cow.liveHour();
        cow.liveHour();
        System.out.println(cow);
        cow.milk();
        System.out.println(cow);

The program print output can be like the following.

Liekki 0.0/23.0
Liekki 7.0/23.0
Liekki 0.0/23.0
Ammu 0.0/35.0
Ammu 9.0/35.0
Ammu 0.0/35.0

MilkingRobot

In modern dairy farms, milking robots handle the milking. The milking robot has to be connected to the bulk tank in order to milk an udder:

  • public MilkingRobot() creates a new milking robot
  • BulkTank getBulkTank() returns the connected bulk tank, or a null reference, if the tank hasn't been installed
  • void setBulkTank(BulkTank tank) installs the parameter bulk tank to the milking robot
  • void milk(Milkable milkable) milks the cow and fills the connected bulk tank; the method returns an IllegalStateException is no tank has been fixed

Implement the class MilkingRobot, and test it using the following program body. Make sure that the milking robot can milk all the objects which implement the interface Milkable!

        MilkingRobot milkingRobot = new MilkingRobot();
        Cow cow = new Cow();
        milkingRobot.milk(cow);
Exception in thread "main" java.lang.IllegalStateException: The MilkingRobot hasn't been installed
        at farmsimulator.MilkingRobot.milk(MilkingRobot.java:17)
        at farmsimulator.Main.main(Main.java:9)
Java Result: 1
        MilkingRobot milkingRobot = new MilkingRobot();
        Cow cow = new Cow();
        System.out.println("");

        BulkTank tank = new BulkTank();
        milkingRobot.setBulkTank(tank);
        System.out.println("Bulk tank: " + tank);

        for(int i = 0; i < 2; i++) {
            System.out.println(cow);
            System.out.println("Living..");
            for(int j = 0; j < 5; j++) {
                cow.liveHour();
            }
            System.out.println(cow);

            System.out.println("Milking...");
            milkingRobot.milk(cow);
            System.out.println("Bulk tank: " + tank);
            System.out.println("");
        }

The print output of the program can look like the following:

Bulk tank: 0.0/2000.0
Mella 0.0/23.0
Living..
Mella 6.2/23.0
Milking...
Bulk tank: 6.2/2000.0

Mella 0.0/23.0
Living..
Mella 7.8/23.0
Milking...
Bulk tank: 14.0/2000.0

CowHouse

Cows are kept (and in this case milked) in cowhouses. The original cowhouses have room for one milking robot. Note that when milking robots are installed, they are connected to a specific cowhouse bulk tank. If a cowhouse does not have a milking robot, it can't be used to handle the cow, either. Implement the class CowHouse with the following constructor and methods:

  • public CowHouse(BulkTank tank)
  • public BulkTank getBulkTank() returns the cowhouse bulk tank
  • public void installMilkingRobot(MilkingRobot milkingRobot) installs a milking robot and connects it to the cowhouse bulk tank
  • public void takeCareOf(Cow cow) milks the parameter cow with the help of the milking robot, the method throws an IllegalStateException if the milking robot hasn't been installed
  • public void takeCareOf(Collection<Cow> cows) milks the parameter cows with the help of the milking robot, the method throws an IllegalStateException if the milking robot hasn't been installed
  • public String toString() returns the state of the bulk tank contained by the cowhouse

Collection is Java's own interface, and it represents collections' behaviour. For instance, the classes ArrayList and LinkedList implement the interface Collection. All instances of classes which implement Collection can be iterated with a for-each construction.

Test your class CowHouse with the help of the following program body. Do not pay to much attention to the class LinkedList; apparently, it works as ArrayList, but the implemantation in encapsulates is slightly different. More information about this in the data structures course!

        CowHouse cowhouse = new CowHouse(new BulkTank());
        System.out.println("CowHouse: " + cowhouse);

        MilkingRobot robot = new MilkingRobot();
        cowhouse.installMilkingRobot(robot);

        Cow ammu = new Cow();
        ammu.liveHour();
        ammu.liveHour();

        cowhouse.takeCareOf(ammu);
        System.out.println("CowHouse: " + cowhouse);

        LinkedList<Cow> cowList = new LinkedList<Cow>();
        cowList.add(ammu);
        cowList.add(new Cow());

        for(Cow cow: cowList) {
            cow.liveHour();
            cow.liveHour();
        }

        cowhouse.takeCareOf(cowList);
        System.out.println("CowHouse: " + cowhouse);

The print output should look like the following:

CowHouse: 0.0/2000.0
CowHouse: 2.8/2000.0
CowHouse: 9.6/2000.0

Farm

Farms have got an owner, a cowhouse and a breed of cows. Farm also implements our old interface Alive: calling the method liveHour makes all the cows of the farm live for an hour. You also have to create method manageCows which calls Cowhouse's method takeCareOf so that all cows are milked. Implement your class Farm, and make it work according to the following example.

        Farm farm = new Farm("Esko", new CowHouse(new BulkTank()));
        System.out.println(farm);

        System.out.println(farm.getOwner() + " is a tough guy!");

Expected print output:

Farm owner: Esko
CowHouse bulk tank: 0.0/2000.0
No cows.
Esko is a tough guy!
        Farm farm = new Farm("Esko", new CowHouse(new BulkTank()));
        farm.addCow(new Cow());
        farm.addCow(new Cow());
        farm.addCow(new Cow());
        System.out.println(farm);

Expected print output:

Farm owner: Esko
CowHouse bulk tank: 0.0/2000.0
Animals:
        Naatti 0.0/19.0
        Hilke 0.0/30.0
        Sylkki 0.0/29.0
        Farm farm = new Farm("Esko", new CowHouse(new BulkTank()));

        farm.addCow(new Cow());
        farm.addCow(new Cow());
        farm.addCow(new Cow());

        farm.liveHour();
        farm.liveHour();

Expected print output:

Farm owner: Esko
CowHouse bulk tank: 0.0/2000.0
Animals:
        Heluna 2.0/17.0
        Rima 3.0/32.0
        Ilo 3.0/25.0
        Farm farm = new Farm("Esko", new CowHouse(new BulkTank()));
        MilkingRobot robot = new MilkingRobot();
        farm.installMilkingRobot(robot);

        farm.addCow(new Cow());
        farm.addCow(new Cow());
        farm.addCow(new Cow());


        farm.liveHour();
        farm.liveHour();

        farm.manageCows();

        System.out.println(farm);

Expected print output:

Farm owner: Esko
CowHouse bulk tank: 18.0/2000.0
Animals:
        Hilke 0.0/30.0
        Sylkki 0.0/35.0
        Hento 0.0/34.0

An Abstract Class

Abstract classes combine interfaces and inheritance. They do not produce instances, but you can create instances of their sublcasses. An abstract class can contain both normal and abstract methods, the first containing the method body, the second having only the method definition. The implementation of the abstract methods is left to the inheriting class. In general, we use abstract classes when the object they represent is not a clear, self-defined concept. In such cases, it is not possible to create instances of it.

Both when we define abstract classes and abstract methods, we use the keyword abstract. An abstract class is defined by the statement public abstract class ClassName, whereas an abstract method is defined by public abstract returnType methodName. Let's consider the following abstract class Operation, which provides a framework for operations, and their excecutions.

public abstract class Operation {

    private String name;

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

    public String getName() {
        return this.name;
    }

    public abstract void excecute(Scanner reader);
}

The abstract class Operation works as a framework to excetute different operations. For instance, an addition can be implemented by inheriting the class Operation in the following way.

public class Addition extends Operation {

    public Addition() {
        super("Addition");
    }

    @Override
    public void execute(Scanner reader) {
        System.out.print("Give the first number: ");
        int first = Integer.parseInt(reader.nextLine());
        System.out.print("Give the second number: ");
        int second = Integer.parseInt(reader.nextLine());

        System.out.println("The sum is " + (first + second));
    }
}

Because all classes which descend from Operation are also Operation-type, we can create a user interface based on Operation-type variables. The following class UserInterface contains a list of operations and a reader. The operations can be added dynamically in the user interface.

public class UserInterface {

    private Scanner reader;
    private List<Operation> operations;

    public UserInterface(Scanner reader) {
        this.reader = reader;
        this.operations = new ArrayList<Operation>();
    }

    public void addOperation(Operation operation) {
        this.operations.add(operation);
    }

    public void start() {
        while (true) {
            printOperations();
            System.out.println("Choice: ");

            String choice = this.reader.nextLine();
            if (choice.equals("0")) {
                break;
            }

            excecuteOperation(choice);
            System.out.println();
        }
    }

    private void printOperations() {
        System.out.println("\t0: Quit");
        for (int i = 0; i < this.operations.size(); i++) {
            String operationName = this.operations.get(i).getName();
            System.out.println("\t" + (i + 1) + ": " + operationName);
        }
    }

    private void executeOperation(String choice) {
        int operation = Integer.parseInt(choice);

        Operation chosen = this.operations.get(operation - 1);
        chosen.execute(reader);
    }
}

The user interface works in the following way:

        UserInterface ui = new UserInterface(new Scanner(System.in));
        ui.addOperation(new Addition());

        ui.start();
Operations:
        0: Quit
        1: Addition
Choice: 1
Give the first number: 8
Give the second number: 12
The sum is 20

Operations:
        0: Quit
        1: Addition
Choice: 0

The difference between interfaces and abstract classes is that abstract classes provide the program with more structure. Because it is possible to define the functionality of abstract classes, we can use them to define the default implementation, for instance. The user interface above made use of a definition of the abstract class to store the operation name.

Different Boxes

Together with the exercise body, you find the classes Thing and Box. The class Box is abstract, and it is programmed so that adding things always implies calling the method add. The add method, resposible of adding one thing, is abstract, and any box which inherits the class Box has to implement the method add. Your task is modifying the class Thing and implementing various different boxes based on Box.

Add all new classes to the package boxes.

package boxes;

import java.util.Collection;

public abstract class Box {

    public abstract void add(Thing thing);

    public void add(Collection<Thing> things) {
        for (Thing thing : things) {
            add(thing);
        }
    }

    public abstract boolean isInTheBox(Thing thing);
}

Modifications to Thing

Add an inspection to the constructor of Thing, to make sure that the thing's weight is never negative (weight 0 is accepted). If the weight is negative, the constructor has to throw an IllegalArgumentException. Also implement the methods equals and hashCode in the class Thing, allowing you to use the contains method of different lists and collections. Implement the methods without taking into consideration the value of the object variable weight. Of course, you can use NetBeans functionality to implement equals and hashCode.

Maximum Weight Box

Implement the class MaxWeightBox in the package boxes; the class inherits Box. MaxWeightBox has the constructor public MaxWeightBox(int maxWeight), which determines the box maximum weight. Things can be added to MaxWeightBox if and only if the thing weight does not exceed the box weight.

        MaxWeightBox coffeeBox = new MaxWeightBox(10);
        coffeeBox.add(new Thing("Saludo", 5));
        coffeeBox.add(new Thing("Pirkka", 5));
        coffeeBox.add(new Thing("Kopi Luwak", 5));

        System.out.println(coffeeBox.isInTheBox(new Thing("Saludo")));
        System.out.println(coffeeBox.isInTheBox(new Thing("Pirkka")));
        System.out.println(coffeeBox.isInTheBox(new Thing("Kopi Luwak")));
true
true
false

One-Thing Box and Black-Hole Box

Next, implement the class OneThingBox in the package boxes; the class inherits Box. OneThingBox has the constructor public OneThingBox(), and only one thing can fit there. If the box already contains one thing, this should not be changed. The weight of the added thing is not important.

        OneThingBox box = new OneThingBox();
        box.add(new Thing("Saludo", 5));
        box.add(new Thing("Pirkka", 5));

        System.out.println(box.isInTheBox(new Thing("Saludo")));
        System.out.println(box.isInTheBox(new Thing("Pirkka")));
true
false

Next, implement the class BlackHoleBox in the package boxes; the class inherits Box. BlackHoleBox has the constructor public BlackHoleBox(); any thing can be added to a black-hole box, but none will be found when you'll look for them. In other words, adding things must always work, but the method isInTheBox has to return always false.

        BlackHoleBox box = new BlackHoleBox();
        box.add(new Thing("Saludo", 5));
        box.add(new Thing("Pirkka", 5));

        System.out.println(box.isInTheBox(new Thing("Saludo")));
        System.out.println(box.isInTheBox(new Thing("Pirkka")));
false
false

Removing Objects from an ArrayList

In the following exercise we see what you may end up to, when you want to remove a part of the list objects while parsing an ArrayList:

   // somewhere, with a definition like:
   // ArrayList<Object> list = new ...

   for ( Object object : list ) {
      if ( hasToBeRemoved(object) ) {
         list.remove(object);
      }
   }

The solution does not work and it throws a ConcurrentModificationException, because it is not possible to modify a list while parsing it with a foreach iterator. We will come back to the topic better on week 6. If you run into such a situation, you can handle it in the following way:

   // somewhere, with a definition like:
   // ArrayList<Object> list = new ...

   ArrayList<Object> toBeRemoved = new ArrayList<Object>();

   for ( Object object : list ) {
      if ( hasToBeRemoved(object) ) {
         toBeRemoved.add(object);
      }
   }

   list.removeAll(toBeRemoved);

The objects which have to be deleted are gathered together while we parse the list, and the remove operation is executed only after parsing the list.

Dungeon

This exercise is worth four points. Attention! Implement the all functionality in the package dungeon.

Attention: you can create only one Scanner object to make your tests works. Do not use Scandinavian letters in the class names. Also, do not use static variables, the tests execute your program many different times, and the static variable values left from the previous execution would possibly disturb them!

With this exercise, you manage to implement a dungeon game. In the game, the player is in a dungeon full of vampires. The player has to stub the vampires before his lamp runs out of battery and the vampires can suck his blood in the darkness. The player can see the vampires with a blinking of their lamp, after which they have to move blind before the following blinking. With one move, the player can walk as many steps as they want.

The game situation, i.e. the dungeon, the player and the vampires are shown in text form. The first line in the print output tells how many moves the player has left (that is to say, how much battery the lamp has). After that, the print output shows player and vampire positions, which in turn are followed by the game map. In the example below, you see the player (@) and three vampires (v); in this case, the player has enough light for fourteen moves.

14

@ 1 2
v 6 1
v 7 3
v 12 2

.................
......v..........
.@.........v.....
.......v.........

The example above shows the lamp has enough battery for 14 blinkings. The player @ is located at 1 2. Note that the coordinates are calculated starting from the high left corner of the game board. In the map below, the character X is located at 0 0, Y is at 2 0 and Z is at 0 2.

X.Y..............
.................
Z................
.................

The user can move by giving a sequence of commands and pressing Enter. The commands are:

  • w go up
  • s go down
  • a go left
  • d go right

When the user commands are executed (the user can give many commands at once), a new game situation is drawn. If the lamp charge reaches 0, the game ends and the text YOU LOSE is printed on the board.

The vampires move randomly in the game, and they take one step for each step the player takes. If the player and a vampire run into each other (even momentarily) the vampire is destroyed. If a vampire tries to step outside the board, or into a place already occupied by another vampire, the move is not executed. When all the vampires are destroyed, the game ends and it prints YOU WIN.

In order to help the tests, create the class Dungeon in your game, with:

  • the constructor public Dungeon(int length, int height, int vampires, int moves, boolean vampiresMove)

    the values length and height represent the dimension of the dungeon (always a square); vampires stands for the initial number of vampires (the positions of the vampires can be decided randomly); moves determines the initial number of moves; and if vampiresMove is false, the vampires do not move.

  • the method public void run(), which starts the game

Attention! The player starts the game in the position 0,0!

Attention! Player and vampires can not move out of the dungeon and two vampires cannot step into the same place!

Below, you find a couple of examples to help you to understand the situation better:

14

@ 0 0
v 1 2
v 7 8
v 7 5
v 8 0
v 2 9

@.......v.
..........
.v........
..........
..........
.......v..
..........
..........
.......v..
..v.......

ssd
13

@ 1 2
v 8 8
v 7 4
v 8 3
v 1 8

..........
..........
.@........
........v.
.......v..
..........
..........
..........
.v......v.
..........

ssss
12

@ 1 6
v 6 9
v 6 5
v 8 3

..........
..........
..........
........v.
..........
......v...
.@........
..........
..........
......v...

dd
11

@ 3 6
v 5 9
v 6 7
v 8 1

..........
........v.
..........
..........
..........
..........
...@......
......v...
..........
.....v....

ddds
10

@ 6 7
v 6 6
v 5 0

.....v....
..........
..........
..........
..........
..........
......v...
......@...
..........
..........

w
9

@ 6 6
v 4 0

....v.....
..........
..........
..........
..........
..........
......@...
..........
..........
..........

www
8

@ 6 3
v 4 0

....v.....
..........
..........
......@...
..........
..........
..........
..........
..........
..........

aa
7

@ 4 3
v 4 2

..........
..........
....v.....
....@.....
..........
..........
..........
..........
..........
..........

w
YOU WIN
Week5

Writing to a File

In section 15, we learnt that reading from a file happened with the help of the classes Scanner and File. The class FileWriter provides the functionality to write to a file. The FileWriter constructor is given as parameter a String illustrating the file location.

        FileWriter writer = new FileWriter("file.txt");
        writer.write("Hi file!\n"); // the line break has to be written, too!
        writer.write("Adding text\n");
        writer.write("And more");
        writer.close(); // the call closes the file and makes sure the written text goes to the file

In the example we write the string "Hi file!" to the file "file.txt"; that is followed by a line break, and by more text. Note that when you use the write method, it does not produce line breaks, but they have to be added later manually.

Both the FileWriter constructor and the write method may throw an exception, which has to be either handled or the responsibility has to be delegated to the calling method. The method which is given as parameter the file name and the text to write into it can look like the following.

public class FileHandler {

    public void writeToFile(String fileName, String text) throws Exception {
        FileWriter writer = new FileWriter(fileName);
        writer.write(text);
        writer.close();
    }
}

In the above writeToFile method, we first create a FileWriter object, which writes into the fileName file stored at the location specified as parameter. After this, we write into the file using the write method. The exception the constructor and write method can possibly throw has to be handled either with the help of a try-catch block or delegating the responsibility. In the method writeToFile the responsibility was delegated.

Let's create a main method where we call the writeToFile method of a FileHandler object. The exception does not have to be handled in the main method either, but the method can declare to throw possibly an exception throw the definition throws Exception.

    public static void main(String[] args) throws Exception {
        FileHandler handler = new FileHandler();
        handler.writeToFile("diary.txt", "Dear Diary, today was a nice day.");
    }

When we call the method above, we create the file "diary.txt", where we write the text "Dear Diary, today was a nice day.". If the file exists already, the old content is erased and the new one is written. The method append() allows us to add text at the end of the already existing file, without erasing the existing text. Let's add the method appendToFile() to the class FileHandler; the method appends the text received as parameter to the end of the file.

public class FileHandler {
    public void writeToFile(String fileName, String text) throws Exception {
        FileWriter writer = new FileWriter(fileName);
        writer.write(text);
        writer.close();
    }

    public void appendToFile(String fileName, String text) throws Exception {
        FileWriter writer = new FileWriter(fileName);
        writer.append(text);
        writer.close();
    }
}

In most of the cases, instead of writing text at the end of a file with the method append, it is easier to write all the file again.

File Manager

Together with the exercise body, you find the class FileManager, which contains the method bodies to read a write a file.

File Reading

Implement the method public ArrayList<String> read(String file) to return the lines of the parameter file in ArrayList form, each file line being a String contained by the ArrayList.

There are two text files to help testing the project: src/testinput1.txt and src/testinput2.txt. The methods are supposed to be used in the following way:

    public static void main(String[] args) throws FileNotFoundException, IOException {
        FileManager f = new FileManager();

        for (String line : f.read("src/testinput1.txt")) {
            System.out.println(line);
        }
    }

The print output should look like the following

first
second

Writing a Line

Modify the method public void save(String file, String text) so that it would write the string of the second argument into the file of the first argument. If the file already exists, the string is written over the old version.

Writing a List

Modify the method public void save(String file, ArrayList texts) so that it would write the strings of the second argument into the file of the first argument; each string of the array list has to go to its own line. If the file already exists, the strings are written over the old version.

Two-Direction Dictionary

With this exercise, we develop the dictionary we implemented earlier, so that words can be both read and written into the file. Also, the dictionary has to translate in both directions, from Finnish into English and from English into Finnish (in this exercise, we suppose unofficially that Finnish and English do not have words which are spellt the same). Your task is creating the dictionary in the class MindfulDictionary. The class has to be implemented in the package dictionary.

Forgetful Basic Functionality

Create a parameterless constructor, as well as the methods:

  • public void add(String word, String translation)
  • adds a word to the dictionary. Each word has only one translation; if the same word is added twice, nothing happens.
  • public String translate(String word)
  • returns the word translation; if the word isn't recognised, it returns null

At this point, the dictionary has to work in the following way:

MindfulDictionary dict = new MindfulDictionary();
dict.add("apina", "monkey");
dict.add("banaani", "banana");
dict.add("apina", "apfe");

System.out.println( dict.translate("apina") );
System.out.println( dict.translate("monkey") );
System.out.println( dict.translate("programming") );
System.out.println( dict.translate("banana") );

Prints:

monkey
apina
null
banaani

As you notice from the example, after adding a pair the dictionary can translate in both directions.

Removing Words

Add the method public void remove(String word), which removes the given word and its translation from your dictionary.

At this point, the dictionary has to work in the following way:

MindfulDictionary dict = new MindfulDictionary();
dict.add("apina", "monkey");
dict.add("banaani", "banana");
dict.add("ohjelmointi", "programming");
dict.remove("apina");
dict.remove("banana");

System.out.println( dict.translate("apina") );
System.out.println( dict.translate("monkey") );
System.out.println( dict.translate("banana") );
System.out.println( dict.translate("banaani") );
System.out.println( dict.translate("ohjelmointi") );

Prints

null
null
null
null
programming

As you see, the delection happens in both ways: whether you remove a word or its translation, the dictionary loses the both the pieces of information.

Loading a File

Create the constructor public MindfulDictionary(String file) and the method public boolean load(), which loads a file whose name is given as parameter in the dictionary constructor. If opening or reading the file does not work, the method returns false and otherwise true.

Each line of the dictionary file contains a word and its translation, divided by the character ":". Together with the exercise body, you find a dictionary file meant to help the tests. It looks like the following:

apina:monkey
alla oleva:below
olut:beer

Read the dictionary file line by line with the reader method nextLine. You can split the lines with the String method split, in the following way:

Scanner fileReader = new ...
while ( fileReader.hasNextLine() ){
    String line = fileReader.nextLine();
    String[] parts = line.split(":");   // the line is split at :

    System.out.println( parts[0] );     // the part of the line before :
    System.out.println( parts[1] );     // the part of the line after :
}

The dictionary is used in the following way:

MindfulDictionary dict = new MindfulDictionary("src/words.txt");
dict.load();

System.out.println( dict.translate("apina") );
System.out.println( dict.translate("ohjelmointi") );
System.out.println( dict.translate("alla oleva") );

Printing

monkey
null
below

Saving Data

Create the method public boolean save(); when the method is called, the dictionary contents are written into the file whose name was given as parameter to the constructor. The method returns false if the file can't be saved; otherwise it returns true. Dictionary files have to be saved in the form described above, meaning that the program has to be able to read its own files.

Attention: even though the dictionary can translate in both directions, only one direction has to be stored into the dictionary file. For instance, if the dictionary knows that tietokone = computer, you have to write either the line:

tietokone:computer

or the line

computer:tietokone

but not both!

It may be useful to write the new translation list over the old file; in fact, the append command which came out in the material should not be used.

The final version of your dictionary should be used in the following way:

MindfulDictionary dict = new MindfulDictionary("src/words.txt");
dict.load();

// using the dictionary

dict.save();

At the beginning we load the dictionary from our file, and we save it back in the end, so that the changes made to the dictionary will be available next time, too.

User Interfaces


Attention! A part of the user interface tests opens a user interface and uses your mouse to click on the user interface components. When you are executing user interface tests, do not use your mouse!


So far, our programs have only been composed of application logic and text user interface which made use of application logic. In a couple of exercises we have also got a graphical user interface, but they had usually been created for us. Next, we see how we can create graphical user interfaces in Java.

User interfaces are windows which contain different types of buttons, text boxes, and menus. When we program user interfaces we use Java's Swing component library, which provides us with classes to create and handle user interfaces.

The basic element of a user interface is the class JFrame, and we create the user interface components in its component section. Orthodox user interfaces implement the interface Runnable, and they are started in the main program. In this course, we use the following user interface body:

import java.awt.Container;
import java.awt.Dimension;
import javax.swing.JFrame;
import javax.swing.WindowConstants;

public class UserInterface implements Runnable {

    private JFrame frame;

    public UserInterface() {
    }

    @Override
    public void run() {
        frame = new JFrame("Title");
        frame.setPreferredSize(new Dimension(200, 100));

        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        createComponents(frame.getContentPane());

        frame.pack();
        frame.setVisible(true);
    }

    private void createComponents(Container container) {
    }

    public JFrame getFrame() {
        return frame;
    }
}

Let's have a closer look the the user interface code above.

public class UserInterface implements Runnable {

The class UserInterface implements Java's Runnable interface, which allows us to execute a threaded program. Executing a threaded program means that we execute different parts of the program at the same time. We do not dig deeper into threads, but a further information on threads is provided by the course Operating Systems.

    private JFrame frame;

The user interface contains a JFrame object as variable, which is the basic element of a visible user interface. All user interface components are added to the JFrame object component container. Note that object variables cannot be initiated outside the methods. For instance, an initialisation of the object variable JFrame with the class definition "private JFrame frame = new JFrame()" would evade user interface thread execution order, and it can lead to a breakdown.

    @Override
    public void run() {
        frame = new JFrame("Title");
        frame.setPreferredSize(new Dimension(200, 100));

        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        createComponents(frame.getContentPane());

        frame.pack();
        frame.setVisible(true);
    }

The interface Runnable defines the method public void run(), which has to be implemented by all classes which implement the interface. With the method public void run(), we first create a new JFrame whose title is "Title". After this, we define the frame size whose width is 200 pixels and height is 100 pixels. The statement frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); tells to the JFrame object that the user interface has to close when the user presses the cross icon.

Afterwards, we call the method createComponents which is defined lower down in the class. The method is given JFrame's Container object as parameter, where we can add user interface components.

Finally, we call the method frame.pack() which packs the JFrame object as defined before and sorts the user interface components of the Container object contained by JFrame. At the end, we call the method frame.setVisible(true), to show the user interface to the user.

    private void createComponents(Container container) {
    }

In the method createComponents we add user interface components to the JFrame's container. In our example there is no UI component in addition to our JFrame window. The class UserInterface has also the method getFrame which we can use to retrieve the JFrame object which is encapsulated in the class.

Swing user interfaces are started through the method invokeLater, which is provided by the class SwingUtilities. invokeLater receives as parameter an object which implements the interface Runnable. The method adds the Runnable object to the execution queue and calls it as soon as possible. With the classSwingUtilities, we can start new threads when we need them.

import javax.swing.SwingUtilities;

public class Main {

    public static void main(String[] args) {
        UserInterface ui = new UserInterface();
        SwingUtilities.invokeLater(ui);
    }
}

When we execute the main method above, the user interface we have defined in the class UserInterface appears in our screen.

UI Components

User Interfaces are composed of a background window (JFrame) and a component Container, as well as the UI components which are set into the container. UI components are different kinds of buttons, texts, and other items. Every component has its own class. It's useful to get accustomed to Oracle visual sequence of components at the address http://docs.oracle.com/javase/tutorial/uiswing/components/index.html.

Text

Text can be displayed with the help of the class JLabel. JLabel provides a UI component which can be assigned text and whose text can be modified. The text is assigned either in the constructor, or separately, with the setText method.

Let's modify our UI container to display text. We create a new JLabel text component within the method createComponents. Then we retrieve the Container object from our JFrame object, and we add JLabel to Container using its add method.

    private void createComponents(Container container) {
        JLabel text = new JLabel("Text field!");
        container.add(text);
    }

As you see from the code above, JLabel shall display the text "Text field!". When we execute the user interface, we see the following window.

Greeter

Implement a user interface which displays the text "Hi!". The width of the user interface (i.e. of the JFrame object) has to be at least 400px, its height 200px, and its title should be "Swing on". The JFrame object should be created and become visible inside the method run(), and the text components are added to the user interface with the method createComponents(Container container).

ATTENTION: The ui object variables have to be initiated in the methods or in the constructor! Do not initiate an object variable directly in its definition.

Buttons

You can add buttons to your user interface using the class JButton. Adding a JButton object to your user interface is similar to adding a JLabel object.

    private void createComponents(Container container) {
        JButton button = new JButton("Click!");
        container.add(button);
    }

Next, we try to add both text and a button to our user interface.

    private void createComponents(Container container) {
        JButton button = new JButton("Click!");
        container.add(button);
        JLabel text = new JLabel("Text.");
        container.add(text);
    }

When we execute the program we see the following user interface.

Only the last component we have added is visible, and our program does not work as we would expect. What is the problem, in fact?

Setting up UI Components

All UI components have got their own location in the user interface. The component location is defined by the UI Layout Manager. Before, when we tried to add many different components to our Container object, only one component became visible. Every Container object has a default UI layout manager: BorderLayout.

BorderLayout places the UI components to five areas: the user interface centre and the four compass points. When we use the Container's add method, we can give it another parameter, clarifying where we would like to place the component. the BorderLayout class has five class variables available for use: BorderLayout.NORTH, BorderLayout.EAST, BorderLayout.SOUTH, BorderLayout.WEST, ja BorderLayout.CENTER.

The UI layout manager we want to use is assigned to the Container object in the parameter of the method setLauout. In addition to the UI component, the method add can also be assigned the location wehere the component should be placed. In the example below, we assign a component to every BorderLayout location.

    private void createComponents(Container container) {
        // the following line is not essential in this case, because BorderLayout is default in JFrame
        container.setLayout(new BorderLayout());

        container.add(new JButton("North"), BorderLayout.NORTH);
        container.add(new JButton("East"), BorderLayout.EAST);
        container.add(new JButton("South"), BorderLayout.SOUTH);
        container.add(new JButton("West"), BorderLayout.WEST);
        container.add(new JButton("Center"), BorderLayout.CENTER);

        container.add(new JButton("Default (Center)"));
    }

Notice that the button "Center" is not visible in our user interface because the button "Default (Center)" is assigned to its place by default. A container with the code above will look like the following after increasing its size manually.

As for UI components, there are also many UI layout managers. Oracle has a visual guide to learn more about UI layout managers at http://docs.oracle.com/javase/tutorial/uiswing/layout/visual.html. Below, we introduce the layout manager BoxLayout.

BoxLayout

When we use BoxLayout, UI components are added into the user interface either horizontally or vertically. The BoxLayout constructor is given a Container object as parameter -- where we have been adding the UI components -- and the layout direction of the UI components. The layout direction can be either BoxLayout.X_AXIS, i.e. components are set up horizontally, or BoxLayout.Y_AXIS, i.e. the componets are set up vertically. Differently than BorderLayout, BoxLayout does not have a limited number of places. In other words, you can add to your Container as many components as you want.

Arranging the user interface with BoxLayout works as using BorderLayout. We first create the layout manager and we assign it to the Container object using its method setLayout. After this, we can add components to the Container object using the add method. We don't need a further parameter specifying the location. Below, you find an example of components placed in horizontal order.

    private void createComponents(Container container) {
        BoxLayout layout = new BoxLayout(container, BoxLayout.X_AXIS);
        container.setLayout(layout);

        container.add(new JLabel("First!"));
        container.add(new JLabel("Second!"));
        container.add(new JLabel("Third!"));
    }

Setting up the components vertically does not require major changes. We modify the direction parameter of the BoxLayout constructor: BoxLayout.Y_AXIS.

    private void createComponents(Container container) {
        BoxLayout layout = new BoxLayout(container, BoxLayout.Y_AXIS);
        container.setLayout(layout);

        container.add(new JLabel("First!"));
        container.add(new JLabel("Second!"));
        container.add(new JLabel("Third!"));
    }

Using the different layout managers, we can create user interfaces where the components are set up appropriately. Below, there is an example of user interface where the components are placed vertically. First there is some text, and then an optional selection. You can create a multiple-exclusion scope for a set of buttons -- meaning that turning "on" one of those buttons turns off all the others in the group -- using ButtonGroup and JRadioButton.

    private void createComponents(Container container) {
        BoxLayout layout = new BoxLayout(container, BoxLayout.Y_AXIS);
        container.setLayout(layout);

        container.add(new JLabel("Choose meat or fish:"));

        JRadioButton meat = new JRadioButton("Meat");
        JRadioButton fish = new JRadioButton("Fish");

        ButtonGroup buttonGroup = new ButtonGroup();
        buttonGroup.add(meat);
        buttonGroup.add(fish);

        container.add(meat);
        container.add(fish);
    }

Once the UI is launched, and Meat is selected, the UI looks as follows.

Survey

Implement a user interface in the exercise body; the interface has to look like the following:

Use BoxLayout as layout manager for your user interface; the components are the classes JLabel, JRadioButton, JCheckBox and JButton.

Use the class ButtonGroup to make sure the options "No reason." and "Because it is fun!" cannot be chosen at the same time.

Make sure that the user interface is big enough so that the user can click the buttons without resizing the interface. For instance, you can use 200 pixels for the width and 300 pixels for the height.

Managing Action Events

So far, even though our user interfaces are beautiful, they are quite boring: they do not react in any way according to the actions done on the interfaces. Such unresponsiveness does not depend on our user interface components, but on the fact we haven't provided them with any way to listen to action events.

Action event listeners listen the UI components they are assigned to. Always when we perform an action on our UI components -- pressing a button, for instance -- the UI component calls a particular method of all the action event listeners assigned to it. Action event listeners are classes which implement a particular interface, and whose instances can be assigned to UI components. When an action event happens, the UI component goes through all its action event listeners, and calls the method defined by the interface.

The most used action event listener interface with Swing user interfaces is ActionListener. The interface ActionListener defines the method void actionPerformed(ActionEvent e), which receives an ActionEvent object as parameter.

Let's implement our first own action event listener, which has to print a message only when we press the relative button. The class MessageListener implements ActionListener and prints the message "Message received!" when the method actionPerformed is called.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class MessageListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent ae) {
        System.out.println("Message received!");
    }
}

Next, we create the a JButton for our user interface, and we add a instance of MessageListener to it. The class JButton can be added an action event listener by using the method defined in its parent class AbstractButton: public void addActionListener(ActionListener actionListener).

    private void createComponents(Container container) {
        JButton button = new JButton("Send!");
        button.addActionListener(new MessageListener());

        container.add(button);
    }

When we press the button in our user interface we see the following message.

Message received!

Handling Objects in the Action Event Listeners

Often, we want that an action event listener modified the state of an object. In order to have access to the object in the action event listener, the action event listener constructor has to be assigned a reference to the obejct concerned. Action eventlisteners are exactly similar to other Java's class, and we can program their whole functionality.

Let's take the following user interface, which has two JTextAreas -- where the user can input text, and a JButton. The user interface makes use of GridLayout, which makes the user interface look like a coordinate system. In the GridLayout constructor, we defined one line and three columns.

    private void createComponents(Container container) {
        GridLayout layout = new GridLayout(1, 3);
        container.setLayout(layout);

        JTextArea textAreaLeft = new JTextArea("The Copier");
        JTextArea textAreaRight = new JTextArea();
        JButton copyButton = new JButton("Copy!");

        container.add(textAreaLeft);
        container.add(copyButton);
        container.add(textAreaRight);
    }

After a manual resize, the UI looks like the following.

We want to implement our user interface so that the text in the left area would be copied into the right area when we press the JButton. This is possible by implementing an action event listener. Let's create the class AreaCopier which implements ActionListener and copies the text from one to the other JTextArea.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JTextArea;

public class AreaCopier implements ActionListener {

    private JTextArea origin;
    private JTextArea destination;

    public AreaCopier(JTextArea origin, JTextArea destination) {
        this.origin = origin;
        this.destination = destination;
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        this.destination.setText(this.origin.getText());
    }
}

Adding the new action event listener to the JButton object is possible using the method addActionListener.

    private void createComponents(Container container) {
        GridLayout layout = new GridLayout(1, 3);
        container.setLayout(layout);

        JTextArea textAreaLeft = new JTextArea("The Copier");
        JTextArea textAreaRight = new JTextArea();
        JButton copyButton = new JButton("Copy!");

        AreaCopier copier = new AreaCopier(textAreaLeft, textAreaRight);
        copyButton.addActionListener(copier);

        container.add(textAreaLeft);
        container.add(copyButton);
        container.add(textAreaRight);
    }

When we press the button, the text in the left area is copied into the right one.

Notice Board

Implement a user interface in the exercise body; the interface has to look like the following:

The program has to be composed of the following classes, which are contained in the package noticeboard. The class NoticeBoard is the user interface class, and it is started from the Main class. The notice board has various different components: JTextField, JButton, and JLabel. You can manage the layout of the ui components with GridLayout: the call new GridLayout(3, 1) creates a new layout manager, which sets up three ui elements vertically.

The application also has to contain the class ActionEventListener, which implements the interface ActionListener. The action event listener is connected to the button, and when pressed, it has to copy the contents of the JTextField into JLabel. At the same time, it wipes the JTextField by setting its contents as "".

Make sure that the user interface is big enough to click on each button.

Separating Application and UI Logic

Mixing the application logic (the functionality to print or compute, for instance) and the user interface together in the same classes is usually a bad thing. It makes much more difficult to test and modify a program, and it makes the code much more difficult to read. As the single responsibility principle states: each class should have only one clear responsibility. Separating the application logic from the UI logic works smoothly planning your interfaces appropriately. Let's suppose, that we have got a the class PersonRecord, and we want to implement a user interface to record people.

public interface PersonRecord {
    void record(Person person);
    Person get(String id);

    void delete(Person person);
    void delete(String id);
    void deleteAll();

    Collection<Person> getAll();
}

UI Implementation

When we implement our user interface, a good start is adding the components to it. If we want to record people, we need fields for their name and their ID number, as well as a button to add the person. We use Java's JTextField to input text, and the class JButton to implement our button. In addition, we also create JLabel textual descriptions which tell the user what to do.

For our UI layout, we use GridLayout. There are three lines and two columns in our user interface. We add the action event listener later. The UI method createComponents looks like the following.

    private void createComponents(Container container) {
        GridLayout layout = new GridLayout(3, 2);
        container.setLayout(layout);

        JLabel textName = new JLabel("Name: ");
        JTextField nameField = new JTextField();
        JLabel textID = new JLabel("ID: ");
        JTextField idField = new JTextField();

        JButton addButton = new JButton("Add!");
        // event listener

        container.add(textName);
        container.add(nameField);
        container.add(textID);
        container.add(idField);
        container.add(new JLabel(""));
        container.add(addButton);
    }

After adding the information, our user interface looks like the following.

The action event listener has to know about the recording functionality (PersonRecord interface), as well as the fields it uses. Let's create the class PersonRecordListener which implements ActionListener. As constructor parameter, the class is assigned an object which implements the interface PersonRecord, as well as two JTextField objects which stand for the name and ID fields. In the method actionPerformed we create a new Person object and we record it using the record method of our PersonRecord object.

public class PersonRecordListener implements ActionListener {

    private PersonRecord personRecord;
    private JTextField nameField;
    private JTextField idField;

    public PersonRecordListener(PersonRecord personRecord, JTextField nameField, JTextField idField) {
        this.personRecord = personRecord;
        this.nameField = nameField;
        this.idField = idField;
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        Person person = new Person(nameField.getText(), idField.getText());
        this.personRecord.record(person);
    }
}

In order to retrieve a PersonRecord reference to PersonRecordListener, the user interface must have access to it. Let's add to our user interface the object variable private PersonRecord personRecord which is set up in the constructor. We also modify the constructor of the class UserInterface, which is assigned a class which implements the interface PersonRecord.

public class UserInteface implements Runnable {

    private JFrame frame;
    private PersonRecord personRecord;

    public UserInteface(PersonRecord personRecord) {
        this.personRecord = personRecord;
    }
    // ...


Now we can create the action event listener PersonRecordListener, which is given both a PersonRecord reference and the fields.

    private void createComponents(Container container) {
        GridLayout layout = new GridLayout(3, 2);
        container.setLayout(layout);

        JLabel nameText = new JLabel("Name: ");
        JTextField nameField = new JTextField();
        JLabel idText = new JLabel("ID: ");
        JTextField idField = new JTextField();

        JButton addButton = new JButton("Add!");
        PersonRecordListener listener = new PersonRecordListener(personRecord, nameField, idField);
        addButton.addActionListener(listener);

        container.add(nameText);
        container.add(nameField);
        container.add(idText);
        container.add(idField);
        container.add(new JLabel(""));
        container.add(addButton);
    }

Axe Click Effect

In this exercise we implement a calculator to count the number of clicks. In the exercise, the application logic (counting) and the ui logic are divided from each other. The final application should look like the following, so far.

PersonalCalculator

Implement the class PersonalCalculator in the package clicker.applicationlogic; the class implements the interface Calculator. At first, the method giveValue of PersonalCalculator returns 0. Whenever the method increase is called, the value increases by one.

If you want, you can test the class using the following program.

        Calculator calc = new PersonalCalculator();
        System.out.println("Value: " + calc.giveValue());
        calc.increase();
        System.out.println("Value: " + calc.giveValue());
        calc.increase();
        System.out.println("Value: " + calc.giveValue());
Value: 0
Value: 1
Value: 2

ClickListener

Implement the class ClickListener in the package clicker.ui; the class implements the interface ActionListener. ClickListener receives two objects as constructor parameters: an object which implements the interface Calculator and a JLabel object.

Implement the actionPerformed method so that the Calculator object increases by one at first, and after it, the calculator value is set as text of the JLabel object. The text of the JLabel object can be modified with the method setText.

User Interface

Modify the class UserInterface; now the user interface has to receive a Calculator object as constructor parameter: you need a new constructor. Add the necessary ui components to your UserInterface. Also, set the action event listener you implemented in the previous section to the button.

Use the functionality provided by the class BorderLayout to manage the layout of the ui components. Also, change the Main class so that the user interface is assigned a PersonalCalculator object. When the "Click!" button in the user interface has been pressed twice, the application looks like below, more or less.

Nested Container Objects

Sometimes we end up in a situation, where the Container object provided by JFrame is not suitable enough for our UI layout. We may want our user interface to look different or to group up UI components according to their use. For instance, building a user interface like the one below would not be so easy, using only the Container object provided by the class JFrame.

We can place Container objects inside each other. The class JPanel (see also How to Use Panels) allows for nested Container objects. It is possible to add UI components to a JPanel instance in the same way we add components to the Container instance of JFrame class. Moreover, it is possible to add an instance of JPanel to a Container object. This makes possible to use many Container objects to develop one user interface.

Creating a user interface like the one above is easier with JPanel. Let's create a user interface with three buttons -- Execute, Test, and Send -- plus a text field. The buttons are a group on its own, and we assign them to a JPanel object which is placed in the lower part of the Container object which we have got from JFrame class.

    private void createComponents(Container container) {
        container.add(new JTextArea());
        container.add(createPanel(), BorderLayout.SOUTH);
    }

    private JPanel createPanel() {
        JPanel panel = new JPanel(new GridLayout(1, 3));
        panel.add(new JButton("Execute"));
        panel.add(new JButton("Test"));
        panel.add(new JButton("Send"));
        return panel;
    }

The JPanel class is given as constructor parameter the layout style to use. If in its constructor the layout style requires a reference to the Container object used, the JPanel class also has the method setLayout.

If our user interface has clear, separate, groups of components we can also inherit the JPanel class. For instance, the panel above could be implemented in the following way, too.

import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JPanel;

public class MenuPanel extends JPanel {

    public MenuPanel() {
        super(new GridLayout(1, 3));
        createComponents();
    }

    private void createComponents() {
        add(new JButton("Execute"));
        add(new JButton("Test"));
        add(new JButton("Send"));
    }
}

Now we can create a MenuPanel instance in our user interface class.

    private void createComponents(Container container) {
        container.add(new JTextArea());
        container.add(new MenuPanel(), BorderLayout.SOUTH);
    }

Note that in case you need an action event listener, the class MenuPanel must be given all the objects its need as parameter.

Calculator

The goal of the exercise is creating a simple calculator. The calculator user interface has to look like the following:

Together with the exercise body, you find the main program which starts the calculator, as well as the class GraphicCalculator which contains a graphic user interface. The user interface has to follow exactly the following points, but you can plan the program structure as you wish. Note: GraphicCalculator provides empty methods run, createComponents and getFrame.

The Layout

You find a JFrame with the exercise body; you manage its layout using GridLayout with three lines and one column. JTextField has to be placed in the upper block and has to be used for the text output; it must be set off with the method call setEnabled(false). The second block has to contain JTextField for the text input. Originally, the input field contains the text "0", and the input field is empty.

The lowest block has to contain JPanel, and this has to have the layout manager GridLayout, with one line and three columns. The panel has three JButtons, with texts "+", "-" and "Z".

Basic Functionality

The calculator basic functionality is the following. When the user writes a number n into the input field and presses +, the value of the output field is added n and the output field is updated with a new value. Accordingly, when the user writes a number n into the input field and presses -, the value of the output field is decreased by n, and the output field is updated with the new value. If the user presses Z, the output field value is reset to zero.

Cozy Management

Let's extend our program with the following features:

  • If the output field is 0, the user can't press the Z button, i.e. the button has to be set off with the method call setEnabled(false). Otherwise, the button has to be on.
  • When the user presses any of the buttons +, -, Z the input field is wiped.
  • If the input value is not an integer and the user presses one of the buttons +, -, Z, the input field is wiped and the value of the output field does not change (unless the button is Z).

Drawing

Its Container functionality is not the only reason why we use the class JPanel: it is also used as drawing board, and the user inherits the JPanel class and overrides the method protected void paintComponent(Graphics graphics). The user interface calls the method paintComponent whenever we want to draw again the UI component contents. The parameter of the method paintComponent receives from the user interface an object which implements the abstract class Graphics. Let's create the class DrawingBoard JPanel which inherits from JPanel and which overrides the paintComponent method.

public class DrawingBoard extends JPanel {

    public DrawingBoard() {
        super.setBackground(Color.WHITE);
    }

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
    }
}

The drawing board above does not contain concrete drawing functionality. In the constructor, we can define the colour of our drawing board to be white by calling its superclass' method setBackground. The method setBackGround receives an instance of the class Color as parameter. The class Color contains the most common colours as class variables; for instance, you get the white colour using the class variable Color.WHITE.

The overridden paintComponent method calls the superclass' paintComponent method, and it does not do anything else. Let's add the drawing board to the createComponents method of class UserInterface. We use the user interface which was defined at the beginning of the section 58. User Interfaces.

    private void createComponents(Container container) {
        container.add(new DrawingBoard());
    }

When we start our user interface we see an empty screen, whose background colour is white. The size of the user interface below is set to 300x300 through the method setPreferredSize, and its title is "Drawing Board".

Drawing on the board is possible using the methods provides by the Graphics object. Let's modify the method paintComponent of DrawingBoard and let's draw two rectangles using the method fillRect of the Graphics object.

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);

        graphics.fillRect(50, 80, 100, 50);
        graphics.fillRect(200, 20, 50, 200);
    }

The method fillRect receives as parameter the x and y coordinates of a rectangle, plus the rectangle width and height. In fact, above we first draw a rectangle which starts with pixel whose coordinates are (50, 80), which is 100 pixels long, and 50 pixels high. Afterwards, we draw a 50-pixel long, 100-pixel high rectangle which begins at (200, 20).

As you notice from the picture, the coordinate system does not work as we are accustomed to.

Java's Graphics object (and most of other programming language libraries) expects the value of the y axis to grow downwards. The coordinate system origin, i.e. the point (0,0) is in the upper left corner: the Graphics object always knows the UI component where we draw, and it is able to define the location of the point to draw based on it. The location of the UI origin can become clear with the help of the following program. First we draw a green 10-pixel long, 200-pixel high rectangle which starts from the point (0,0). Then we draw a black 200-pixel long, 10-pixel high rectangle which starts from the point (0,0). The drawing colour is defined by the method setColor of our Graphics object.

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);

        graphics.setColor(Color.GREEN);
        graphics.fillRect(0, 0, 10, 200);
        graphics.setColor(Color.BLACK);
        graphics.fillRect(0, 0, 200, 10);
    }

Such coordinate system reversibility depends on the way user interface size is modified. When we modify the size of a user interface, this is reduced or increased by "dragging the bottom right corner"; in this way, the drawing on the screan would move while we change the UI size. Because the grid starts from the upper left corner, the drawing position is always the same, but the visible part changes.

Drawing Board

With the exercise body, you find a pre-made user interface, which is connected to the class DrawingBoard, which inherits JPanel. Modify the method paintCOmponent of DrawingBoard so that it would draw the following figure. You can only use the fillRect method of the graphics object, in this exercise.

Attention! Do not use more than five fillRect calls. The figure does not have to be identical to the one above, the tests tell you when your figure is close enough to the required one.

Let's extend our previous example and draw an independent avatar-object in our user interface. Let's create the class Avatar; it has the coordinates of the point where it appears, and it is a circle with a 10-pixel diameter. The location of the avatar can be changed by calling its move method.

import java.awt.Graphics;

public class Avatar {

    private int x;
    private int y;

    public Avatar(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public void move(int movingX, int movingY) {
        this.x += movingX;
        this.y += movingY;
    }

    public void draw(Graphics graphics) {
        graphics.fillOval(x, y, 10, 10);
    }
}

Let's modify our drawing board, giving it an instance of our Avatar as constructor parameter. The method paintComponent of DrawingBoard does not draw the character itself, but it delegates the responsibility to the instance of the class Avatar.

import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;

public class DrawingBoard extends JPanel {

    private Avatar avatar;

    public DrawingBoard(Avatar avatar) {
        super.setBackground(Color.WHITE);
        this.avatar = avatar;
    }

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
        avatar.draw(graphics);
    }
}

Let's also give our avatar as parameter to our user interface. Avatar is an independent object in the user interface, and we only want to draw it in the user interface. It is essential to change our UI constructor so that it received a Avatar object. Moreover, in the method createComponents we give an instance of the class Avatar as parameter to our DrawingBoard object.

public class UserInterface implements Runnable {

    private JFrame frame;
    private Avatar avatar;

    public UserInterface(Avatar avatar) {
        this.avatar = avatar;
    }

// ...

    private void createComponents(Container container) {
        DrawingBoard drawingBoard = new DrawingBoard(avatar);
        container.add(drawingBoard);
    }
// ...

Now, our user interface can be started giving it an Avatar object as constructor parameter.

        UserInterface ui = new UserInterface(new Avatar(30, 30));
        SwingUtilities.invokeLater(ui);

In the user interface above, we see a ball-like Avatar.

Let's now add the functionality to move the avatar. We want to move it using our keyboard. When the user presses the left arrow, the avatar should move left. Pressing the right arrow should move the avatar right. We need an action event listener, which would listen to our keyboard. The interface KeyListener defines the functionality needed to listener to a keyboard.

The interface KeyListener calls for implementing the methods keyPressed, keyReleased, and keyTyped. We are only interested to the case in which the keyboard is pressed, so we can leave empty the methods keyReleased and keyTyped. Let's create the class KeyboardListener, which implements the interface KeyListener. The class receives as parameter a Avatar object, and the action event manager has to shift it.

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class KeyboardListener implements KeyListener {

    private Avatar avatar;

    public KeyboardListener(Avatar avatar) {
        this.avatar = avatar;
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            avatar.move(-5, 0);
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            avatar.move(5, 0);
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void keyTyped(KeyEvent ke) {
    }
}

The method keyPressed receives as parameter an instance of KeyEvent from the user interface. The KeyEvent object knows the number related to the pressed key thanks to its method getKeycode(). Different keys have got different class variables in the KeyEvent class; for instance, the left arrow is KeyEvent.VK_LEFT.

We want to listen to the keystrokes directed to our user interface (we don't want to write to the text field, for instance), and therefore we assign our keyboard listener to the JFrame instance. Let's modify our user interface and add the keyboard listener to the JFrame object.

    private void createComponents(Container container) {
        DrawingBoard drawingBoard = new DrawingBoard(avatar);
        container.add(drawingBoard);

        frame.addKeyListener(new KeyboardListener(avatar));
    }

Our application now listens to keystrokes, and it leads them to the instance of the class KeyboardListener.

However, when we try out our user interface it does not work: the avatar does not move on the screen. What is the problem, in fact? We can check the keystrokes which are received by our KeyboardListener by adding a text printout to the beginning of our keyPressed method.

    @Override
    public void keyPressed(KeyEvent e) {
        System.out.println("Keystroke " + e.getKeyCode() +  " pressed.");

        // ...

If we start our program and press some keys we will notice the following output.

Keystroke 39 pressed.
Keystroke 37 pressed.
Keystroke 40 pressed.
Keystroke 38 pressed.

In fact, our keyboard listener works, but our drawing board does not update.

Drawing Board Repainting

User interface components usually have the functionality to repaint the component outer face, when needed. For instance, when we press the button, the instance of the class JButton is able to paint the button as if it was pressed, and to paint it normal again afterwards. The drawing board we have implemented does not have a pre-made update functionality; instead, we have to ask our drawing board to paint itself again when needed.

Each subclass of Component has the method public void repaint(), which repaints the component after it is called. We want that our DrawingBoard object would get repainted while the avatar moves. The avatar moves in the class KeyboardListener, and it is logic the repainting would happen there, too.

In order to be repainted, our keyboard listener needs a drawing board reference. Let's modify our class KeyboardListener, so that it would receive as parameter both an Avatar object and the Component object to repaint. We call the repaint method of the Component object after each keyPressed action event.

import java.awt.Component;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class KeyboardListener implements KeyListener {

    private Component component;
    private Avatar avatar;

    public KeyboardListener(Avatar avatar, Component component) {
        this.avatar = avatar;
        this.component = component;
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            avatar.move(-5, 0);
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            avatar.move(5, 0);
        }

        component.repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void keyTyped(KeyEvent ke) {
    }
}

Let's also modify the createComponents method of UserInterface and give an instance of DrawingBoard as parameter to our keyboard listener.

    private void createComponents(Container container) {
        DrawingBoard drawingBoard = new DrawingBoard(hahmo);
        container.add(drawingBoard);

        frame.addKeyListener(new KeyboardListener(avatar, drawingBoard));
    }

Now, our avatar's moves are visible also in the user interface. Whenever the user presses the keyboard, the user interface keyboard listener handles the call. At the end of each call, the repaint method of our drawing board is called, and the drawing board gets repainted.

A Moving Figure

We create a program which allows the user to move the figures drawn on a board using their keyboard. Together with the program you find the body of a user interface, which you can modify as your program proceeds.

At first, we create a couple of classes to manage the figures. Later on we will be able to draw the figures to our board. Create all the classes of the program in the package movingfigure.

In this exercise we make use of both inheritance and abstract classes: go back to sections 18.1, 18.2, and 18.5, if you need.

Figure: an Abstract Class

Create the abstract class Figure. Figures have the object variables x and y, which tell the figure position on the board; they also have the method public void move(int dx, int dy), which moves the figures according to the parameter coordinate movements. For instance, if the position is (100,100), at the beginning, after calling the method move(10,-50) the position will be (110,50). The class constructor public Figure(int x, int y) has to define the original position of the figure. Additionally, implement also the methods public int getX() and public int getY().

The class also has to have the abstract method public abstract void draw(Graphics graphics), which draws the figure on the drawing board. The figure drawing method is implemented in the classes which inherit Figure.

Circle

Create the class Circle which inherits Figure. Circle has a diameter, whose value is defined by the constructor public Circle(int x, int y, int diameter). The Circle position is stored into the object variables defined in its parent class.

The circle defines the method draw so that it would draw a circle of the right size, in the place defined by the coordinates, and using the method fillOval of the Graphics object; the first two parameters of the method are taken for the position of the circle. Take example from the relative method in the Avatar example. For more information about the methods of Graphics objects, you can have a look at Java's API.

Drawing Board

Create the class DrawingBoard which inherits JPanel; you can take example from the drawing board in the previous exercise, for instance. DrawingBoard receives a Figure object as parameter. Override JPanel's method protected void paintComponent(Graphics g) so that it first calls the superclass' paintComponent method, and then the draw method of the figure which was assigned to the drawing board.

Modify the class UserInterface so that it would receive a Figure object as constructor parameter. Assign your DrawingBoard to the user interface together with the createComponents(Container container) method, and assign to the drawing board the figure which was given to the user interface as constructor parameter.

Finally, test that the following sample code draws a circle on the screen.

        UserInterface ui = new UserInterface(new Circle(50, 50, 250));
        SwingUtilities.invokeLater(ui);

Keyboard Listener

Let's extend our drawing board, so that we could move the figures using our keyboard arrows. Create the class KeyboardListener which implements the interface KeyListener. The class KeyboardListener has two constructor parameters: an instance of the class Component and one of the class Figure.

The instance of Component is given to the keyboard listener so that the component would be updated each time the keyboard is pressed. The component is updated with the method call repaint, which is inherited from he class Component. The type of the class DrawingBoard is Component, because Component is the upper class of the class which inherits JPanel.

Implement the method keyPressed(KeyEvent e) of the interface KeyListener; when the user presses the left arrow, the figure moves one point left. Pressing right, the figure moves one point right. Pressing up it moves one point up, and pressing down the figure moves one point down. Note that the y axe grows from the upper side of the window downwards. The arrow key codes are KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, and KeyEvent.VK_DOWN. Leave empty the other methods required by the interface KeyListener.

Always call class Component's method repaint in the end of keylistener event.

Add keylistener in the UserInterface's method addListeners. KeyListener must be connected to JFrame-object.

Square and Box

Let Square and Box inherit the class Figure. Square has the constructor public Square(int x, int y, int sideLength); the constructor of box is public Box(int x, int y, int width, int height). Use the method fillRect of a graphic object to draw the figures.

Make sure that squares and boxes are drawn and move right on the DrawingBoard.

        UserInterface ui = new UserInterface(new Square(50, 50, 250));
        SwingUtilities.invokeLater(ui);

        UserInterface ui = new UserInterface(new Box(50, 50, 100, 300));
        SwingUtilities.invokeLater(ui);

Compound Figure

Let CompoundFigure inherit Figure. Compound figure contains other figures which are stored in an ArrayList. CompoundFigure has the method public void add(Figure f), which adds a new Figure object to the compound figure. Compound figures do not have their own position, and it is not important what values are assigned to x and y coordinates. Compound figures draw themselves by asking their parts to draw themselves; the same thing happens when a compound figure moves. This means that the inherited method move has to be overwritten!

You can test whether your compound figure moves and is drawn well using the following code:

        CompoundFigure truck = new CompoundFigure();

        truck.add(new Box(220, 110, 75, 100));
        truck.add(new Box(80, 120, 200, 100));
        truck.add(new Circle(100, 200, 50));
        truck.add(new Circle(220, 200, 50));

        UserInterface ui = new UserInterface(truck);
        SwingUtilities.invokeLater(ui);

Note how the object responsibilities are shared here. Each Figure is in charge of drawing and moving itself. Simple figures all move in the same way. Each simple figure has to manage their own drawing themselves. Compound figures move by asking their parts to move, and the same thing happens when it comes to be drawn. The drawing board knows a Figure object which, in fact, can be whatever simple figure or a compound figure: they are all drawn and moved in the same way. In fact, the drawing board works correctly regardless of the real type of the figure; the drawing board does not have to know the details of the figure. When the drawing board calls the method draw or move of the figure, the method of the real type of the figure is called, thanks to polymorphism.

It's worth to notice that CompoundFigure can contain whatever Figure object, even another CompoundFigure! The class structure allows for highly complex figure formations, whereas figures move and draw themselves always in the same way.

The class structure can also be expanded easily; for instance, a compound figure would work without needing changes even if we created new types which inherit Figure (say Triangle, Point, Line, exc.), and the same thing would apply to the drawing board and user interface.

Pre-made Application Frameworks

An application framework is a program which provides a baseline and a set of features to implement a particular application. One way to create an application framework is to create a class which provides pre-made features, so that classes can inherit it and build a particular application. Application frameworks are usually wide, and they are thought for a special purpose, for instance to program games or develop web-applications. Let's quickly get acquainted with a pre-made application library, by greating the application logic of a Game of Life.

Game of Life

In this exercise, we implement the application logic of a Game of Life, inheriting a pre-made application body.The application body is in a library which has been added to the project singularly, and its source codes are not visible.

ATTENTION: your task won't be extremely difficult, but the exercise descriptions may look a bit confusing, at first. Read the instruction carefully, or ask for help if you can't get started. The exercise is definitely worth of your energies, because the result is beautiful!

Game of Life is a simple "population simulator" which was developed by the mathematician John Conway; see http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life.

Game of Life rules:

  • Every living cell dies if they have less than two living neighbours.
  • Every living cell keeps on living during the following iteration (i.e. turn) if they have two or three living neighbours.
  • Every living cell dies if they have more than three living neighbours.
  • Every dead cell is turned back to life if they have exactly three living neighbours.

The abstract class GameOfLifeBoard provides the following functionality

  • public GameOfLifeBoard(int length, int height) creates a game board of the defined dimensions
  • public boolean[][] getBoard() provides access to the game board, which is a bidimensional table containing boolean values – as you may guess from the method return value! We come back to bidimensional boards later on when needed.
  • public int getWidth() returns the board width
  • public int getHeight() returns the board height
  • public void playTurn() simulates a turn of the game

The class GameOfLifeBoard has also got the following abstract method, which you will have to implement.

  • public abstract void turnToLiving(int x, int y) animates the cell whose coordinates are (x, y), that is to say it assigns the value true to it. If the coordinates are outside the board, nothing happens.
  • public abstract void turnToDead(int x, int y) kills the cell whose coordinates are (x, y), that is to say it assigns the value false to it. If the coordinates are outside the board, nothing happens.
  • public abstract boolean isAlive(int x, int y) tells whether the cell at (x, y) is alive. If the coordinates are outside the board, the method returns false.
  • public abstract void initiateRandomCells(double probabilityForEachCell) initiates all the cells of the board: every cell is alive with a probability of probabilityForEachCell. The probability is a double value between [0, 1]. If a method is called with value 1, all the cells have to be alive. Accordingly, if the probability is 0, all the cells have to be dead.
  • public abstract int getNumberOfLivingNeighbours(int x, int y) tells the number of living neighbours for the cell at (x, y).
  • public abstract void manageCell(int x, int y, int livingNeighbours) managese the cell (x, y) according to the rules of the Game of Life.

GameOfLife implementation, Part 1

Create the class PersonalBoard into the package game; PersonalBoard inherits the class GameOfLifeBoard which is in the package gameoflife. Note that the package gameoflife is not visible in your project, but it comes together with the class library. In the class PersonalBoard, implement the constructor public PersonalBoard(int width, int height), which calls the superclass constructor with the given parameters:

import gameoflife.GameOfLifeBoard;

public class PersonalBoard extends GameOfLifeBoard {

    public PersonalBoard(int width, int height) {
        super(width, height);
    }

    // ..

You can first replace all the abstract methods with non-abstract ones, which do not do anything particular anyway, so far. However, because the methods are not abstract, this class can create instances, differently than the abstract class GameOfLifeBoard.

Implement the following methods

  • public abstract void turnToLiving(int x, int y) animates the cell whose coordinates are (x, y), that is to say it assigns the value true to it. If the coordinates are outside the board, nothing happens.
  • public abstract void turnToDead(int x, int y) kills the cell whose coordinates are (x, y), that is to say it assigns the value false to it. If the coordinates are outside the board, nothing happens.
  • public abstract boolean isAlive(int x, int y) tells whether the cell at (x, y) is alive. If the coordinates are outside the board, the method returns false.

Hint: You have access to the bidimensional table of the superclass through the superclass method getBoard(). Bidimensional tables are used as normal tables, but they are assigned two indexes. The first index tells the row and the second tells the column. For instance, the following program chunk creates a 10 x 10 table, and prints the value at (3,1).

boolean[][] values = new boolean[10][10];
System.out.println(values[3][1]);

Accordingly, we can print the value at (x,y) of our PersonalBoard's superclass, in the following way:

boolean[][] board = getBoard();
System.out.println(board[x][y]);

And an index (x,y) can be assigned a value in the following way:

boolean[][] board = getBoard();
board[x][y] = true;

Or straight, using a helping variable:

getBoard()[x][y] = true;

Test your implementation with the following program.

package game;

public class Main {
    public static void main(String[] args) {
        PersonalBoard board = new PersonalBoard(7, 5);

        board.turnToLiving(2, 0);
        board.turnToLiving(4, 0);

        board.turnToLiving(3, 3);
        board.turnToLiving(3, 3);

        board.turnToLiving(0, 2);
        board.turnToLiving(1, 3);
        board.turnToLiving(2, 3);
        board.turnToLiving(3, 3);
        board.turnToLiving(4, 3);
        board.turnToLiving(5, 3);
        board.turnToLiving(6, 2);

        GameOfLifeTester tester = new GameOfLifeTester(board);
        tester.play();
    }
}

The output should look like the following:

Press enter to continue, otherwise quit: <enter>

  X X

X     X
 XXXXX

Press enter to continue, otherwise quit: stop
Thanks!

GameOfLife implementation, Part 2

Implement the method public abstract void initiateRandomCells(double probabilityForEachCell) initiates all the cells of the board: every cell is alive with a probability of probabilityForEachCell. The probability is a double value between [0, 1].

Test the method. Given the value 0.0, there should be no cell alive; given the value 1.0, all the cells should be alive (i.e. visible in the form of X characters). With the value 0.5, around fifty precent of the cells should be alive.

        PersonalBoard board = new PersonalBoard(3, 3);
        board.initiateRandomCells(1.0);

        GameOfLifeTester tester = new GameOfLifeTester(board);
        tester.play();
Press enter to continue, otherwise quit: <enter>

XXX
XXX
XXX
Press enter to continue, otherwise quit: stop
Thanks!

GameOfLife Implementation, part 3

Implement the method getNumberOfLivingNeighbours(int x, int y), which calculates the number of neighbour cells which are alive. Central cells have eight neighbours, the ones on the side have five, and the ones in the corner have only three.

Test the method with the following sentences (of course, you can create your own test instances!):

PersonalBoard board = new PersonalBoard(7, 5);

board.turnToLiving(0, 1);
board.turnToLiving(1, 0);
board.turnToLiving(1, 2);
board.turnToLiving(2, 2);
board.turnToLiving(2, 1);

System.out.println("Neighbours alive (0,0): " + board.getNumberOfLivingNeighbours(0, 0));
System.out.println("Neighbours alive (1,1): " + board.getNumberOfLivingNeighbours(1, 1));

The print output should look like the following:

Neighbours alive (0,0): 2
Neighbours alive (1,1): 5

GameOfLife Implementation, Part 4

Only one method is missing: manageCell(int x, int y, int livingNeighbours). Game of Life rules were the following:

  • Every living cell dies if they have less than two living neighbours.
  • Every living cell keeps on living during the following iteration (i.e. turn) if they have two or three living neighbours.
  • Every living cell dies if they have more than three living neighbours.
  • Every dead cell is turned back to life if they have exactly three living neighbours.

Implement the method manageCell(int x, int y, int livingNeighbours) according to the following rules. It's good to program and test one rule at one time!

When you are done with all the rule, you can test the program with the following graphic simulator.

package game;

import gameoflife.Simulator;

public class Main {

    public static void main(String[] args) {
        PersonalBoard board = new PersonalBoard(100, 100);
        board.initiateRandomCells(0.7);

        Simulator simulator = new Simulator(board);
        simulator.simulate();
    }
}
Week6

Some Useful Techniques

Before the course comes to its end, we can still have a look at some useful particular features of Java.

Regular Expressions

A regular expression is a compact form to define a string. Regular expressions are often used to check the validity of strings. Let's have a look at an exercise where we have to check whether the student number given by the user is written in the valid form or not. Finnish student numbers start with the string "01" which is followed by seven numerical digits from 0 to 9.

We can check the validity of a student number parsing its each character with the help of the method charAt. Another way would be checking whether the first character is "0", and using the method Integer.parseInt to translate the string into a number. Then, we could check whether that number is smaller than 20000000.

Validity check with the help of regular expressions requires we define a suitable regular expression. Then we can use the matches method of the class String, which checks whether the string matches with the regular expression in parameter. In the case of a student number, a suitable regular expression is "01[0-9]{7}", and you can check the validity of what the user has input in the following way:

System.out.print("Give student number: ");
String num = reader.nextLine();

if (num.matches("01[0-9]{7}")) {
    System.out.println("The form is valid.");
} else {
    System.out.println("The form is not valid.");
}

Next, we can go through the most commonly used regular expressions.

Vertical Bar: Logical or

The vertical bar means that the parts of the regular expression are optional. For instance, the expression 00|111|0000 defines the strings 00, 111 and 0000. The method matches returns true if the string matches one of the alternatives defined.

    String string = "00";

    if(string.matches("00|111|0000")) {
        System.out.println("The string was matched by some of the alternatives");
    } else {
        System.out.println("The string not was matched by any of the alternatives");
    }
The string was matched by some of the alternatives

The regular expression 00|111|0000 requires the exactly same form of the string: its functionality is not like "contains".

    String string = "1111";

    if(string.matches("00|111|0000")) {
        System.out.println("The string was matched by some of the alternatives");
    } else {
        System.out.println("The string not was matched by any of the alternatives");
    }
The string not was matched by any of the alternatives

Round Brackets: a Delimited Part of the String

With the help of round brackets it is possible to define what part of the regular expression is affected by the symbols. If we want to allow for the alternatives 00000 and 00001, we can define it with the help of a vertical bar: 00000|00001. Thanks to round brakers we can delimit the choice to only a part of the string. The expression 0000(0|1) defines the strings 00000 and 00001.

Accordingly, the regular expression look(|s|ed) defines the basic form of the verb to look (look), the third person (looks), and the past (looked).

System.out.print("Write a form of the verb to look: ");
String word = reader.nextLine();

if (word.matches("look(|s|ed|ing|er)")) {
    System.out.println("Well done!");
} else {
    System.out.println("Check again the form.");
}

Repetitions

We often want to know whether a substring repeats within another string. In regular expressions, we can use repetition symbols:

You can also use various different repetition symbols within one regular expression. For instance, the regular expression 5{3}(1|0)*5{3} defines strings which start and end with three fives. In between there can be an indefinite number of 1 and 0.

Square Brackets: Character Groups

With the help of square brackets we can quickly define groups of characters. The characters are written inside the brackets, and we can define an interval with the help of a hyphen (-). For instance, [145] means the same as (1|4|5), whereas [2-36-9] means the same as (2|3|6|7|8|9). Accordingly, [a-c]* defines a regular expression with a string made only of characters a, b and c.

Regular Expressions

Let's train to use regular expressions. The exercises are done in the Main class of the default package .

Week Days

Create the method public static boolean isAWeekDay(String string) in the class Main, using regular expressions. The method returns true if its parameter string is the abbreviation of a week day (mon, tue, wed, thu, fri, sat or sun).

The following is a sample print output of the method:

Give a string: tue
The form is fine.
Give a string: abc
The form is wrong.

Vowel Inspection

Create the method public static boolean allVowels(String string) in the class Main, which makes use of regular expressions and checks whether the String argument contains only vowel characters.

The following is a sample print output of the method:

Give a string: aie
The form is fine.
Give a string: ane
The form is wrong.

Clock Time

Regual expressions suit in particular situations. In some cases, the expressions become too complicate and it may be useful to check the "regularity" of a string in a different way, or it may be appropriate to use regular expressions to manage only a part of the inspection.

Create the method public static boolean clockTime(String string) in the class Main, which makes use of regular expressions and checks whether the String argument conforms with the clock time hh:mm:ss (two-digit hours, minutes, and seconds). In this exercise you can use whatever tecnique, in addition to regular expressions.

The following is a sample print output of the method:

Give a string: 17:23:05
The form is fine.
Give a string: abc
The form is wrong.
Give a string: 33:33:33
The form is wrong.

Enum: Enumerated Type

Previously, we implemented the class Card which represented a playing card:

public class Card {

    public static final int DIAMONDS = 0;
    public static final int SPADES = 1;
    public static final int CLUBS = 2;
    public static final int HEARTS = 3;

    private int value;
    private int suit;

    public Card(int value, int suit) {
        this.value = value;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return suitName() + " "+value;
    }

    private String suitName() {
        if (suit == 0) {
            return "DIAMONDS";
        } else if (suit == 1) {
            return  "SPADES";
        } else if (suit == 2) {
            return "CLUBS";
        }
        return "HEARTS";
    }

    public int getSuit() {
        return suit;
    }
}

The card suit is stored as object variable integer. Indicating the suit is made easier by constants which help the legibility. The constants which represent cards and suits are used in the following way:

public static void main(String[] args) {
        Card card = new Card(10, Card.HEARTS);

        System.out.println(card);

        if (card.getSuit() == Card.CLUBS) {
            System.out.println("It's clubs");
        } else {
            System.out.println("It's not clubs");
        }

}

Representing the suit as a number is a bad solution, because the following absurd ways to use cards are possible:

        Card absurdCard = new Card(10, 55);

        System.out.println(absurdCard);

        if (absurdCard.getSuit() == 34) {
            System.out.println("The card's suit is 34");
        } else {
            System.out.println("The card's suit is not 34");
        }

        int suitPower2 = absurdCard.getSuit() * absurdCard.getSuit();

        System.out.println("The card's suit raised to the power of two is " + suitPower2);

If we already know the possible values of our variables, we can use a enum class to represent them: an enumerated type. In addition to being classes and interfaces, enumerated types are also a class type of their own. Enumerated types are defined with the keyword enum. For instance the following Suit enum class defines four values: DIAMONDS, SPADES, CLUBS and HEARTS.

public enum Suit {
    DIAMONDS, SPADES, CLUBS, HEARTS
}

In its most basic from, enum lists its constant values divided by a comma. Enum constants are usually written in capital letters.

Enums are usually created in their own file, in the same way as classes and interfaces. In Netbeans, you can create a enum by clicking to new/other/java/java enum on your project name.

The following Card class is represented with the help of enum:

public class Card {

    private int value;
    private Suit suit;

    public Card(int value, Suit suit) {
        this.value = value;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return suit + " "+value;
    }

    public Suit getSuit() {
        return suit;
    }

    public int getValue() {
        return value;
    }
}

The new version of the card is used in the following way:

public class Main {

    public static void main(String[] args) {
        Card first = new Card(10, Suit.HEARTS);

        System.out.println(first);

        if (first.getSuit() == Suit.CLUBS) {
            System.out.println("It's clubs");
        } else {
            System.out.println("It's not clubs");
        }

    }
}

Prints:

HEARTS 10
It's not clubs

We notice that enum names are printed smoothly! Because card suits' type is Suit, absurd practices like the one above -- raising a suit to the power of two -- do not work. Oracle has a tutorial for enum type at http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html.

Iterator

Let's have a look at the following class Hand which represents the cards a player has in his hand in a card game:

public class Hand {
    private ArrayList<Card> cards;

    public Hand() {
        cards = new ArrayList<Hand>();
    }

    public void add(Card card){
        cards.add(card);
    }

    public void print(){
        for (Card card : cards) {
            System.out.println( card );
        }
    }
}

The print method prints each card in the hand by using a "for each" statement. ArrayList and other "object containers" which implement the Collection interface indirectly implement the interface Iterable. Objects which implement Iterable can be parses, or better "iterated", with statements such as for each.

Object containers can also be iterated using a so called iterator, that is an object, which was thought to parse a particular object collection. Below, there is a version of an iterator used to print cards:

public void print() {
    Iterator<Card> iterator = cards.iterator();

    while ( iterator.hasNext() ){
        System.out.println( iterator.next() );
    }
}

The iterator is taken from the ArrayList cards. The iterator is like a finger, which always points out a specific object of the list, from the first to the second, to the third, and so on, till the finger has gone through each object.

The iterator provides a couple of methods. The method hasNext() asks whether there are still objects to be iterated. If there are, we can retrieve the following object using the method next(). The method returns the following object in the collection, and makes the iterator -- the "finger" -- point out the following object.

The object reference returned by the Iterator's next() method can be stored into a variable, of course; in fact, we could modify the method print in the following way:

public void print(){
    Iterator<Card> iterator = cards.iterator();

    while ( iterator.hasNext() ){
        Card nextCard = iterator.next();
        System.out.println( nextCard );
    }
}

We can create a method to delete the cards which are smaller than a specific value:

public class Hand {
    // ...

    public void deleteWorst(int value) {
        for (Card card : cards) {
            if ( card.getValue() < value ) {
                cards.remove(card);
            }
        }
    }
}

We notice that running the method causes a strange error:

Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
        at java.util.AbstractList$Itr.next(AbstractList.java:343)
        at Hand.deleteWorst(Hand.java:26)
        at Main.main(Main.java:20)
Java Result: 1

The reason is that you can't delete objects from a list while you are parsing it: the for each statement "gets all worked up".

If we want to delete a part of the objects while we parse our list, we have to use an iterator. If we call the remove method of our iterator object, we can neatly delete the value which was returned by the iterator with its previous next method call. The following method works fine:

public class Hand {
    // ...

    public void deleteWorst(int value) {
        Iterator<Card> iterator = cards.iterator();

        while (iterator.hasNext()) {
            if (iterator.next().getValue() < value) {
                // delete the object returned by the iterator with its previous method call
                iterator.remove();
            }
        }
    }
}

Enum and Iterator

Let's create a program to manage the staff personnel of a small business.

Education

Create the enumerated type, or enum, Education in the package personnel. The enum has the titles D (doctor), M (master), B (bachelor), GRAD (graduate).

Person

Create the class Person in personnel. Person is assigned a name and an education title as constructor parameters. Person has also a method to communicate their education, public Education getEducation(), as well as a toString method which returns the person information following the example below.

    Person arto = new Person("Arto", Education.D);
    System.out.println(arto);
Arto, D

Employees

Create the class Employees in personnel. An Employees object contains a list of Person objects. The class has a parameterless constructor plus the following methods:

  • public void add(Person person) adds the parameter person to the employees
  • public void add(List<Person> persons) adds the parameter list of people to the employees
  • public void print() prints all the employees
  • public void print(Education education) prints all the employees, who have the same education as the one specified as parameter

ATTENTION: The Print method of the class Employees have to be implemented using an iterator!

Firing

Create the method public void fire(Education education) in the class Employees. The method deletes all the employees whose education is the same as the method argument.

ATTENTION: implement the method using an iterator!

Below, you find an example of the class usage:

public class Main {

    public static void main(String[] args) {
        Employees university = new Employees();
        university.add(new Person("Matti", Education.D));
        university.add(new Person("Pekka", Education.GRAD));
        university.add(new Person("Arto", Education.D));

        university.print();

        university.fire(Education.GRAD);

        System.out.println("==");

        university.print();
}

Prints:

Matti, D
Pekka, GRAD
Arto, D
==
Matti, D
Arto, D

Loops and continue

In addition to the break statement, loops have also got the continue statement, which allows you to skip to the following loop stage.

    List<String> names = Arrays.asList("Matti", "Pekka", "Arto");

    for(String name: names) {
        if (name.equals("Arto")) {
            continue;
        }

        System.out.println(name);
    }
Matti
Pekka

The continue statement is used especially when we know the iterable variables have got values with which we do not want to handle at all. The classic manner of approach would be using an if statement, but the continue statement allows for another approach to handle with the values, which avoids indentations and possibly helps readability. Below, you find two examples, where we go through the numbers of a list. If the number is smaller than 5 and contains 100, or if it contains 40, it is not printed; otherwise it is.

    List<Integer> values = Arrays.asList(1, 3, 11, 6, 120);

    for(int num: values) {
        if (num > 4 && num % 100 != 0 && num % 40 != 0) {
            System.out.println(num);
        }
    }

    for(int num: values) {
        if (num < 5) {
            continue;
        }

        if (num % 100 == 0) {
            continue;
        }

        if (num % 40 == 0) {
            continue;
        }

        System.out.println(num);
    }
11
6
11
6

More about Enums

Next, we create enums which contain object variables and implement an interface.

Enumerated Type Constructor Parameters

Enumerated types can contain object variables. Object variable values have to be set up in the constructor of the class defined by enumerated type. Enum-type classes cannot have public constructors.

public enum Colour {
    RED("red"), // the constructor parameters are defined as constant values when they are read
    GREEN("green"),
    BLUE("blue");

    private String name; // object variable

    private Colour(String name) { // constructor
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

The enumerated value Colour can be used in the following way:

    System.out.println(Colour.GREEN.getName());
green

Film Reference

Recently, in October 2006 after arriving to Finland, Netflix promised one million dollars to the person or group of people who developed a program, which would be 10% better than their own program. The challenge was met September 2009 (http://www.netflixprize.com/).

With this exercise, we create a program to recommend films. Below, you see how it should work:

    EvaluationRegister ratings = new EvaluationRegister();

    Film goneWithTheWind = new Film("Gone with the Wind");
    Film theBridgesOfMadisonCounty = new Film("The Bridges of Madison County");
    Film eraserhead = new Film("Eraserhead");

    Person matti = new Person("Matti");
    Person pekka = new Person("Pekka");
    Person mikke = new Person("Mikke");
    Person thomas = new Person("Thomas");

    ratings.addRating(matti, goneWithTheWind, Rating.BAD);
    ratings.addRating(matti, theBridgesOfMadisonCounty, Rating.GOOD);
    ratings.addRating(matti, eraserhead, Rating.FINE);

    ratings.addRating(pekka, goneWithTheWind, Rating.FINE);
    ratings.addRating(pekka, theBridgesOfMadisonCounty, Rating.BAD);
    ratings.addRating(pekka, eraserhead, Rating.MEDIOCRE);

    ratings.addRating(mikke, eraserhead, Rating.GOOD);


    Reference reference = new Reference(votes);
    System.out.println(thomas + "'s recommendation: " +
            reference.recommendFilm(thomas));
    System.out.println(mikke + "'s recommendation: " +
            reference.recommendFilm(mikke));
Thomas's recommendation: The Bridges of Madison County
Mikke's recommendation: Gone with the Wind

The program is able to recommend films both according to their common appraisal and according to the ratings given by a specific person. Let's start to build our program.

Person and Film

Create the package reference.domain, and there you add the classes Person and Film. Both classes have a public constructor public Class(String name), as well as the method public String getName(), which returns the name received with the argument.

    Person p = new Person("Pekka");
    Film f = new Film("Eraserhead");

    System.out.println(p.getName() + " and " + f.getName());
Pekka and Eraserhead

Also add the method public String toString() which returns the name received with the argument, as well as the method equals and hashCode.

Override equals so that the equivalence is checked according to the object variable name. Look at the example in section 45.1. In Section 45.2 there are guidelines to override hashCode methods. At least, you'd better generate the HashCode automatically, following the instructions at the end of the section:

NetBeans allows for the automatic creation of the equals and hashCode methods. From the menu Source -> Insert Code, you can choose equals() and hashCode(). After this, NetBeans asks which object variables the methods shall use.

Attention: to help finding mistakes, you may want to implement toString methods to Person and Film, but the tests do not require them.

Rating

Create the enumerated type Rating in reference.domain. The enum class Rating has a public method public int getValue(), which returns the value of the rating. The value names and their grades have to be the following:

RatingValue
BAD-5
MEDIOCRE-3
NOT_WATCHED0
NEUTRAL1
FINE3
GOOD5

The class could be used in the following way:

    Rating given = Rating.GOOD;
    System.out.println("Rating " + given + ", value " + given.getValue());
    given = Rating.NEUTRAL;
    System.out.println("Rating " + given + ", value " + given.getValue());
Rating GOOD, value 5
Rating NEUTRAL, value 1

RatingRegister, Part 1

Let's get started with the implementation necessary to store the ratings.

Create the class RatingRegister in the package reference; the class has the constructor public RatingRegister(), as well as the following methods:

  • public void addRating(Film film, Rating rating) adds a new rating to the parameter film. The same film can have various same ratings.
  • public List<Rating> getRatings(Film film) returns a list of the ratings which were added in connection to a film.
  • public Map<Film, List<Rating>> filmRatings() returns a map whose keys are the evaluated films. Each film is associated to a list containing the ratings for that film.

Test the methods with the following source code:

    Film theBridgesOfMadisonCounty = new Film("The Bridges of Madison County");
    Film eraserhead = new Film("Eraserhead");

    RatingRegister reg = new RatingRegister();
    reg.addRating(eraserhead, Rating.BAD);
    reg.addRating(eraserhead, Rating.BAD);
    reg.addRating(eraserhead, Rating.GOOD);

    reg.addRating(theBridgesOfMadisonCounty, Rating.GOOD);
    reg.addRating(theBridgesOfMadisonCounty, Rating.FINE);

    System.out.println("All ratings: " + reg.filmRatings());
    System.out.println("Ratings for Eraserhead: " + reg.getRatings(eraserhead));
All ratings: {The Bridges of Madison County=[GOOD, FINE], Eraserhead=[BAD, BAD, GOOD]}
Ratings for Eraserhead: [BAD, BAD, GOOD]

RatingRegister, Part 2

Let's make possible to add personal ratings.

Add the following methods to the class RatingRegister:

  • public void addRating(Person person, Film film, Rating rating) adds the rating of a specific film to the parameter person. The same person can recommend a specific film only once. The person rating has also to be added to the ratings connected to all the films.
  • public Rating getRating(Person person, Film film) returns the rating the paramater person has assigned to the parameter film. If the person hasn't evaluated such film, the method returns Rating.NOT_WATCHED.
  • public Map<Film, Rating> getPersonalRatings(Person person) returns a HashMap which contains the person's ratings. The HashMap keys are the evaluated films, and their values are the ratings of these films.
  • public List<Person> reviewers() returns a list of the people who have evaluate the films.

People's ratings should be stored into a HashMap, and the people should act as keys. The values of the HashMap is another HashMap, whose keys are films and whose values are ratings.

Test your improved RatingRegister with the following source code:

    RatingRegister ratings = new RatingRegister();

    Film goneWithTheWind = new Film("Gone with the Wind");
    Film eraserhead = new Film("Eraserhead");

    Person matti = new Person("Matti");
    Person pekka = new Person("Pekka");

    ratings.addRating(matti, goneWithTheWind, Rating.BAD);
    ratings.addRating(matti, eraserhead, Rating.FINE);

    ratings.addRating(pekka, goneWithTheWind, Rating.GOOD);
    ratings.addRating(pekka, eraserhead, Rating.GOOD);

    System.out.println("Ratings for Eraserhead: " + ratings.getRatings(eraserhead));
    System.out.println("Matti's ratings: " + ratings.getPersonalRatings(matti));
    System.out.println("Reviewers: " + ratings.reviewers());
Ratings for Eraserhead: [FINE, GOOD]
Matti's ratings: {Gone with the Wind=BAD, Eraserhead=FINE}
Reviewers: [Pekka, Matti]

Next, we create a couple of helping classes to help evaluation.

PersonComparator

Create the class PersonComparator in the package reference.comparator. The class PersonComparator has to implement the interface Comparator<Person>, and it has to have the constructor public PersonComparator(Map<Person, Integer> peopleIdentities). The class PersonComparator is used later on to sort people according to their number.

Test the class with the following source code:

    Person matti = new Person("Matti");
    Person pekka = new Person("Pekka");
    Person mikke = new Person("Mikke");
    Person thomas = new Person("Thomas");

    Map<Person, Integer> peopleIdentities = new HashMap<Person, Integer>();
    peopleIdentities.put(matti, 42);
    peopleIdentities.put(pekka, 134);
    peopleIdentities.put(mikke, 8);
    peopleIdentities.put(thomas, 82);

    List<Person> ppl = Arrays.asList(matti, pekka, mikke, thomas);
    System.out.println("People before sorting: " + ppl);

    Collections.sort(ppl, new PersonComparator(peopleIdentities));
    System.out.println("People after sorting: " + ppl);
People before sorting: [Matti, Pekka, Mikke, Thomas]
People after sorting: [Pekka, Thomas, Matti, Mikke]

FilmComparator

Create the class FilmComparator in the package reference.comparator. The class FilmComparator has to implement the interface Comparator<Film>, and it has to have the constructor public FilmComparator(Map<Film, List<Rating>> ratings). The class FilmComparator will be used later on to sort films according to their ratings.

The class FilmComparator has to allow for film sorting according to the average of the rating values they have received. The films with the greatest average should be placed first, and the ones with the smallest average should be the last.

Test the class with the following source code:

    RatingRegister ratings = new RatingRegister();

    Film goneWithTheWind = new Film("Gone with the Wind");
    Film theBridgesOfMadisonCounty = new Film("The Bridges of Madison County");
    Film eraserhead = new Film("Eraserhead");

    Person matti = new Person("Matti");
    Person pekka = new Person("Pekka");
    Person mikke = new Person("Mikke");

    ratings.addRating(matti, goneWithTheWind, Rating.BAD);
    ratings.addRating(matti, theBridgesOfMadisonCounty, Rating.GOOD);
    ratings.addRating(matti, eraserhead, Rating.FINE);

    ratings.addRating(pekka, goneWithTheWind, Rating.FINE);
    ratings.addRating(pekka, theBridgesOfMadisonCounty, Rating.BAD);
    ratings.addRating(pekka, eraserhead, Rating.MEDIOCRE);

    ratings.addRating(mikke, eraserhead, Rating.BAD);

    Map<Film, List<Rating>> filmRatings = ratings.filmRatings();

    List<Film> films = Arrays.asList(goneWithTheWind, theBridgesOfMadisonCounty, eraserhead);
    System.out.println("The films before sorting: " + films);

    Collections.sort(films, new FilmComparator(filmRatings));
    System.out.println("The films after sorting: " + films);
The films before sorting: [Gone with the Wind, The Bridges of Madison County, Eraserhead]
The films after sorting: [The Bridges of Madison County, Gone with the Wind, Eraserhead]

Reference, Part 1

Implement the class Reference in the package reference. The class Reference receives a RatingRegister object as constructor parameter. Reference uses the ratings in the rating register to elaborate a recommendation.

Implement the method public Film recommendFilm(Person person), which implements films to people. Hint: you need three things to find out the most suitable film. These are at least the class FilmComparator which you created earlier on; the method public Map<Film, List<Rating>> filmRatings() of the class RatingRegister; and a list of the existing films.

Test your program with the following source code:

    RatingRegister ratings = new RatingRegister();

    Film goneWithTheWind = new Film("Gone with the Wind");
    Film theBridgesOfMadisonCounty = new Film("The Bridges of Madison County");
    Film eraserhead = new Film("Eraserhead");

    Person matti = new Person("Matti");
    Person pekka = new Person("Pekka");
    Person mikke = new Person("Mikke");

    ratings.addRating(matti, goneWithTheWind, Rating.BAD);
    ratings.addRating(matti, theBridgesOfMadisonCounty, Rating.GOOD);
    ratings.addRating(matti, eraserhead, Rating.FINE);

    ratings.addRating(pekka, goneWithTheWind, Rating.FINE);
    ratings.addRating(pekka, theBridgesOfMadisonCounty, Rating.BAD);
    ratings.addRating(pekka, eraserhead, Rating.MEDIOCRE);

    Reference ref = new Reference(ratings);
    Film recommended = ref.recommendFilm(mikke);
    System.out.println("The film recommended to Michael is: " + recommended);
The film recommended to Michael is: The Bridges of Madison County

Now, our first part works fine exclusively for people who have never evaluated any movie. In such cases, we can't say anything about their film tastes, and the best choice is recommending them the film which has received the hightest average among the ratings.

Reference, Part 2

Attention! The exercise is challenging. First you should do the previous exercises and coming back to this one later. You can return the sequence of exercises in TMC; even though you don't get the point for this part, you'd get the points for the perious ones, as it is with all the exercises.

Unfortunately, the error diagnostics of this part is not similar to the previous parts.

If people have added their own preferences to the reference service, we know something about their film tastes. Let's extend the functionality of our reference to create a personal recommendation if the person has evaluated films. The functionality implemented in the previous part has to be preserved: if a person hasn't evaluated any film, we recommend them a film according to film ratings.

Personal recommendations are based on the similarity between the person ratings and other people's ratings. Let's reason about it with the help of the following table; in the first line on the top there are films, and the people who have rated are on the left. The brackets describe the ratings given.

Person \ FilmGone with the WindThe Bridges of Madison CountyEraserheadBlues Brothers
MattiBAD (-5)GOOD (5)FINE (3)-
PekkaFINE (3)-BAD (-5)MEDIOCRE (-3)
Mikael--BAD (-5)-
Thomas-GOOD (5)-GOOD (5)

If we want to find the suitable film for Mikael, we can explore the similarity between Mikael's and other people's preferences. The similarity is calculated based on the ratings: as the sum of the products of the ratings for the films watched by both. For instance, Mikael and Thomas's similarity is 0, because they haven't watched the same films.

If we calculate Mikael and Pekka's similarity, we find out that the sum of the products of the films they have in common is 25. Mikael and Pekka have both watched only one film, and they have both given it the grade bad (-5).

-5 * -5 = 25

Mikael and Matti's similarity is -15. Mikael and Matti have also watched only one same film. Mikael gave the grade bad (-5) to the film, whereas Matti gave it the grade fine (3).

-5 * 3 = -15

Based on that Mikael can be recommended films according to Pekka's taste: the recommendation is Gone with the Wind.

On the other hand, if we want to find a suitable film for Matti, we have to find the similarity between Matti and everyone else. Matti and Pekka have watched two same films. Matti gave Gone with the Wind the grade bad (-5), Pekka the grade fine (3). Matti gave fine (3) to Eraserhead, and Pekka gave bad (-5). Matti and Pekka's similarity is -30.

-5 * 3 + 3 * -5 = -30

Matti and Mikael's similarity is -15, which we know according to out previous calculations. Similarities are symmetrical.

Matti and Thomas have watched Gone with the Wind, and they both gave it the grade good (5). Matti and Thomas's similarity is 25, then.

5 * 5 = 25

Matti has to be recommended a film according to Thomas' taste: the recommendation will be the Blues Brothers.

Implement the recommendation mechanism described above. The method recommendFilm should return null in two cases: if you cannot find any film to recommend; if you find a, say, person1 whose film taste is appropriate to recommend films to, say, person2, but person1 has rated bad, mediocre, or neutral, all the films person2 hasn't watched, yet. The approach described above has to work also if the person hasn't added any rating.

Do not suggest films which have already been watched.

You can test your program with the following source code:

    RatingRegister ratings = new RatingRegister();

    Film goneWithTheWind = new Film("Gone with the Wind");
    Film theBridgesOfMadisonCounty = new Film("The Bridges of Madison County");
    Film eraserhead = new Film("Eraserhead");
    Film bluesBrothers = new Film("Blues Brothers");

    Person matti = new Person("Matti");
    Person pekka = new Person("Pekka");
    Person mikke = new Person("Mikael");
    Person thomas = new Person("Thomas");
    Person arto = new Person("Arto");

    ratings.addRating(matti, goneWithTheWind, Rating.BAD);
    ratings.addRating(matti, theBridgesOfMadisonCounty, Rating.GOOD);
    ratings.addRating(matti, eraserhead, Rating.FINE);

    ratings.addRating(pekka, goneWithTheWind, Rating.FINE);
    ratings.addRating(pekka, eraserhead, Rating.BAD);
    ratings.addRating(pekka, bluesBrothers, Rating.MEDIOCRE);

    ratings.addRating(mikke, eraserhead, Rating.BAD);

    ratings.addRating(thomas, bluesBrothers, Rating.GOOD);
    ratings.addRating(thomas, theBridgesOfMadisonCounty, Rating.GOOD);

    Reference ref = new Reference(ratings);
    System.out.println(thomas + " recommendation: " + ref.recommendFilm(thomas));
    System.out.println(mikke + " recommendation: " + ref.recommendFilm(mikke));
    System.out.println(matti + " recommendation: " + ref.recommendFilm(matti));
    System.out.println(arto + " recommendation: " + ref.recommendFilm(arto));
Thomas recommendation: Eraserhead
Mikael recommendation: Gone with the Wind
Matti recommendation: Blues Brothers
Arto recommendation: The Bridges of Madison County

Have we made one million bucks? Not yet, maybe. In the course Introduction to Artificial Intelligence and Machine Learning we learn more techniques to build learning programs.

Variable Number of Method Parameters

So far, we have been creating methods which had a clearly defined number of parameters. Java makes it possible to give an indefinite number of a specific type of parameters by placing an ellipsis after the parameter type. For instance the method public int sum(int... values) can be given as parameter as many integers (int) as the user wants. The parameter values can be handled as a table.

    public int sum(int... values) {
        int sum = 0;
        for (int i = 0; i < values.length; i++) {
            sum += values[i];
        }
        return sum;
    }
    System.out.println(sum(3, 5, 7, 9));  // values = {3, 5, 7, 9}
    System.out.println(sum(1, 2));        // values = {1, 2}
24
3

Note that the parameter definition above int... values depends on the fact that the method has a table-like variable called values.

A method can be assigned only one parameter which receives an indefinite number of values, and this must be the first parameter in the method definition. For instance:

    public void print(String... characterStrings, int times) // right!
    public void print(int times, String... strings) // wrong!

An indefinite number of parameter values is used for instance when we want to create an interface which would not force the user to use a precise number of parameters. An alternative approach would be defining a list of that precise type as parameter. In this case, the objects can be assigned to the list before the method call, and the method can be called and given the list as parameter.

Flexible Filtering Criteria

In some exercises (see Library in Introduction to Programming, and Word Inspection in Advanced Programming), we have run into such situations where we had to filter out list objects according to particular criteria; for instance, in Word Inspection the methods wordsContainingZ, wordsEndingInL, palindromes, wordsWhichContainAllVowels all to the same same thing: they go through the file content one word after the other, and they make sure that the specific filtering criteria are satisfied, in which case they store the word. Because the method criteria are different, we haven't been able to get rid of this repetition, and the code of all those methods made wide use of copypaste.

With this exercise, we create a program to filter the lines of the books found on the Project Guttenberg pages. In the following example we analyze Dostojevski's Crime and Punishment. We want to have various different filtering criteria, and that it would be possible to filter according to different criteria combinations. The program structure should also allow for adding new criteria later on.

A suitable solution to the problem, is defining the filtering criteria as objects of their own which implement the interface Criterion. The interface definition is below:

public interface Criterion {
    boolean complies(String line);
}

A filtering class which implements the interface:

public class ContainsWord implements Criterion {

    String word;

    public ContainsWord(String word) {
        this.word = word;
    }

    @Override
    public boolean complies(String line) {
        return line.contains(word);
    }
}

The class objects are quite simple, in fact, and they only remember the word given as constructor parameter. The only method of the object can be asked whether the criterion complies to the parameter String; if so, this means that the object contains the word stored into the String object.

Together with the excercise body, you find the pre-made class GutenbergReader, which helps you to analyze book lines according to the filtering criteria given as parameter:

public class GutenbergReader {

    private List<String> lines;

    public GutenbergReader(String address) throws IllegalArgumentException {
        // the code which retrieves the book from the Internet
    }

    public List<String> linesWhichComplyWith(Criterion c){
        List<String> complyingLines = new ArrayList<String>();

        for (String line : lines) {
            if (c.complies(line)) {
                complyingLines.add(line);
            }
        }

        return complyingLines;
    }
}

With the following code, we print all the lines in Crime and Punishment which contain the word "beer":

public static void main(String[] args) {
    String address = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergReader book = new GutenbergReader(address);

    Criterion criterion = new ContainsWord("beer");

    for (String line : book.linesWhichComplyWith(criterion)) {
        System.out.println(line);
    }
}

All Lines

Create the class AllLines which implements Criterion, which accepts all the lines. This and the other classes of the exercise have to be implemented in the package reader.criteria.

public static void main(String[] args) {
    String address = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergReader book = new GutenbergReader(address);

    Criterion criterion = new AllLines();

    for (String line : book.linesWhichComplyWith(criterion)) {
        System.out.println(line);
    }
}

Ends with Question or Exclamation Mark

Implement class EndsWithQuestionOrExclamationMark, which implements the interface Criterion and accepts the lines whose last character is a question or an exclamation mark.

public static void main(String[] args) {
    String address = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergReader book = new GutenbergReader(address);

     Criterion criterion = new EndsWithQuestionOrExclamationMark();

    for (String line : book.linesWhichComplyWith(criterion)) {
        System.out.println(line);
    }
}

Reminder: you compare character in Java with the == operator:

String name = "pekka";

// ATTENTION: 'p' is a character, that is to say char p; differently, "p" is a String, whose only character is p
if ( name.charAt(0) == 'p' ) {
    System.out.println("beginning with p");
} else {
    System.out.println("beginning with something else than p");
}

Length At Least

Implement the class LengthAtLeast, which implements the interface Criterion and accepts the lines whose length is equal or greater than the number received as constructor parameter.

public static void main(String[] args) {
    String address = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergReader book = new GutenbergReader(address);

     Criterion criterion = new LengthAtLeast(40);

    for (String line : book.linesWhichComplyWith(criterion)) {
        System.out.println(line);
    }
}

Both

Create the class Both which implements the interface Criterion. The objects of this class receive two objects as constructor parameter, both implementing the interface Criterion. Both objects accept the lines which comply with both the criteria received as constructor parameters. We print below all the lines which end with a question or an exclamation mark and that contain the word "beer".

public static void main(String[] args) {
    String address = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergReader book = new GutenbergReader(address);

    Criterion criterion = new Both(
                    new EndsWithQuestionOrExclamationMark(),
                    new ContainsWord("beer")
                );

    for (String line : book.linesWhichComplyWith(criterion)) {
        System.out.println(line);
    }
}

Negation

Create the class Not which implement the interface Criterion and accepts the lines, which don't comply with the criterion received as parameter. We print below the lines whose length is less than 10.

public static void main(String[] args) {
    String address = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergReader book = new GutenbergReader(address);

    Criterion criterion = new Not( new LengthAtLeast(10) );

    for (String line : book.linesWhichComplyWith(criterion)) {
        System.out.println(line);
    }
}

At Least One

Create the class AtLeastOne which implements the interface Criterion. The objects of this class receive as parameter an optional amount of objects which implement the interface Criterion; this means that the constructor receives a list of variable length as parameter. AtLeastOne objects accept the lines which comply with at least one of the criteria received as constructor parameter. We print below the lines which contain one ot the words "beer", "milk" or "oil".

public static void main(String[] args) {
    String address = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergReader book = new GutenbergReader(address);

    Criterion criterion =new AtLeastOne(
                    new ContainsWord("beer"),
                    new ContainsWord("milk"),
                    new ContainsWord("oil")
                );

    for (String line : book.linesWhichComplyWith(criterion)) {
        System.out.println(line);
    }
}

Note that the criteria can be combined as preferred. You find below a criterion which accepts the lines which have at least one of the words "beer", "milk" and "oil" and whose length is between 20-30 characters.

    Criterion words = new AtLeastOne(
                    new ContainsWord("beer"),
                    new ContainsWord("milk"),
                    new ContainsWord("oil")
                );

    Criterion rightLength = new Both(
                         new LengthAtLeast(20),
                         new Not( new LengthAtLeast(31))
                       );

    Criterion wanted = new Both(words, rightLength);

StringBuilder

We have got accustomed to building strings in the following way:

    public static void main(String[] args) {
        int[] t = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        System.out.println(build(t));
    }

    public static String build(int[] t) {
        String str = "{";

        for (int i = 0; i < t.length; i++) {
            str += t[i];
            if (i != t.length - 1) {
                str += ", ";

            }
        }

        return str + "}";
    }

Result:

{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

This works, but it could be more effective. As we remember, strings are immutable objects. The result of a String operation is always a new String object. This means that in the interphases of previous example 10 String objects were created. If the dimension of the input was bigger, creating new objects in the various interphases would start to have an unpleasant impact on the program execution time.

String builder

In situations like the previous one, it is better to use StringBuilder objects when it comes to building strings. Differently from Strings, StringBuilders are not immutable, meaning StringBuilder-objects can be modified. Get acquainted with the description of the StringBuilder API (you'll find it by googling "stringbuilder java api 6") and modify the method in the exercise body, public static String build(int[] t), so that it would use StringBuilder and work in the following way:

{
1, 2, 3, 4,
5, 6, 7, 8,
9, 10
}
  

The curly brackets are on their own lines. There are up to four values in each line of the table, and the first value requires an initial space. Before each number, and after the comma there must be exactly one space.

Grande Finale

The course is nearing the end, and it's time for the grande finale!

Worm Game

In this exercise, we create the structure and functionality for the following worm game. Unlike in the picture below, our worm game will be coloured differently: worm is black, apple is red and background is gray.

Piece and Apple

Create the class Piece in wormgame.domain. The class Piece has the constructor public Piece(int x, int y), which receives the position of the piece as parameter. Moreover, the class Piece has the following methods.

  • public int getX() returns the x coordinate of the piece, which was assigned in the constructor.
  • public int getY() returns the y coordinate of the piece, which was assigned in the constructor.
  • public boolean runsInto(Piece piece) returns true if the object has the same coordinates as the Piece instance received as paramater.
  • public String toString() returns the piece position following the pattern (x,y). For instance. (5,2) if the value of x is 5 and the value of y is 2.

Also implement the class Apple in wormgame.domain, and let Apple inherit Piece.

Worm

Implement the class Worm in the package wormgame.domain. The class Worm has the constructor public Worm(int originalX, int originalY, Direction originalDirection), which creates a new worm whose direction is the parameter originalDirection. Worm is composed of a list of instances of the class Piece. Attention: the pre-made enum Direction can be found in the package wormgame.

When it's created, Worm's length is one, but its "mature" length is three. Worm has to grow by one piece while it moves. When its length is three, Worm grows only by eating.

Implement the following methods

  • public Direction getDirection() return's Worm's direction.
  • public void setDirection(Direction dir) sets a new direction for the worm. The worm starts to move in the new direction when the method move is called again.
  • public int getLength() returns the Worm's length. The Worm's length has to be equal to the length of the list returned by the method getPieces().
  • public List<Piece> getPieces() returns a list of Piece objects which the worm is composed of. The pieces in the list are in order, with the worm head at the end of the list.
  • public void move() moves the worm one piece forward.
  • public void grow() grows the worm by one piece. The worm grows together with the following move method call; after the first move method call the worm does not grow any more.
  • public boolean runsInto(Piece piece) checks whether the worm runs into the parameter piece. If so -- that is, if a piece of the worm runs into the parameter piece -- the method returns the value true; otherwise it returns false.
  • public boolean runsIntoItself() check whether the worm runs into itself. If so -- that is, if one of the worm's pieces runs into another piece -- the method returns the value true. Otherwise it returns false.

The functionality of public void grow() and public void move() has to be implemented so that the worm grows only with the following move.

Motion should be implemented in a way that the worm is always given a new piece. The position of the new piece depends on the moving direction: if moving left, the new piece location should on the left of the head piece, i.e. the x coordinate of the head should be smaller by one. If the location of the new piece is under the old head -- if the worm's direction is down, the y coordinate of the new piece should be by one bigger than the y coordinate of the head (when we draw, we will have to get accustometd to a coordinate system where the y axe is reverse).

When the worm moves, a new piece is added to the list, and the first piece is deleted from the beginning of the list. In this way, you don't need to update the coordinates of each single piece. Implement the growth so that the first piece is deleted if the grow method has just been called.

Attention! The worm has to grow constantly if its length is less than 3.

        Worm worm = new Worm(5, 5, Direction.RIGHT);
        System.out.println(worm.getPieces());
        worm.move();
        System.out.println(worm.getPieces());
        worm.move();
        System.out.println(worm.getPieces());
        worm.move();
        System.out.println(worm.getPieces());

        worm.grow();
        System.out.println(worm.getPieces());
        worm.move();
        System.out.println(worm.getPieces());

        worm.setDirection(Direction.LEFT);
        System.out.println(worm.runsIntoItself());
        worm.move();
        System.out.println(worm.runsIntoItself());
[(5,5)]
[(6,5), (5,5)]
[(7,5), (6,5), (5,5)]
[(8,5), (7,5), (6,5)]
[(8,5), (7,5), (6,5)]
[(9,5), (8,5), (7,5), (6,5)]
false
true

Worm Game, Part 1

Let's modify the class WormGame which is contained in wormgame.game, and encapsulates the functionality of the game. The class WormGame inherits the class Timer, which provides the time functionality to update the game. In order to work, the class Timer requires a class which implements the interface ActionListener, and we have implemented it in WormGame.

Modify the functionality of WormGame's constructor so that the game's Worm is created in the constructor. Create the worm so that the position of the worm depends on the parameters received in the constructor of the class WormGame. The worm's x coordinate has to be width / 2, the y coordinate height / 2 , and the direction Direction.DOWN.

Also create an apple inside the constructor. The apple coordinates have to be random, but the apple x coordinate has to be contained between [0, width[, and the y coordinate between [0, height[.

Also add the following methods to the WormGame:

  • public Worm getWorm() returns the WormGame worm.
  • public void setWorm(Worm worm) sets on the game the method parameter worm. If the method getWorm is called after the worm has been set up, it has to return a reference to the same worm.
  • public Apple getApple returns the apple of the WormGame.
  • public void setApple(Apple apple) sets the method parameter apple on the worm game. If the method getApple is called after the apple has been set up, it has to return a reference to the same apple.

Worm Game, Part 2

Modify the functionality of the method actionPerformed so that it would implement the following tasks in the given order.

  1. Move the worm
  2. If the worm runs into the apple, it eats the apple and calls the grow method. A new apple is randomly created.
  3. If the worm runs into itself, the variable continue is assigned the value false
  4. Call update, which is a method of the variable updatable which implements the interface Updatable.
  5. Call the setDelay method which is inherited from the Timer class. The game velocity should grow with respect to the worm length. The call setDelay(1000 / worm.getLength()); will work for it: in the call we expect that you have defined the object variable worm.

Let's start next to build our user interface components.

Keyboard listener

Implement the class KeyboardListener in wormgame.gui. The class has the constructor public KeyboardListener(Worm worm), and it implements the interface KeyListener. Replace the method keyPressed so that the worm is assigned direction up when the up arrow key. Respectively, the worm is assigned the directions down, left, or right, when the user presses the down, left, or right arrow key.

DrawingBoard

Create the class DrawingBoard in wormgame.gui. The DrawingBoard inherits the class JPanel, and its constructor receives an instance of the class WormGame and the int variable pieceLength as parameters. The variable pieceLength tells the dimension of the pieces; length and height of the pieces are equal.

Replace the paintComponent method which was inherited from the class JPanel so that the method draws the worm and the apple. Use the Graphics object's fill3DRect method to draw the worm. The worm colour has to be black (Color.BLACK). The apple has to be drawn with the Graphics object's fillOval method, and its colour has to be red.

Also implement the interface Updatable in DrawingBoard. The method update of Updatable has to call the repaint method of the class JPanel.

User Interface

Modify the class UserInterface to contain DrawingBoard. In the method createComponents, you have to create an instance of DrawingBoard and add it into the Container object. Create an instance of KeyboardListener at the end of createComponents, and add it to the Frame object.

Also add the method public Updatable getUpdatable() to the class UserInterface, returning the drawing board which was created in createComponents.

You can start the user interface from the Main class in the following way. Before the game starts, we wait that the user interface is created. When the user interface is created, it gets connected to the worm game and the game gets started.

        WormGame game = new WormGame(20, 20);

        UserInterface ui = new UserInterface(game, 20);
        SwingUtilities.invokeLater(ui);

        while (ui.getUpdatable() == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                System.out.println("The drawing board hasn't been created yet.");
            }
        }

        game.setUpdatable(ui.getUpdatable());
        game.start();

Course Feedback

Course Feedback

We have received a lot of valuable feedback through TMC. Therefore, as your last task in the course we would like to have feedback on the course in general. Give feedback by filling out the comment section which appears when you have submitted the exercise. If comment section doesn't pop up, please write your feedback in the Main-class of the downloaded exercise and submit it again.

In order to receive the points of this exercise, run the TMC tests and send the exercise to our server.

Retired

Tiedostojen valitseminen käyttöliittymästä

Silloin tällöin eteen tulee tilanne, jossa käyttäjän pitää pystyä valitsemaan tiedosto tiedostojärjestelmästä. Java tarjoaa tiedostojen valintaan valmiin käyttöliittymäkomponentin JFileChooser.

JFileChooser poikkeaa tähän mennessä käyttämistämme käyttöliittymäkomponenteista siinä, että se avaa uuden ikkunan. Avautuvan ikkunan ulkonäkö riippuu hieman käyttöjärjestelmästä, esimerkiksi hieman vanhemmassa Fedora-käyttöjärjestelmässä ikkuna on seuraavannäköinen.

JFileChooser-olio voidaan luoda missä tahansa. Olion metodille showOpenDialog annetaan parametrina käyttöliittymäkomponentti, johon se liittyy, esimerkiksi JFrame-luokan ilmentymä. Metodi showOpenDialog avaa tiedostonvalintaikkunan, ja palauttaa int-tyyppisen statuskoodin riippuen käyttäjän valinnasta. Luokassa JFileChooser on määritelty int-tyyppiset luokkamuuttujat, jotka kuvaavat statuskoodeja. Esimerkiksi onnistuneella valinnalla on arvo JFileChooser.APPROVE_OPTION.

Valittuun tiedostoon pääsee JFileChooser-oliosta käsiksi metodilla getSelectedFile.

    JFileChooser chooser = new JFileChooser();

    int valinta = chooser.showOpenDialog(frame);

    if (valinta == JFileChooser.APPROVE_OPTION) {
        File valittu = chooser.getSelectedFile();
        System.out.println("Valitsit tiedoston: " + valittu.getName());
    } else if (valinta == JFileChooser.CANCEL_OPTION) {
        System.out.println("Et valinnut tiedostoa!");
    }

Yllä oleva esimerkki avaa valintaikkunan, ja tulostaa valitun tiedoston nimen jos valinta onnistuu. Jos valinta epäonnistuu, ohjelma tulostaa "Et valinnut tiedostoa!".

Tiedostojen filtteröinti

Tiedostojen filtteröinnillä tarkoitetaan vain tietynlaisten tiedostojen näyttämistä tiedostoikkunassa. JFileChooser-oliolle voi asettaa filtterin metodilla setFileFilter. Metodi setFileFilter saa parametrina abstraktin luokan FileFilter-ilmentymän, esimerkiksi luokasta FileNameExtensionFilter tehdyn olion.

Luokka FileNameExtensionFilter mahdollistaa tiedostojen filtteröinnin niiden päätteiden perusteella. Esimerkiksi pelkät txt-päätteiset tekstitiedostot saa näkyviin seuraavasti.

    JFileChooser chooser = new JFileChooser();
    chooser.setFileFilter(new FileNameExtensionFilter("Tekstitiedostot", "txt"));

    int valinta = chooser.showOpenDialog(frame);

    if (valinta == JFileChooser.APPROVE_OPTION) {
        File valittu = chooser.getSelectedFile();
        System.out.println("Valitsit tiedoston: " + valittu.getName());
    } else if (valinta == JFileChooser.CANCEL_OPTION) {
        System.out.println("Et valinnut tiedostoa!");
    }

Nopeustesti

Luodaan ohjelma, joka mittaa kliksutteluvauhtia. Käyttöliittymä tulee näyttämään esimerkiksi seuraavalta.

Oma luokka JButtonille

Toteuta pakkaukseen nopeustesti luokka Nappi, joka perii JButtonin. Luokalla Nappi tulee olla konstruktori public Nappi(String text, Color aktiivinen, Color passiivinen). Konstruktorin parametrina saama characterString text tulee antaa parametrina yläluokan konstruktorille (kutsu super(text)).

Korvaa luokasta JButton peritty metodi protected void paintComponent(Graphics g) siten, että piirrät metodissa napin kokoisen värillisen ympyrän. Saat napin leveyden ja korkeuden JButton-luokalta perityistä metodeista getWidth() ja getHeight(). Kutsu korvatun metodin alussa yläluokan paintComponent-metodia.

Ympyrän värin tulee riippua Napin tilasta: jos nappi on aktiivinen (metodi isEnabled palauttaa true tulee ympyrän väri olla konstruktorin parametrina saatu aktiivinenVari. Muulloin käytetään väriä passiivinenVari.

Perustoiminta

Toteuta luokkaan Nopeustesti käyttöliittymä, jossa on neljä nappulaa ja teksti. Käytä asettelussa napeille omaa JPanel-alustaa, joka asetetaan BorderLayout-asettelijan keskelle. Teksti tulee BorderLayout-asettelijan alaosaan.

Käytä edellisessä osassa luomaasi Nappi-luokkaa. Napeille tulee antaa konstruktorissa tekstit 1, 2, 3 ja 4.

Nappuloiden aktiivisuus

Vain yhden nappulan kerrallaan tulee olla painettavissa (eli aktiivisena). Voit tehdä nappulasta ei-aktiivisen metodikutsulla nappi.setEnabled(false). Vastaavasti nappi muutetaan aktiiviseksi kutsulla nappi.setEnabled(true).

Kun aktiivisena olevaa nappulaa painetaan, tulee käyttöliittymän arpoa uusi aktiivinen nappi.

Pisteytys

Tehdään peliin pisteytys: mitataan 20 painallukseen kuluva aika. Helpoin tapa ajan mittaamiseen on metodin System.currentTimeMillis() kutsuminen. Metodi palauttaa kokonaisluvunu, joka laskee millisekunteja (tuhannesosasekunteja) jostain tietysti ajanhetkestä lähtien. Siispä voit mitata kulunutta aikaa kutsumalla currentTimeMillis pelin alussa ja lopussa ja laskemalla erotuksen.

Toteuta siis seuraava: peli laskee napinpainallusten määrän, ja 20. painalluksen jälkeen asettaa kaikki nappulat epäaktiivisiksi ja näyttää JLabel-komponentissa viestin "Pisteesi: XXXX", jossa XXXX on painalluksiin kulunut aika (millisekunteina) jaettuna 20:lla. Pienempi pistemäärä on siis parempi.

Tiedostonnäytin

Tässä tehtävässä toteutetaan ohjelma, joka lukee käyttäjän valitseman tiedoston ja näyttää sen sisällön käyttöliittymässä.

Ohjelmassa on eroteltu käyttöliittymään ja sovelluslogiikka. Tehtäväpohjassa on valmiina sovelluslogiikan rajapinta TiedostonLukija sekä käyttöliittymäluokan runko Kayttoliittyma.

Käyttöliittymän rakentaminen

Täydennä käyttöliittymäluokan metodi luoKomponentit. Ohjelma tarvitsee toimiakseen kolme käyttöliittymäkomponenttia:

Koska tässä tehtävässä on vain kolme aseteltavaa komponenttia, riittävät asetteluun BorderLayout-asettelijan vaihtoehdot: BorderLayout.NORTH, BorderLayout.CENTER ja BorderLayout.SOUTH. Käyttöliittymäkomponentti JTextArea kannattaa sijoittaa keskelle, jotta se saa mahdollisimman paljon tilaa tekstin näyttämiselle.

Käyttöliittymän pitäisi näyttää suunnilleen seuraavalta. Alla olevassa esimerkissä JLabel-oliossa ei ole mitään tekstiä.

Tiedoston lukeminen

Luo pakkaukseen tiedostonnaytin.sovelluslogiikka luokka OmaTiedostonLukija, joka toteuttaa rajapinnan TiedostonLukija. Rajapinnassa on yksi metodi, lueTiedosto, joka lukee sille annetun tiedoston kokonaisuudessaan characterStringon ja palauttaa tämän characterStringn.

Rajapinnan koodi:

package tiedostonnaytin.sovelluslogiikka;

import java.io.File;

public interface TiedostonLukija {
    String lueTiedosto(File tiedosto);
}

Huom: Palautettavassa characterStringssa tulee säilyttää myös rivinvaihdot "\n". Esimerkiksi Scanner-lukijan metodi nextLine() poistaa palauttamistaan characterStringista rivinvaihdot, joten joudut joko lisäämään ne takaisin tai lukemaan tiedostoa eri tavalla.

Käyttöliittymän kytkeminen sovelluslogiikkaan

Viimeisessä tehtävän osassa toteutetaan käyttöliittymän JButton-napille tapahtumankuuntelija. Saat itse päättää luokalle sopivan nimen.

Tapahtumankuuntelijan tehtävänä on näyttää JFileChooser-tiedostonvalintaikkuna kun JButton-nappia painetaan. Kun käyttäjä valitsee tiedoston, tulee tapahtumankuuntelijan lukea tiedoston sisältö ja näyttää se JTextArea-kentässä. Tämän jälkeen tapahtumankuuntelijan tulee vielä päivittää JLabel-kenttään näytetyn tiedoston nimi (ilman tiedostopolkua).

JFileChooser-olion metodille showOpenDialog tulee antaa parametrina Kayttoliittyma-luokassa oleva JFrame-ikkunaolio. Jos käyttäjä valitsee tiedoston, tulee tiedosto lukea tapahtumankuuntelijassa Kayttoliittyma-luokassa määriteltyä TiedostonLukija-oliota apuna käyttäen. Kannattaa luoda tapahtumankuuntelija siten, että sille annetaan konstruktorissa kaikki tarvitut oliot.

Huomaa, että valintaikkunan voi myös sulkea valitsematta tiedostoa!

Kun tiedosto on avattu, tulee käyttöliittymän näyttää esimerkiksi seuraavalta.

Tekstiseikkailu

Tehtäväsarjassa tehdään laajennettava tekstiseikkailupelin runko. Seikkailu koostuu kohdista, joissa jokaisessa ruudulle tulee tekstiä. Kohdat voivat olla joko välivaiheita, kysymyksiä, tai monivalintakohtia. Monivalinta-tyyppisen kohdan näyttämä teksti voi olla esimerkiksi seuraavanlainen:

Huoneessa on kaksi ovea. Kumman avaat?

1. Vasemmanpuoleisen.
2. Oikeanpuoleisen.
3. Juoksen pakoon.

Käyttäjä vastaa kohdassa esitettävään tekstiin. Yllä olevaan tekstiin voi vastata 1, 2 tai 3, ja vastauksesta riippuu, minne käyttäjä siirtyy seuraavaksi.

Peliin tullaan toteuttamaan kohtia kuvaava rajapinta ja tekstikäyttöliittymä, jonka kautta peliä pelataan.

Huom! Toteuta kaikki tehtävän vaiheet pakkaukseen "seikkailu"

Kohta ja Välivaihe

Pelissä voi olla hyvinkin erilaisia kohtia, ja edellä olleessa esimerkissä ollut monivalinta on vain eräs vaihtoehto.

Toteuta kohdan käyttäytymistä kuvaava rajapinta Kohta. Rajapinnalla Kohta tulee olla metodi String teksti(), joka palauttaa kohdassa tulostettavan tekstin. Metodin teksti lisäksi kohdalla tulee olla metodi Kohta seuraavaKohta(String vastaus), jonka toteuttavat luokat palauttavat seuraavan kohdan vastauksen perusteella.

Toteuta tämän jälkeen yksinkertaisin tekstiseikkailun kohta, eli ei-interaktiivinen tekstiruutu, josta pääsee etenemään millä tahansa syötteellä. Toteuta ei-interaktiivista tekstiruutua varten luokka Valivaihe, jolla on seuraavanlainen API.

Testaa ohjelmaasi seuraavalla esimerkillä:

    Scanner lukija = new Scanner(System.in);

    Valivaihe alkuteksti = new Valivaihe("Olipa kerran ohjelmoija.");
    Valivaihe johdanto = new Valivaihe("Joka alkoi ohjelmoimaan Javalla.");
    alkuteksti.asetaSeuraava(johdanto);

    Kohta nykyinen = alkuteksti;
    System.out.println(nykyinen.teksti());

    nykyinen = nykyinen.seuraavaKohta(reader.nextLine());
    if (nykyinen == null) {
        System.out.println("Virhe ohjelmassa!");
    }

    System.out.println(nykyinen.teksti());

    nykyinen = nykyinen.seuraavaKohta(reader.nextLine());
    if (nykyinen != null) {
        System.out.println("Virhe ohjelmassa!");
    }
Olipa kerran ohjelmoija.
(jatka painamalla enteriä)

Joka alkoi ohjelmoimaan Javalla.
(jatka painamalla enteriä)

Käyttöliittymä

Pelin käyttöliittymä (luokka Kayttoliittyma) saa konstruktorin parametrina Scanner-olion ja Kohta-rajapinnan toteuttavan pelin aloittavan olion. Luokka tarjoaa metodin public void start(), joka käynnistää pelin suorituksen.

Käyttöliittymä käsittelee kaikkia kohtia Kohta-rajapinnan kautta. Käyttöliittymän tulee jokaisessa kohdassa kysyä kohtaan liittyvältä metodilta teksti tekstiä, joka käyttäjälle näytetään. Tämän jälkeen käyttöliittymä kysyy käyttäjältä vastauksen, ja antaa sen parametrina kohta-olion metodille seuraavaKohta. Metodi seuraavaKohta palauttaa vastauksen perusteella seuraavan kohdan, johon pelin on määrä siirtyä. Peli loppuu, kun metodi seuraavaKohta palauttaa arvon null.

Koska pääohjelma tulee käyttämään kohtia vain Kohta-rajapinnan kautta, voidaan peliin lisätä vaikka minkälaisia kohtia pääohjelmaa muuttamatta. Riittää tehdä uusia Kohta-rajapinnan toteuttavia luokkia.

Toteuta luokka Kayttoliittyma, ja testaa sen toimintaa seuraavalla esimerkillä

        Scanner lukija = new Scanner(System.in);
        Valivaihe alku = new Valivaihe("Olipa kerran ohjelmoija.");
        Valivaihe johdanto = new Valivaihe("Joka alkoi ohjelmoimaan Javalla.");
        Valivaihe loppu = new Valivaihe("Ja päätti muuttaa Helsinkiin.");

        alku.asetaSeuraava(johdanto);
        johdanto.asetaSeuraava(loppu);

        new Kayttoliittyma(lukija, alku).start();
Olipa kerran ohjelmoija.
(jatka painamalla enteriä)
>

Joka alkoi ohjelmoimaan Javalla.
(jatka painamalla enteriä)
>

Ja päätti muuttaa Helsinkiin.
(jatka painamalla enteriä)
>

Käytä seuraavaa metodia käyttöliittymän start-metodina. Yritä piirtää paperille mitä käy kun käyttöliittymä käynnistetään.

    public void start() {
        Kohta nykyinen = alkukohta;

        while (nykyinen != null) {
            System.out.println(nykyinen.teksti());
            System.out.print("> ");
            String vastaus = reader.nextLine();

            nykyinen = nykyinen.seuraavaKohta(vastaus);
            System.out.println("");
        }
    }

Käyttöliittymän start-metodi sisältää siis toistolauseen, jossa ensin tulostetaan käsiteltävän kohdan teksti. Tämän jälkeen kysytään käyttäjältä syötettä. Käyttäjän syöte annetaan vastauksena käsiteltävän kohdan seuraavaKohta-metodille. Metodi seuraavaKohta palauttaa kohdan, jota käsitellään seuraavalla toiston kierroksella. Jos palautettu kohta oli null, lopetetaan toisto.

Kysymyksiä

Tekstiseikkailussa voi olla kysymyksiä, joihin on annettava oikea vastaus ennen kuin pelaaja pääsee eteenpäin. Tee luokka Kysymys seuraavasti:

Luokkaa voi testata seuraavalla pääohjelmalla:

    Scanner lukija = new Scanner(System.in);

    Kysymys alku = new Kysymys("Minä vuonna Javan ensimmäinen versio julkaistiin?", "1995");
    Valivaihe hyva = new Valivaihe("Hyvä! Lisätietoa: Javan alkuperäinen ideoija on James Gosling.");

    alku.asetaSeuraava(hyva);

    new Kayttoliittyma(lukija, alku).start();
Minä vuonna Javan ensimmäinen versio julkaistiin?
> 2000

Minä vuonna Javan ensimmäinen versio julkaistiin?
> 1995

Hyvä! Lisätietoa: Javan alkuperäinen ideoija on James Gosling.
(jatka painamalla enteriä)
>

Monivalintakysymykset

Tällä hetkellä tekstiseikkailu tukee välivaiheita ja yksinkertaisia kysymyksiä. Tekstiseikkailu on siis lineaarinen, eli lopputulokseen ei voi käytännössä vaikuttaa. Lisätään seikkailuun monivalintakysymyksiä, joiden avulla pelin kehittäjä voi luoda vaihtoehtoista toimintaa.

Esimerkki vaihtoehtoisesta toiminnasta:

Kello on 13:37 ja päätät mennä syömään. Minne menet?
1. Exactumiin
2. Chemicumiin
> 1

Ruoka on loppu :(
(jatka painamalla enteriä)
>
Kello on 13:37 ja päätät mennä syömään. Minne menet?
1. Exactumiin
2. Chemicumiin
> 2

Mainio valinta!
(jatka painamalla enteriä)
>

Toteuta luokka Monivalinta, jonka API on seuraavanlainen

Testaa ohjelmasi toimintaa seuraavalla pääohjelmalla:

    Scanner lukija = new Scanner(System.in);

    Monivalinta lounas = new Monivalinta("Kello on 13:37 ja päätät mennä syömään. Minne menet?");
    Monivalinta chemicum = new Monivalinta("Lounasvaihtoehtosi ovat seuraavat:");

    Valivaihe exactum = new Valivaihe("Exactumista on kaikki loppu, joten menet Chemicumiin.");

    exactum.asetaSeuraava(chemicum);

    lounas.lisaaVaihtoehto("Exactumiin", exactum);
    lounas.lisaaVaihtoehto("Chemicumiin", chemicum);

    Valivaihe nom = new Valivaihe("Olipas hyvää");

    chemicum.lisaaVaihtoehto("Punajuurikroketteja, ruohosipuli-soijajogurttikastiketta", nom);
    chemicum.lisaaVaihtoehto("Jauhelihakebakot, paprikakastiketta", nom);
    chemicum.lisaaVaihtoehto("Mausteista kalapataa", nom);

    new Kayttoliittyma(lukija, lounas).start();
Kello on 13:37 ja päätät mennä syömään. Minne menet?
1. Exactumiin
2. Chemicumiin
> 1

Exactumista on kaikki loppu, joten menet Chemicumiin.
(jatka painamalla enteriä)
>

Lounasvaihtoehtosi ovat seuraavat:
1. Punajuurikroketteja, ruohosipuli-soijajogurttikastiketta
2. Jauhelihakebakot, paprikakastiketta
3. Mausteista kalapataa
> 2

Olipas hyvää
(jatka painamalla enteriä)
>

Luokan Monivalinta sisäinen toteutus saattaa olla haastava. Kannattaa esimerkiksi käyttää listaa vastausvaihtoehtojen (characterStringjen) tallentamiseen, ja hajautustaulua kohtien tallentamiseen valintavaihtoehdon indeksillä.

Tilastot kuntoon

NHL:ssä pidetään pelaajista yllä monenlaisia tilastotietoja. Teemme nyt oman ohjelman NHL-pelaajien tilastojen hallintaan.

Pelaajalistan tulostus

Tee luokka Pelaaja, johon voidaan tallettaa pelaajan nimi, joukkue, pelatut ottelut, maalimäärä, ja syöttömäärä. Luokalla tulee olla konstruktori, joka saa edellämainitut tiedot edellä annetussa järjestyksessä.

Tee kaikille edelläminituille arvoille myös ns. getterimetodit, jotka palauttavat arvot:

Talleta seuraavat pelaajat ArrayList:iin ja tulosta listan sisältö:

    public static void main(String[] args) {
        ArrayList<Pelaaja> pelaajat = new ArrayList<Pelaaja>();
        pelaajat.add(new Pelaaja("Alex Ovechkin", "WSH", 71, 28, 46));
        pelaajat.add(new Pelaaja("Dustin Byfuglien", "ATL", 69, 19, 31));
        pelaajat.add(new Pelaaja("Phil Kessel", "TOR", 70, 28, 24));
        pelaajat.add(new Pelaaja("Brendan Mikkelson", "ANA, CGY", 23, 0, 2));
        pelaajat.add(new Pelaaja("Matti Luukkainen", "SaPKo", 1, 0, 0 ));

        for (Pelaaja pelaaja : pelaajat) {
            System.out.println(pelaaja);
        }
    }

Pelaajan toString()-metodin muodostaman tulostuksen tulee olla seuraavassa muodossa:

Alex Ovechkin WSH 71 28 + 46 = 74
Dustin Byfuglien ATL 69 19 + 31 = 50
Phil Kessel TOR 70 28 + 24 = 52
Brendan Mikkelson ANA, CGY 23 0 + 2 = 2
Matti Luukkainen SaPKo 1 0 + 0 = 0

Ensin siis nimi, sitten joukkue, jonka jälkeen ottelut, maalit, plusmerkki, syötöt, yhtäsuuruusmerkki ja kokonaispisteet eli maalien ja syöttöjen summa.

Tulostuksen siistiminen

Tee Pelaaja-luokkaan metodi toSiisticharacterString(), joka palauttaa samat tiedot siististi aseteltuna siten, että jokaiselle muuttujalle on varattu tietty määrä tilaa tulostuksessa.

Tulostuksen tulee näyttää seuraavalta:

Alex Ovechkin             WSH             71  28 + 46 = 74
Dustin Byfuglien          ATL             69  19 + 31 = 50
Phil Kessel               TOR             70  28 + 24 = 52
Brendan Mikkelson         ANA, CGY        23   0 +  2 =  2
Matti Luukkainen          SaPKo            1   0 +  0 =  0

Nimen jälkeen joukkueen nimien täytyy alkaa samasta kohdasta. Saat tämän aikaan esim. muotoilemalla nimen tulostuksen yhteydessä seuraavasti:

String nameJaTyhjaa = String.format("%-25s", nimi);

Komento tekee characterStringn nimiJaTyhjaa joka alkaa characterStringn nimi sisällöllä ja se jälkeen tulee välilyöntejä niin paljon että characterStringn pituudeksi tulee 25. Joukkueen nimi tulee vastaavalla tavalla tulostaa 14 merkin pituisena characterStringna. Tämän jälkeen on otteluiden määrä (2 merkkiä), jota seuraa 2 välilyöntiä. Tämän jälkeen on maalien määrä (2 merkkiä), jota seuraa characterString " + ". Tätä seuraa syöttöjen määrä (2 merkkiä), characterString " = ", ja lopuksi yhteispisteet (2 merkkiä).

Lukuarvot eli ottelu-, maali-, syöttö- ja pistemäärä muotoillaan kahden merkin mittaisena, eli lukeman 0 sijaan tulee tulostua välilyönti ja nolla. Seuraava komento auttaa tässä:

String maalitMerkkeina = String.format("%2d", maalit);

Pistepörssin tulostus

Lisää luokalle Pelaaja rajapinta Comparable<Pelaaja>, jonka avulla pelaajat voidaan järjestää kokonaispistemäärän mukaiseen laskevaan järjestykseen. Järjestä pelaajat Collections-luokan avulla ja tulosta pistepörssi:

        Collections.sort(pelaajat);

        System.out.println("NHL pistepörssi:\n");
        for (Pelaaja pelaaja : pelaajat) {
            System.out.println(pelaaja);
        }

Prints:

NHL pistepörssi:

Alex Ovechkin             WSH           71  28 + 46 = 74
Phil Kessel               TOR           70  28 + 24 = 52
Dustin Byfuglien          ATL           69  19 + 31 = 50

Ohjeita tähän tehtävään materiaalissa.

Kaikkien pelaajien tiedot

Tilastomme on vielä hieman vajavainen, siinä on vaan muutaman pelaajan tiedot (ja nekin vastaavat 16.3. tilannetta). Kaikkien tietojen syöttäminen käsin olisi kovin vaivalloista. Onneksemme internetistä osoitteesta http://nhlstatistics.herokuapp.com/players.txt löytyy päivittyvä, koneen luettavaksi tarkoitettu lista pelaajatiedoista.

Huom: kun menet osoitteeseen ensimmäistä kertaa, sivun latautuminen kestää muutaman sekunnin (sivu pyörii virtuaalipalvelimella joka sammutetaan jos sivua ei ole hetkeen käytetty). Sen jälkeen sivu toimii nopeasti.

Datan lukeminen internetistä on helppoa. Projektissasi on valmiina luokka Tilasto, joka lataa annetun verkkosivun.

import java.io.InputStream;
import java.net.URL;
import java.util.Scanner;

public class Tilasto {
    private static final String OSOITE =
            "http://nhlstatistics.herokuapp.com/players.txt";

    private Scanner lukija;

    public Tilasto() {
        this(OSOITE);
    }

    public Tilasto(String osoite) {
        try {
            URL url = new URL(osoite);
            lukija = new Scanner(url.openStream());
        } catch (Exception ex) {
        }
    }

    public Tilasto(InputStream in) {
        try {
            lukija = new Scanner(in);
        } catch (Exception ex) {
        }
    }

    public boolean onkoRivejaJaljella() {
        return reader.hasNextLine();
    }

    public String annaSeuraavaRivi() {
        String rivi = reader.nextLine();
        return rivi.trim();
    }
}

Tilasto-luokka lukee pelaajien tilastotiedot internetistä. Metodilla annaSeuraavaRivi() saadaan selville yhden pelaajan tiedot. Tietoja on tarkoitus lukea niin kauan kuin pelaajia riittää, tämä voidaan tarkastaa metodilla onkoRivejaJaljella()

Kokeile että ohjelmasi onnistuu tulostamaan Tilasto-luokan hakemat tiedot:

    public static void main(String[] args) {
        Tilasto tilasto = new Tilasto();

        while (tilasto.onkoRivejaJaljella()) {
            String pelaajaRivina = tilasto.annaSeuraavaRivi();
            System.out.println(pelaajaRivina);
        }
    }

Tulostus on seuraavan muodoinen:

Evgeni Malkin;PIT;62;39;46;54 
Steven Stamkos;TBL;70;50;34;64
Claude Giroux;PHI;66;26;56;27
Jason Spezza;OTT;72;29;46;30
// ... ja yli 800:n muun pelaajan tiedot

Huom: tulostuksen alussa ja lopussa ja jokaisen pelaajan välissä on html-tägejä, esim. <br/> joka aiheuttaa www-sivulle rivin vaihtumisen.

Tulostuksessa pelaajan tiedot on erotettu toisistaan puolipisteellä. Ensin nimi, sitten joukkue, ottelut, maalit, syötöt ja laukaukset.

Pelaajaa vastaava characterString on siis yksittäinen characterString. Saat pilkottua sen osiin split-komennolla seuraavasti:

        while (tilasto.onkoRivejaJaljella()) {
            String pelaajaRivina = tilasto.annaSeuraavaRivi();
            String[] pelaajaOsina = pelaajaRivina.split(";");
            for (int j = 0; j < pelaajaOsina.length; j++) {
                System.out.print(pelaajaOsina[j] + " ");
            }
            System.out.println("");
        }

Kokeile että tämä toimii. Saat tästä tehtävästä pisteet seuraavan tehtävän yhteydessä.

Kaikkien pelaajien pistepörssi

Tee kaikista Tilasto-luokan hakemien pelaajien tiedoista Pelaaja-olioita ja lisää ne ArrayListiin. Lisää tehtävään luokka PelaajatTilastosta. Käytä alla olevaa koodia luokan runkona.

import java.util.ArrayList;

public class PelaajatTilastosta {
    public ArrayList<Pelaaja> haePelaajat(Tilasto tilasto) {
        ArrayList<Pelaaja> pelaajat = new ArrayList<Pelaaja>();
        while (tilasto.onkoRivejaJaljella()) {
            String pelaajaRivina = tilasto.annaSeuraavaRivi();
            String[] pelaajaOsina = pelaajaRivina.split(";");

            // Lisätään uusi pelaaja vain, jos syötteessä on kenttiä riittävästi
            if (pelaajaOsina.length > 4) {
                int ottelut = Integer.parseInt(pelaajaOsina[2].trim());
                // Täydennä koodia lukemalla kaikki pelaajaOsina-taulukon kentät uuteen Pelaaja-olioon
                // ...
                // pelaajat.add(new Pelaaja( ... ));
            }
        }

        return pelaajat;
    }
}

Tehtävänäsi on täydentää runkoa siten, että jokaisesta luetusta rivistä luodaan pelaaja, joka lisätään pelaajat-listaan. Huom! Tilasto-luokka palauttaa characterStringja, joten joudut muuntamaan characterStringja myös numeroiksi. Esimerkiksi numeromuotoinen ottelut on muutettava int:iksi Integer.parseInt-metodilla.

Jos characterStringn metodi split ei ole tuttu, se jakaa characterStringn useampaan osaan annetun merkin kohdalta. Esimerkiksi komento characterString.split(";"); palauttaa characterStringsta taulukon, jossa alkuperäisen characterStringn puolipisteellä erotetut osat ovat kukin omassa taulukon indeksissä.

Voit käyttää testauksen apuna seuraavaa pääohjelmaa:

        Tilasto tilasto = new Tilasto();

        PelaajatTilastosta pelaajienHakija = new PelaajatTilastosta();
        ArrayList<Pelaaja> pelaajat = pelaajienHakija.haePelaajat(tilasto);

        for (Pelaaja pelaaja : pelaajat) {
            System.out.println( pelaaja );
        }

Maali ja syöttöpörssi

Haluamme tulostaa myös maalintekijäpörssin eli pelaajien tiedot maalimäärän mukaan järjestettynä sekä syöttöpörssin. NHL:n kotisivu tarjoaa tämänkaltaisen toiminnallisuuden, eli selaimessa näytettävä lista on mahdollista saada järjestettyä halutun kriteerin mukaan.

Edellinen tehtävä määritteli pelaajien suuruusjärjestyksen perustuvan kokonaispistemäärään. Luokalla voi olla vain yksi compareTo-metodi, joten joudumme muunlaisia järjestyksiä saadaksemme turvautumaan muihin keinoihin.

Vaihtoehtoiset järjestämistavat toteutetaan erillisten luokkien avulla. Pelaajien vaihtoehtoisten järjestyksen määräävän luokkien tulee toteuttaa Comparator<Pelaaja>-rajapinta. Järjestyksen määräävän luokan olio vertailee kahta parametrina saamaansa pelaajaa. Metodeja on ainoastaan yksi compare(Pelaaja p1, Pelaaja p2), jonka tulee palauttaa negatiivinen arvo, jos pelaaja p1 on järjestyksessä ennen pelaajaa p2, positiivinen arvo jos p2 on järjestyksessä ennen p1:stä ja 0 muuten.

Periaatteena on luoda jokaista järjestämistapaa varten oma vertailuluokka, esim. maalipörssin järjestyksen määrittelevä luokka:

import java.util.Comparator;

public class Maali implements Comparator<Pelaaja> {
    public int compare(Pelaaja p1, Pelaaja p2) {
        // maalien perusteella tapahtuvan vertailun koodi tänne
    }
}

Tee Comparator-rajapinnan toteuttavat luokat Maali ja Syotto, ja niille vastaavat maali- ja syöttöpörssien generoimiseen sopivat sopivat vertailufunktiot.

Järjestäminen tapahtuu edelleen luokan Collections metodin sort avulla. Metodi saa nyt toiseksi parametrikseen järjestyksen määräävän luokan olion:

Maali maalintekijat = new Maali();
Collections.sort(pelaajat, maalintekijat);
System.out.println("NHL parhaat maalintekijät\n");

// tulostetaan maalipörssi

Järjestyksen määrittelevä olio voidaan myös luoda suoraan sort-kutsun yhteydessä:

Collections.sort(pelaajat, new Maali());
System.out.println("NHL parhaat maalintekijät\n");

// tulostetaan maalipörssi

Kun sort-metodi saa järjestyksen määrittelevän olion parametrina, se käyttää olion compareTo()-metodia pelaajia järjestäessään.

Olioiden samuus

pois?

Kaikki oliot ovat tyyppiä Object, joten minkä tahansa tyyppisen olion voi antaa parametrina Object-tyyppisiä parametreja vastaanottavalle metodille.

Tehtävän mukana tulee rajapinta Vertaaja. Toteuta pakkaukseen samuus luokka OlioidenVertaaja, joka toteuttaa rajapinnan Vertaaja. Metodien tulee toimia seuraavasti:

Tehtävän mukana tulee luokka Person, jossa equals- ja compareTo-metodit on korvattu. Kokeile toteuttamiesi metodien toimintaa seuraavalla esimerkkikoodilla.

   OlioidenVertaaja vertaaja = new OlioidenVertaaja();
   Person henkilo1 = new Person("221078-123X", "Pekka", "Helsinki");
   Person henkilo2 = new Person("221078-123X", "Pekka", "Helsinki");  // täysin samansisältöinen kuin eka
   Person henkilo3 = new Person("110934-123X", "Pekka", "Helsinki");  // eri pekka vaikka asuukin helsingissä

   System.out.println(vertaaja.samaOlio(henkilo1, henkilo1));
   System.out.println(vertaaja.samaOlio(henkilo1, henkilo2));
   System.out.println(vertaaja.vastaavat(henkilo1, henkilo2));
   System.out.println(vertaaja.vastaavat(henkilo1, henkilo3));
   System.out.println(vertaaja.samacharacterStringEsitys(henkilo1, henkilo2));

   Person henkilo4 = new Person("221078-123X", "Pekka", "Savonlinna"); // henkilo1:n pekka mutta asuinpaikka muuttuu

   System.out.println(vertaaja.samaOlio(henkilo1, henkilo4));
   System.out.println(vertaaja.vastaavat(henkilo1, henkilo4));
   System.out.println(vertaaja.samacharacterStringEsitys(henkilo1, henkilo4));

Ylläolevan koodin tulostuksen pitäisi olla seuraava:

true
false
true
false
true
false
true
false

Kuviot

Tehtäväpohjan mukana tulee luokat Ympyra, Suorakulmio ja TasasivuinenKolmio. Luokat liittyvät samaan aihepiiriin, ja niillä on hyvin paljon yhteistä toiminnallisuutta. Tutustu luokkiin ennenkuin lähdet tekemään, jolloin hahmotat tarkemmin syyt muutoksille. Jos huomaat että luokissa on alustavasti sisennys hieman pielessä, kannattaa sisennys hoitaa kuntoon luettavuuden helpottamiseksi.

Kuvio

Toteuta pakkaukseen kuviot abstrakti luokka Kuvio, jossa on kuvioihin liittyvää toiminnallisuutta. Luokan kuvio tulee sisältää konstruktori public Kuvio(int x, int y), metodit public int getX(), public int getY(), sekä abstraktit metodit public abstract double pintaAla() ja public abstract double piiri().

Ympyra perii kuvion

Muuta luokan Ympyra toteutusta siten, että se perii luokan Kuvio. Luokan Ympyra ulkoinen toiminnallisuus ei saa muuttua, eli sen tulee tarjota samat metodit kuin aiemminkin -- joko luokan Kuvio avulla tai itse. Muistathan että konstruktorikutsun super avulla voit käyttää yliluokan konstruktoria. Kun metodi public int getX() on toteutettu jo yliluokassa se ei tarvitse erillistä toteutusta luokassa Ympyra.

        Kuvio kuvio = new Ympyra(10, 10, 15);
        System.out.println("X " + kuvio.getX());
        System.out.println("Y " + kuvio.getY());
        System.out.println("Pinta-ala " + kuvio.pintaAla());
        System.out.println("Piiri " + kuvio.piiri());
X 10
Y 10
Pinta-ala 706.85834...
Piiri 94.24777...

Suorakulmio ja Tasakylkinen kolmio perii kuvion

Muuta luokkien Suorakulmio ja TasakylkinenKolmio toteutusta siten, että ne perivät luokan Kuvio. Luokkien ulkoinen toiminnallisuus ei saa muuttua, eli niiden tulee tarjota samat metodit kuin aiemminkin -- joko luokan Kuvio avulla tai itse.

        Kuvio kuvio = new Suorakulmio(10, 10, 15, 15);
        System.out.println("X " + kuvio.getX());
        System.out.println("Y " + kuvio.getY());
        System.out.println("Pinta-ala " + kuvio.pintaAla());
        System.out.println("Piiri " + kuvio.piiri());
        System.out.println("");

        kuvio = new TasakylkinenKolmio(10, 10, 15);
        System.out.println("X " + kuvio.getX());
        System.out.println("Y " + kuvio.getY());
        System.out.println("Pinta-ala " + kuvio.pintaAla());
        System.out.println("Piiri " + kuvio.piiri());
X 10
Y 10
Pinta-ala 225.0
Piiri 60.0

X 10
Y 10
Pinta-ala 97.42785...
Piiri 45.0

TreeMap

Joukkojen järjestyksessä pitäminen onnistuu Set rajapinnan toteuttavan TreeSet-olion avulla. Aiemmassa Tehtavakirjanpito-esimerkissä henkilökohtaiset tehtäväpisteet tallennettiin Map-rajapinnan toteuttavaan HashMap-olioon. Kuten HashSet, HashMap ei pidä alkioita järjestyksessä. Rajapinnasta Map on olemassa toteutus TreeMap, jossa hajautustaulun avaimia pidetään järjestyksessä. Muutetaan Tehtavakirjanpito-luokkaa siten, että henkilökohtaiset pisteet tallennetaan TreeMap-tyyppiseen hajautustauluun.

public class Tehtavakirjanpito {
    private Map<String, Set<Integer>> tehdytTehtavat;

    public Tehtavakirjanpito() {
        this.tehdytTehtavat = new TreeMap<String, Set<Integer>>();
    }

    public void lisaa(String kayttaja, int tehtava) {
        if (!this.tehdytTehtavat.containsKey(kayttaja)) {
            this.tehdytTehtavat.put(kayttaja, new TreeSet<Integer>());
        }

        Set<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja);
        tehdyt.add(tehtava);
    }

    public void tulosta() {
        for (String kayttaja: this.tehdytTehtavat.keySet()) {
            System.out.println(kayttaja + ": " + this.tehdytTehtavat.get(kayttaja));
        }
    }
}

Muunsimme samalla Set-rajapinnan toteutukseksi TreeSet-luokan. Huomaa että koska olimme käyttäneet rajapintoja, muutoksia tuli hyvin pieneen osaan koodista. Etsi kohdat jotka muuttuivat!

Käyttäjäkohtaiset tehtävät voidaan nyt tulostaa järjestyksessä.

        Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito();
        kirjanpito.lisaa("Mikael", 3);
        kirjanpito.lisaa("Mikael", 4);
        kirjanpito.lisaa("Mikael", 3);
        kirjanpito.lisaa("Mikael", 3);

        kirjanpito.lisaa("Pekka", 4);
        kirjanpito.lisaa("Pekka", 4);

        kirjanpito.lisaa("Matti", 1);
        kirjanpito.lisaa("Matti", 2);

        kirjanpito.tulosta();
Matti: [1, 2]
Mikael: [3, 4]
Pekka: [4]

Luokka TreeMap vaatii että avaimena käytetyn luokan tulee toteuttaa Comparable-rajapinta. Jos luokka ei toteuta rajapintaa Comparable, voidaan luokalle TreeMap antaa konstruktorin parametrina Comparator-luokan toteuttama olio aivan kuten TreeSet-luokalle.

Sähköposteja

Tehtävänäsi on toteuttaa sähköpostiohjelmaan komponentti, joka säilöö viestejä. Tehtäväpohjan mukana tulee luokka Sahkoposti, joka esittää sähköpostiviestiä. Luokalla Sahkoposti on oliomuuttujat:

Toteutetaan tässä luokka Viestivarasto, joka tarjoaa sähköpostien hallintaan liittyviä toimintoja.

Viestivarasto, lisääminen ja hakeminen

Luo pakkaukseen posti luokka Viestivarasto, ja lisää sille seuraavat metodit:

Voit olettaa että millään kahdella viestillä ei ole samaa otsikkoa.

Ajan perusteella hakeminen

Lisää luokkaan Viestivarasto seuraavat metodit

Huom! Kannattaa käyttää kahta erillistä rakennetta viestien tallentamiseen. Otsikon perusteella tallentamiseen voit käyttää HashMappia, ja viestien tallentamiseen ajan mukaan TreeMappia. Näin saat toteutettua hae-operaatiot tehokkaasti. Tutustu myös TreeMapin metodeihin lastKey() ja floorKey().

Ohjelmien automaattinen testaaminen

Errare humanum est

Ihminen on erehtyväinen ja paraskin ohjelmoija tekee virheitä. Ohjelman kehitysvaiheessa tapahtuvien virheiden lisäksi huomattava osa virheistä syntyy olemassa olevaa ohjelmaa muokattaessa. Ohjelman muokkauksen aikana tehdyt virheet eivät välttämättä näy muokattavassa osassa, vaan voivat ilmaantua välillisesti erillisessä osassa ohjelmaa: osassa, joka käyttää muutettua osaa.

Ohjelmien automaattinen testaaminen tarkoittaa toistettavien testien luomista. Testeillä varmistetaan että ohjelma toimii halutusti, ja että ohjelma säilyttää toiminnallisuutensa myös muutosten jälkeen. Sanalla automaattinen painotetaan sitä, että luodut testit ovat toistettavia ja että ne voidaan suorittaa aina haluttaessa -- ohjelmoijan ei tarvitse olla läsnä testejä suoritettaessa.

Otimme aiemmin askeleita kohti testauksen automatisointia antamalla Scanner-oliolle parametrina characterStringn, jonka se tulkitsee käyttäjän näppäimistöltä antamaksi syötteeksi. Automaattisessa testaamisessa testaaminen viedään viedä pidemmälle: koneen tehtävänä on myös tarkistaa että ohjelman tuottama vastaus on odotettu.

Automaattisen testauksen tällä kurssilla painotettu osa-alue on yksikkötestaus, jossa testataan ohjelman pienten osakokonaisuuksien -- metodien ja luokkien -- toimintaa. Yksikkötestaamiseen käytetään Javalla yleensä JUnit-testauskirjastoa.

Pino ja automaattiset testit

Pino on kaikille ihmisille tuttu asia. Esimerkiksi ravintola Unicafessa lautaset ovat yleensä pinossa. Pinon päältä voi ottaa lautasen ja pinon päälle voi lisätä lautasia. On myös helppo selvittää onko pinossa vielä lautasia jäljellä.

Pino on myös ohjelmoinnissa usein käytetty aputietorakenne. Rajapintana lukuja sisältävä pino näyttää seuraavalta.

public interface Pino {
    boolean tyhja();
    boolean taynna();
    void pinoon(int luku);
    int pinosta();
    int huipulla();
    int lukuja();
}

Rajapinnan määrittelemien metodien on tarkoitus toimia seuraavasti:

Toteutetaan rajapinnan Pino toteuttava luokka OmaPino, johon talletetaan lukuja. Pinoon mahtuvien lukujen määrä annetaan pinon konstruktorissa. Toteutamme pinon hieman aiemmasta poikkeavasti -- emme testaa ohjelmaa pääohjelman avulla, vaan käytämme pääohjelman sijasta automatisoituja JUnit-testejä ohjelman testaamiseen.

Tutustuminen JUnitiin

NetBeansissa olevat ohjelmamme ovat tähän asti sijainneet aina Source Packagesissa tai sen sisällä olevissa pakkauksissa. Ohjelman lähdekoodit tulevat aina kansioon Source Packages. Automaattisia testejä luodessa testit luodaan valikon Test Packages alle. Uusia JUnit-testejä voi luoda valitsemalla projektin oikealla hiirennapilla ja valitsemalla avautuvasta valikosta New -> JUnit Test.... Jos vaihtoehto JUnit test ei näy listassa, löydät sen valitsemalla Other.

JUnit-testit sijaitsevat luokassa. Uutta testitiedostoa luodessa ohjelma pyytää testitiedoston nimen. Tyypillisesti nimeksi annetaan testattavan luokan tai toiminnallisuuden nimi. Luokan nimen tulee aina päättyä sanaan Test. Esimerkiksi alla luodaan testiluokka PinoTest, joka sijaitsee pakkauksessa pino. NetBeans haluaa luoda käyttöömme myös valmista runkoa testiluokalle -- joka käy hyvin.

Jos NetBeans kysyy minkä JUnit-version haluat käyttöösi, valitse JUnit 4.x.

Kun testiluokka PinoTest on luotu, näkyy se projektin valikon Test Packages alla.

Luokka PinoTest näyttää aluksi seuraavalta

package pino;

import org.junit.*;
import static org.junit.Assert.*;

public class PinoTest {

    public PinoTest() {
    }

    @BeforeClass
    public static void setUpClass() throws Exception {
    }

    @AfterClass
    public static void tearDownClass() throws Exception {
    }

    @Before
    public void setUp() {
    }

    @After
    public void tearDown() {
    }
    // TODO add test methods here.
    // The methods must be annotated with annotation @Test. For example:
    //
    // @Test
    // public void hello() {}
}

Meille oleellisia osia luokassa PinoTest ovat metodit public void setUp, jonka yläpuolella on merkintä @Before, ja kommentoitu metodipohja public void hello(), jonka yläpuolella on merkintä @Test. Metodit, joiden yläpuolella on merkintä @Test ovat ohjelman toiminnallisuutta testaavia testimetodeja. Metodi setUp taas suoritetaan ennen jokaista testiä.

Muokataan luokkaa PinoTest siten, että sillä testataan rajapinnan Pino toteuttamaa luokkaa OmaPino. Älä välitä vaikkei luokkaa OmaPino ole vielä luotu. Pino on testiluokan oliomuuttuja, joka alustetaan ennen jokaista testiä metodissa setUp.

package pino;

import org.junit.*;
import static org.junit.Assert.*;

public class PinoTest {

    Pino pino;

    @Before
    public void setUp() {
        pino = new OmaPino(3);
    }

    @Test
    public void alussaTyhja() {
        assertTrue(pino.tyhja());
    }

    @Test
    public void lisayksenJalkeenEiTyhja() {
        pino.pinoon(5);
        assertFalse(pino.tyhja());
    }

    @Test
    public void lisattyAlkioTuleePinosta() {
        pino.pinoon(3);
        assertEquals(3, pino.pinosta());
    }

    @Test
    public void lisayksenJaPoistonJalkeenPinoOnTaasTyhja() {
        pino.pinoon(3);
        pino.pinosta();
        assertTrue(pino.tyhja());
    }

    @Test
    public void lisatytAlkiotTulevatPinostaOikeassaJarjestyksessa() {
        pino.pinoon(1);
        pino.pinoon(2);
        pino.pinoon(3);

        assertEquals(3, pino.pinosta());
        assertEquals(2, pino.pinosta());
        assertEquals(1, pino.pinosta());
    }

    @Test
    public void tyhjennyksenJalkeenPinoonLaitettuAlkioTuleeUlosPinosta() {
        pino.pinoon(1);
        pino.pinosta();

        pino.pinoon(5);

        assertEquals(5, pino.pinosta());
    }

    // ...
}

Jokainen testi, eli merkinnällä @Test varustettu metodi, alkaa tilanteesta, jossa on luotu uusi tyhjä pino. Jokainen yksittäinen @Test-merkitty metodi on oma testinsä. Yksittäisellä testimetodilla testataan aina yhtä pientä osaa pinon toiminnallisuudesta. Testit suoritetaan toisistaan täysin riippumattomina, eli jokainen testi alkaa "puhtaaltä pöydältä", setUp-metodin alustamasta tilanteesta.

Yksittäiset testit noudattavat aina samaa kaavaa. Ensin luodaan tilanne jossa tapahtuvaa toimintoa halutaan testata, sitten tehdään testattava toimenpide, ja lopuksi tarkastetaan onko tilanne odotetun kaltainen. Esimerkiksi seuraava testi testaa että lisäyksen ja poiston jälkeen pino on taas tyhjä -- huomaa myös kuvaava testimetodin nimentä:

    @Test
    public void lisayksenJaPoistonJalkeenPinoOnTaasTyhja() {
        pino.pinoon(3);
        pino.pinosta();
        assertTrue(pino.tyhja());
    }

Ylläoleva testi testaa toimiiko metodi tyhja() jos pino on tyhjennetty. Ensin laitetaan pinoon luku metodilla pinoon, jonka jälkeen pino tyhjennetään kutsumalla metodia pinosta(). Tällöin on saatu aikaan tilanne jossa pinon pitäisi olla tyhjennetty. Viimeisellä rivillä testataan, että pinon metodi tyhja() palauttaa arvon true testausmetodilla assertTrue(). Jos metodi tyhja() ei palauta arvoa true näemme testejä suorittaessa virheen.

Jokainen testi päättyy jonkun assert-metodin kutsuun. Esimerkiksi metodilla assertEquals() voidaan varmistaa onko metodin palauttama luku tai characterString haluttu, ja metodilla assertTrue() varmistetaan että metodin palauttama arvo on true. Erilaiset assert-metodit saadaan käyttöön luokan alussa olevalla määrittelyllä import static org.junit.Assert.*;.

Testit suoritetaan joko painamalla alt ja F6 tai valitsemalla Run -> Test project. (Macintosh-koneissa tulee painaa ctrl ja F6). Punainen väri ilmaisee että testin suoritus epäonnistui -- testattava toiminnallisuus ei toiminut kuten toivottiin. Vihreä väri kertoo että testin testaama toiminnallisuus toimi kuten haluttiin.

Luokan OmaPino toteutus

Pinon toteuttaminen testien avulla tapahtuisi askel kerrallaan siten, että lopulta kaikki testit toimivat. Ohjelman rakentaminen aloitetaan yleensä hyvin varovasti. Rakennetaan ensin luokka OmaPino siten, että ensimmäinen testi alussaTyhja alkaa toimimaan. Älä tee mitään kovin monimutkaista, "quick and dirty"-ratkaisu kelpaa näin alkuun. Kun testi menee läpi (eli näyttää vihreää), siirry ratkaisemaan seuraavaa kohtaa.

Testi alussaTyhja menee läpi aina kun palautamme arvon true metodista tyhja.

package pino;

import java.util.ArrayList;
import java.util.List;

public class OmaPino implements Pino {

    public OmaPino(int maksimikoko) {
    }

    @Override
    public boolean tyhja() {
        return true;
    }

    // tyhjät metodirungot

Siirrytään ratkaisemaan kohtaa lisayksenJalkeenEiTyhja. Tarvitsemme toteutuksen metodille pinoon. Yksi lähestymistapa on muokata luokkaa OmaPino siten, että se sisältää taulukon. Taulukkoa käytetään, että pinottavat values talletetaan pinon taulukkoon yksi kerrallaan. Seuraava kuvasarja selkeyttää taulukossa olevien alkioiden pinoon laittamista ja pinosta ottamista.

pino = new OmaPino(4);

  0   1   2   3
-----------------
|   |   |   |   |
-----------------
alkioita: 0

pino.pinoon(5);

  0   1   2   3
-----------------
| 5 |   |   |   |
-----------------
alkiota: 1

pino.pinoon(3);

  0   1   2   3
-----------------
| 5 | 3 |   |   |
-----------------
alkiota: 2

pino.pinoon(7);

  0   1   2   3
-----------------
| 5 | 3 | 7 |   |
-----------------
alkiota: 3

pino.pinosta();

  0   1   2   3
-----------------
| 5 | 3 |   |   |
-----------------
alkiota: 2

Ohjelman tulee siis muistaa kuinka monta alkiota pinossa on. Uusi alkio laitetaan jo pinossa olevien perään. Alkion poisto aiheuttaa sen, että taulukon viimeinen käytössä ollut paikka vapautuu ja alkiomäärän muistavan muuttujan arvo pienenee.

Luokan OmaPino toteutusta jatketaan askel kerrallaan kunnes kaikki testit menevät läpi. Jossain vaiheessa ohjelmoija todennäköisesti huomaisi, että taulukko kannattaa vaihtaa ArrayList-rakenteeksi.

Huomaat todennäköisesti ylläolevan esimerkin luettuasi että olet jo tehnyt hyvin monta testejä käyttävää ohjelmaa. Osa TMC:n toiminnallisuudesta rakentuu JUnit-testien varaan, ongelmat ovat varsinkin kurssin alkupuolella pilkottu pieniin testeihin, joiden avulla ohjelmoijaa on ohjattu eteenpäin. TMC:n mukana tulevat testit ovat kuitenkin usein monimutkaisempia kuin ohjelmien normaalissa automaattisessa testauksessa niiden tarvitsee olla. TMC:ssä ja kurssilla käytettävien testien kirjoittajien tulee muunmuassa varmistaa luokkien olemassaolo, jota normaalissa automaattisessa testauksessa harvemmin tarvitsee tehdä.

Harjoitellaan seuraavaksi ensin testien lukemista, jonka jälkeen kirjoitetaan muutama testi.

Tehtävälista

Tehtäväpohjassa on rajapinnan Tehtavalista toteuttava luokka MuistiTehtavalist. Ohjelmaa varten on koodattu valmiiksi testit, joita ohjelma ei kuitenkaan läpäise. Tehtävänäsi on tutustua testiluokkaan TehtavalistaTest, ja korjata luokka MuistiTehtavalista siten, että ohjelman testit menevät läpi.

Huom! Tässä tehtävässä sinun ei tarvitse koskea testiluokkaan TehtavalistaTest.

Lukutilasto

Huom! Tässä tehtävässä on jo mukana testiluokka, johon sinun tulee kirjoittaa lisää testejä. Vastauksen oikeellisuus testataan vasta TMC-palvelimella: tehtävästä saa pisteet vasta kun molemmat tehtävät on suoritettu palvelimella hyväksytysti. Ole tarkka metodien nimennän ja lisättyjen lukujen kanssa.

Tehtävässä tulee pakkauksessa tilasto sijaitseva luokka Lukutilasto.

Testikansiossa olevassa pakkauksessa tilasto on luokka LukutilastoTest, johon sinun tulee lisätä uusia testimetodeja.

Lukujen määrän kasvamisen tarkistus

Lisää testiluokkaan testimetodi public void lukujenMaaraKasvaaKahdellaKunLisataanKaksiLukua(), jossa lukutilastoon lisätään values 3 ja 5. Tämän jälkeen metodissa tarkistetaan että lukutilastossa on kaksi lukua käyttäen lukutilaston metodia lukujenMaara. Käytä Assert-luokan assertEquals-metodia palautettujen arvojen tarkastamiseen.

Summan tarkistus yhdellä luvulla

Lisää testiluokkaan testimetodi public void summaOikeinYhdellaLuvulla(), jossa lukutilastoon lisätään luku 3. Tämän jälkeen metodissa tarkistetaan lukutilaston summa-metodin avulla että tilastossa olevien lukujen summa on 3. Käytä Assert-luokan assertEquals-metodia palautettujen arvojen tarkastamiseen.

Jätkänshakin sovelluslogiikka (pakollinen)

Huom! tämä tehtävä on pakollinen yliopistoon hakeville.

Tämä tehtävä on kolmen yksittäisen tehtäväpisteen arvoinen. Tehtävässä toteutetaan sovelluslogiikka jätkänshakille ja harjoitellaan ohjelmarakenteen osittaista omatoimista suunnittelua.

Tehtäväpohjassa tulee mukana käyttöliittymä jätkänshakille, jossa pelilaudan koko on aina 3x3 ruutua. Käyttöliittymä huolehtii ainoastaan pelilaudalla tehtyihin tapahtumiin reagoimisesta, sekä pelilaudan ja pelitilanteen tietojen päivittämisestä. Pelin logiikka on erotettu JatkanshakinSovelluslogiikka-rajapinnan avulla omaksi luokakseen.

package jatkanshakki.sovelluslogiikka;

public interface JatkanshakinSovelluslogiikka {
    char getNykyinenVuoro();
    int getMerkkienMaara();

    void asetaMerkki(int sarake, int rivi);
    char getMerkki(int sarake, int rivi);

    boolean isPeliLoppu();
    char getVoittaja();
}

Rajapinnan JatkanshakinSovelluslogiikka lisäksi tehtäväpohjassa on apuluokka, joka määrittelee pelilaudan ruutujen mahdolliset tilat char-tyyppisinä kirjaimina. Ruutu voi olla joko tyhjä, tai siinä voi olla risti tai nolla. Apuluokassa Jatkanshakki on näille määrittelyt:

package jatkanshakki.sovelluslogiikka;

public class Jatkanshakki {
    public static final char RISTI = 'X';
    public static final char NOLLA = 'O';
    public static final char TYHJA = ' ';
}

Tehtävänäsi on täydentää pakkauksessa jatkanshakki.sovelluslogiikka olevaa rajapinnan JatkanshakinSovelluslogiikka toteuttavaa luokkaa OmaJatkanshakinSovelluslogiikka. Luokka OmaJatkanshakinSovelluslogiikka mahdollistaa jätkänshakin pelaamisen.

Rajapinta JatkanshakinSovelluslogiikka määrittelee seuraavat toiminnot, jotka luokan OmaJatkanshakinSovelluslogiikka tulee toteuttaa:

Ensimmäinen pelivuoro on aina merkillä RISTI. Pelin voittaa se pelaaja, joka saa ensimmäisenä kolme merkkiä vaakasuoraan, pystysuoraan tai vinottain. Tasapeli todetaan vasta, kun pelilauta on täynnä merkkejä eli tyhjiä ruutuja ei enää ole.

Vinkki: Pelilaudan tilanteen voi esittää esimerkiksi yhdeksän alkion char-taulukolla, jonne talletetaan peliruutujen tilat. Sarakkeen ja rivin perusteella voidaan laskea taulukon indeksi: rivi * 3 + sarake.

Materiaali