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.
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.
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.
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!
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 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());
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.
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.
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.
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!
main
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!
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
.
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.
Create a class Change
, that has the following functionalities:
public Change(char fromCharacter, char toCharacter)
that creates an object that makes changes from character fromCharacter
to toCharacter
public String change(String characterString)
returns the changed version of the given character stringThe 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
Create the class Changer
, with the following functions:
public Changer()
creates a new changerpublic void addChange(Change change)
adds a new Change to the Changerpublic String change(String characterString)
executes all added Changes for the character string in the order of their adding and returns the changed character stringThe 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
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.
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.
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!
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 }
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.
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.
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:
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
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 type | Description | Minimum value | Max value |
---|---|---|---|
int | Integer | -2 147 483 648 (Integer.MIN_VALUE ) | 2 147 483 647 (Integer.MAX_VALUE ) |
long | Long interger | -9 223 372 036 854 775 808 (Long.MIN_VALUE ) | 9 223 372 036 854 775 807 (Long.MAX_VALUE ) |
boolean | Truth value | true or false | |
double | Floating point | Double.MIN_VALUE | Double.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 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 bonusCalculator
and 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:
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.
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.
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.
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 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.
"
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.
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.
In these exercises, we create the classes Thing
, Suitcase
, and Container
, and we train to use objects which contain other objects.
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:
public String getName()
, which returns the thing's namepublic int getWeight()
, which returns the thing's weightpublic 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)
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:
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)
"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)
Add the following methods to Suitcase
:
printThings
, which prints out all the things
inside the suitcasetotalWeight
, which returns the total weight of the things
in your suitcaseBelow, 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.
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)
Create the class Container
, which has the following methods:
public void addSuitcase(Suitcase suitcase)
, which adds the suitcase as a parameter to the containerpublic 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)
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)
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 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
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.
Then, retrieve mikael's nickname and print it.
The tests require you write lower case names.
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!
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.
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-type | Reference-type equivalent |
---|---|
int | Integer |
double | Double |
char | Character |
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; }
Create the class PromissoryNote
with the following functionality:
public PromissoryNote()
creates a new promissory notepublic void setLoan(String toWhom, double value)
which stores the information about loans to specific people.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
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.
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 dictionaryImplement 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
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
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
In this exercise, we also train creating a text user interface. Create the class TextUserInterface
, with the following methods:
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!
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!
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.
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.
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!
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
.
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 equals
method 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
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."); }
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
equals
method in a way that objects with the same content will return true when compared, whereas different-content objects shall return falsehashCode
method in a way that it assigns the same value to all the objects whose content is regarded as similarThe 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.
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
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 anythingpublic 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!
Add still the following methods to your VehicleRegister:
public void printRegistrationPlates()
, which prints out all the registration plates storedpublic 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 carInterface 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.
In the exercise layout, you find the premade interface NationalService
, which contains the following operations:
int getDaysLeft()
which returns the number of days left on servicevoid work()
, which reduces the working days by one. The working days number can not become negative.public interface NationalService { int getDaysLeft(); void work(); }
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.
Create the class MilitaryService
which implements your NationalService
interface. The class constructor has one parameter, defining the days of service (int daysLeft
).
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.
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 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 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!
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.
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.
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?
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?
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.
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
.
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.
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.
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.
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.
Next, we create some programming components which are useful to manage an online shop.
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 valuespublic int price(String product)
returns the price of the parameter product; if the product is not available in the storehouse, the method returns -99Inside 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
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
Let's add another method to our storehouse:
public Set<String> products()
returns a name set of the products contained in the storehouseThe 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
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:
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 pricepublic int price()
, which returns the purchase price. This is obtained by raising the unit amount by the unit pricepublic void increaseAmount()
increases by one the purchase unit amountpublic String toString()
returns the purchase in a string form like the followingAn 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!
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 priceExample 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
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!
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.
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!
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.
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.
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.
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.
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!
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.
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 handpublic void print()
prints the cards in the hand following the below example patternHand 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.
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
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
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.
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
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).
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.
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 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.
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!
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.
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()
.
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.
Create now the package mooc.logic
, and add the class ApplicationLogic
in it. The application logic API should be the following:
public ApplicationLogic(UserInterface ui)
import mooc.ui.UserInterface
must appear at the beginning of he filepublic void execute(int howManyTimes)
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
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.
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 ! } }
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 getID
of 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>.
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!
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.
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.
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)
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)]
Implement now the class Box
in the package moving.domain
. At first, implement the following method for your Box:
public Box(int maximumCapacity)
public boolean addThing(Thing thing)
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.
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 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
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 RuntimeException
s 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
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:
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
.
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
.
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 }
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
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.
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!
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]
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 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.
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
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.
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
In this exercise, we create an application to calculate the number of lines and characters.
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!
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
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.
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.
Create the method public List<String> wordsContainingZ()
, which returns all the file words which contain a z; for instance, jazz and zombie.
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.
Create the method public List<String> palindromes()
, which returns all the palindrome words of the file. Such words are, for instance, ala and enne.
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.
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
.
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.
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)
public Set<String> translate(String word)
Set
object, with all the entries of the word, or a null
reference, if the word is not in the dictionarypublic void remove(String word)
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
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)
public int getNumberOfDetectedDuplicates()
public Set<String> getUniqueCharacterStrings()
Set<String>
. Object should have all unique characterStrings (no duplicates!). If there are no unique characterStrings, method returns an empty set.public void empty()
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: []
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.
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:
to receive two points we also require
to receive three points also
if you want to receive four points, also implement
and to receive all the points:
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:
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.
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
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
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.Objectjava.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
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
.
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:
"x: 3; y: 6"
. Note that the coordinates are separated by a semicolon (;
)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 fiveTry 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
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.
Movable
to the group.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
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.Objectjava.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.
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.
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!
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!"; }
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
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
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
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
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 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.
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) { Listpoints = 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) { Listpoints = 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:
return "("+this.location()+") location "+this.manhattanDistanceFromOrigin();
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...
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.
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.
Together with the exercise, you find the class Container
, with the following constructor and methods:
In this exercise, we create various different containers out of our Container
class. Attention! Create all the classes in the package containers
.
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:
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
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:
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
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:
ContainerHistory
object.Implement analysis methods for your ContainerHistory
class:
Implement analysis methods for your ContainerHistory
class:
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.)
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:
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]
It's time to pick up history! The first version of our history knew only the original value. Implement the following methods:
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!
Implement the following method:
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
Fill the analysis so that it prints the greatest fluctuation and the history variance.
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.
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
.
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.
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.
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:
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
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:
null
reference, if the tank hasn't been installedIllegalStateException
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
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:
IllegalStateException
if the milking robot hasn't been installedIllegalStateException
if the milking robot hasn't been installedCollection
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
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
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.
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); }
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.
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
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
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.
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 ups
go downa
go leftd
go rightWhen 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:
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.
public void run()
, which starts the gameAttention! 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
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.
Together with the exercise body, you find the class FileManager
, which contains the method bodies to read a write a file.
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
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.
Modify the method public void save(String file, ArrayList
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.
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
.
Create a parameterless constructor, as well as the methods:
public void add(String word, String translation)
public String translate(String word)
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.
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.
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
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.
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 "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.
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 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.
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.
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?
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
.
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.
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.
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!
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 JTextArea
s -- 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.
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.
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(); }
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); }
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.
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
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
.
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.
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.
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
.
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 JButton
s, with texts "+", "-" and "Z".
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.
Let's extend our program with the following features:
setEnabled(false)
. Otherwise, the button has to be on.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 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.
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.
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.
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.
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
.
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.
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);
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.
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);
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.
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.
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:
The abstract class GameOfLifeBoard
provides the following functionality
The class GameOfLifeBoard
has also got the following abstract method, which you will have to implement.
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
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!
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!
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
Only one method is missing: manageCell(int x, int y, int livingNeighbours). Game of Life rules were the following:
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(); } }
Before the course comes to its end, we can still have a look at some useful particular features of Java.
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.
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
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."); }
We often want to know whether a substring repeats within another string. In regular expressions, we can use repetition symbols:
*
stands for a repetition from 0 to n times, for instanceString string = "trolololololo"; if(string.matches("trolo(lo)*")) { System.out.println("The form is right."); } else { System.out.println("The form is wrong."); }
The form is right.
+
stands for a repetition from 1 to n times, for instanceString string = "trolololololo"; if(characterString.matches("tro(lo)+")) { System.out.println("The form is right."); } else { System.out.println("The form is wrong."); }
The form is right.
String characterString = "nänänänänänänänä Bätmään!"; if(characterString.matches("(nä)+ Bätmään!")) { System.out.println("The form is right."); } else { System.out.println("The form is wrong."); }
The form is right.
?
stands for a repetition of 0 or 1 time, for instanceString string = "You have accidentally the whole name"; if(characterString.matches("You have accidentally (deleted )?the whole name")) { System.out.println("The form is right."); } else { System.out.println("The form is wrong."); }
The form is right.
{a}
stands for a repetition of a
times, for instanceString string = "1010"; if(string.matches("(10){2}")) { System.out.println("The form is right."); } else { System.out.println("The form is wrong."); }
The form is right.
{a,b}
stands for a repetition from a
to b
times, for instanceString string = "1"; if(string.matches("1{2,4}")) { System.out.println("The form is right."); } else { System.out.println("The form is wrong."); }
The form is wrong.
{a,}
stands for a repetition from a
to n times, for instanceString string = "11111"; if(string.matches("1{2,}")) { System.out.println("The form is right."); } else { System.out.println("The form is wrong."); }
The form is right.
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.
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
.
Let's train to use regular expressions. The exercises are done in the Main
class of the default package .
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.
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.
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.
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.
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(); } } } }
Let's create a program to manage the staff personnel of a small business.
Create the enumerated type, or enum, Education
in the package personnel
. The enum has the titles D
(doctor), M
(master), B
(bachelor), GRAD
(graduate).
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
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 employeespublic void add(List<Person> persons)
adds the parameter list of people to the employeespublic void print()
prints all the employeespublic void print(Education education)
prints all the employees, who have the same education as the one specified as parameterATTENTION: The Print
method of the class Employees
have to be implemented using an iterator!
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
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
Next, we create enums which contain object variables and implement an interface.
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
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.
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.
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:
Rating | Value |
---|---|
BAD | -5 |
MEDIOCRE | -3 |
NOT_WATCHED | 0 |
NEUTRAL | 1 |
FINE | 3 |
GOOD | 5 |
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
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]
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.
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]
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]
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.
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 \ Film | Gone with the Wind | The Bridges of Madison County | Eraserhead | Blues Brothers |
---|---|---|---|---|
Matti | BAD (-5) | GOOD (5) | FINE (3) | - |
Pekka | FINE (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.
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.
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); } }
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); } }
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"); }
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); } }
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); } }
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); } }
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);
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.
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.
The course is nearing the end, and it's time for the grande finale!
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.
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
.
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
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.Modify the functionality of the method actionPerformed
so that it would implement the following tasks in the given order.
continue
is assigned the value false
update
, which is a method of the variable updatable
which implements the interface Updatable
.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.
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.
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.
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();
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.
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ö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!"); }
Luodaan ohjelma, joka mittaa kliksutteluvauhtia. Käyttöliittymä tulee näyttämään esimerkiksi seuraavalta.
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
.
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.
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.
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.
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
.
Täydennä käyttöliittymäluokan metodi luoKomponentit
. Ohjelma tarvitsee toimiakseen kolme käyttöliittymäkomponenttia:
setEditable
. JTextArea
eroaa JTextField
-komponentista siten, että JTextArea
-komponentissa voi olla tekstiä useammalla rivillä.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ä.
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.
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.
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"
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.
Kohta
. public String teksti()
-metodi palauttaa konstruktorissa annetun tekstin sekä rivin "(jatka painamalla enteriä)"
. (Rivinvaihto saadaan aikaan merkillä "\n".)public void asetaSeuraava(Kohta seuraava)
-metodilla voidaan asettaa Kohta
-olio, jonka seuraavaKohta(String vastaus)
aina palauttaa (vastauksesta riippumatta). 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ä)
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.
Tekstiseikkailussa voi olla kysymyksiä, joihin on annettava oikea vastaus ennen kuin pelaaja pääsee eteenpäin. Tee luokka Kysymys
seuraavasti:
Kohta
-rajapinnan. asetaSeuraava
-metodilla. seuraavaKohta
-metodia kutsutaan oikealla vastauksella, metodi palauttaa seuraavan kohdan, muuten metodi ei päästä etenemään ja palauttaa arvon this
, eli viitteen tähän olioon. 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ä) >
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
Kohta
. public Monivalinta(String teksti)
public void lisaaVaihtoehto(String valinta, Kohta seuraava)
public String teksti()
public Kohta seuraavaKohta(String valinta)
Integer
luokkametodilla parseInt
. 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ä.
NHL:ssä pidetään pelaajista yllä monenlaisia tilastotietoja. Teemme nyt oman ohjelman NHL-pelaajien tilastojen hallintaan.
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:
String getName
String getJoukkue
int getOttelut
int getMaalit
int getSyotot
int getPisteet
- laskee kokonaispistemäärän eli maalien ja syöttöjen summanTalleta 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.
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);
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.
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ä.
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 ); }
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.
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:
equals
-metodia. Tarkemmin equals
-metodin toiminnasta Javan Object luokan APIsta. Huomaa, että equals
-metodin toiminta riippuu siitä, onko verrattavan olion luokka korvannut Object
-luokassa määritellyn equals
-metodin. toString
-palauttamat characterStringt) ovat samat. Muutoin metodi palauttaa false.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
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.
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()
.
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...
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
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.
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.
Luo pakkaukseen posti
luokka Viestivarasto
, ja lisää sille seuraavat metodit:
public void lisaa(Sahkoposti s)
lisää viestinpublic Sahkoposti hae(String otsikko)
palauttaa viestin jolla on annettu otsikko tai null jos sellaista ei ole. Voit olettaa että millään kahdella viestillä ei ole samaa otsikkoa.
Lisää luokkaan Viestivarasto
seuraavat metodit
public Sahkoposti hae(int aika)
palauttaa viestin joka saapui annettuun aikaan tai null jos sellaista ei ole. Voit olettaa että millään kahdella viestillä ei ole samaa saapumisaikaa.public Sahkoposti haeUusinViesti()
hakee uusimman viestin (eli sen jonka saapumisaika on isoin) ai null jos sellaista ei ole.public Sahkoposti haeUusinViesti(int ylaraja)
hakee uusimman viestin joka ei ole saapunut annetun ajan ylaraja
jälkeen. Metodi palauttaa null jos tällaista viestiä ei ole.Huom! Kannattaa käyttää kahta erillistä rakennetta viestien tallentamiseen. Otsikon perusteella tallentamiseen voit käyttää HashMap
pia, ja viestien tallentamiseen ajan mukaan TreeMap
pia. Näin saat toteutettua hae-operaatiot tehokkaasti. Tutustu myös TreeMap
in metodeihin lastKey()
ja floorKey()
.
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 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:
public boolean tyhja()
palauttaa true jos pino on tyhjäpublic boolean taynna()
palauttaa true jos pino on täynnäpublic void pinoon(int luku)
laittaa parametrina olevan luvun pinon päällepublic int huipulla()
kertoo pinon huipulla olevan alkionpublic int pinosta()
poistaa ja palauttaa pinon päällä olevan alkionpublic int lukuja()
kertoo pinossa olevien lukujen määränpublic int tilaa()
kertoo pinon vapaan tilan määränToteutetaan 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.
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.
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ä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
.
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
.
public void lisaaLuku(int luku)
public int sum()
public int lukujenMaara()
public boolean sisaltaa(int luku)
Testikansiossa olevassa pakkauksessa tilasto
on luokka LukutilastoTest
, johon sinun tulee lisätä uusia testimetodeja.
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.
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.
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:
RISTI
, NOLLA
tai pelin päätyttyä TYHJA
IllegalArgumentException
, jos sarake tai rivi on pelilaudan ulkopuolella tai ruudussa on jo merkki, ja poikkeuksen IllegalStateException
, jos peli on jo loppu.TYHJA
, RISTI
tai NOLLA
. Metodi heittää poikkeuksen IllegalArgumentException
, jos sarake tai rivi on pelilaudan ulkopuolella.true
, jos toinen pelaajista voitti pelin tai peli päättyi tasapeliin, muutoin metodi palauttaa false
TYHJA
, jos peli on kesken tai peli päättyi tasapeliin, muutoin metodi palauttaa voittajan merkin: RISTI
tai NOLLA
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
.