Preamble
Motivation
This document gives a very dense summary of what we consider the most important topics of the "Programming II" course. It is meant as a starting point, explaining just the bare minimum. You will have to do some further reading – tutorials, books, articles, API documentation.
Everyone is different! We don’t want to force you to read a specific book. That’s why we leave it up to you to find documentation that suits your preferred style. Some people love reading a language specification, others learn best from examples. Some students read books cover to cover, while others hop from one Stack Overflow answer to the next.
Each chapter ends with a "Resources" section which gives helpful links. We don’t specifically recommend an official course book, but the following three might be of interest:
Sprechen Sie Java? by Hanspeter Mössenböck (5. Auflage) – The official "Programming I" course book has a certain overlap with "Programming II". We will link to the relevant sections wherever applicable.
Java Programming for Kids by Yakov Fain (free online version) – Once you get over the humiliation of reading a book for children, you will appreciate its simple and clear style.
Effective Java by Josh Bloch – A very advanced book which does not focus on Java basics, but on writing clear, correct, robust, and reusable code.
Authors
Rahel Lüthy
Rahel Lüthy is a software developer and research associate at the Institute for Medical and Analytical Technologies (FHNW). She has an MSc in evolutionary & population biology (University of Basel), and 15+ years of industry experience as a Java developer. Apart from Java, she is passionate about Scala, functional programming, open-source, Git, and Gradle. A dog, two kids, three karate belts, four bicycles, and five guitars keep her balanced.
Dominique Brodbeck
Dominique Brodbeck is a professor for biomedical informatics at the Institute for Medical and Analytical Technologies (FHNW), as well as founding partner of the company Macrofocus GmbH. He has a PhD in Physics (University of Basel) and holds an Executive MBA in Management of Technology (EPFL/HEC Lausanne). His activities focus on how to extract meaningful information from large amounts of complex data, and on how to make it accessible to humans in a usable way. His motto is: "I want to make people happy by making computers sing and dance". He owns seven bicycles, following rule #12: The correct number of bikes to own is n+1, where n is the number of bikes currently owned.
1. Introduction
In this chapter you will learn how to set up your development environment (IDE) in order to work on the course exercises.
1.1. Test-Driven Development
Throughout this course, you will be asked to solve exercises for each chapter. The organization of exercises is inspired by an idea called Test-Driven Development: You receive automated JUnit tests that define the desired outcome of your exercises, and are asked to implement a solution that passes these tests.
Initially, all tests are failing ("red"). You know that you are done with your exercises as soon as all tests are succeeding ("green").
For each course chapter, you receive a ZIP containing a Java project with the following main elements:
-
A Gradle configuration which allows to conveniently import the project into any IDE (e.g. IntelliJ IDEA)
-
A
src/main/java
folder which contains empty stubs for your solutions -
A
src/main/test
folder which contains JUnit tests
1.2. Exemplary Walk-Through
This introductory chapter will walk you through the steps needed to set up an exemplary project. At the end, you will be all set for working on the "real" exercises later.
1.2.1. Download & IDE Import
Download the prog-II-exercise-01-intro.zip and extract it to any folder.
The directory tree contains a variety of files, but first, we are only interested in the build.gradle
file.
This is a configuration file for Gradle, a build tool that is supported by all major IDEs (IntelliJ IDEA, Eclipse, NetBeans).
A build file describes the structure of the project.
It declares which programming language is being used (java
) and specifies where the actual code is located (e.g. the src/main/java
folder).
You do not need to know Gradle in order to successfully complete this course.
Gradle is only used to allow for a smooth import of exercise projects into your IDE.
This guide assumes that you have a working installation of JDK 8 and IntelliJ IDEA 15. Most setup steps are similar with Eclipse or NetBeans – feel free to use your IDE of choice. |
To import the project into IntelliJ IDEA, invoke the File > New > Project From Existing Sources
action and select the build.gradle
file.
When starting with a very fresh IntelliJ IDEA download, you might run into JAVA_HOME issues.
This Stack Overflow article explains how to resolve them.
From the initial wizard, proceed with Open… to select the build.gradle file and import the project.
|
Trust the default settings and press OK to trigger the import.
Once imported, make sure that IntelliJ IDEA uses JDK 8 to compile your Java code.
Open File > Project Structure
to display the settings:
Finally, you can verify that your setup is correct by compiling the project (Build > Make Project
) and running the SetupChecker
class:
If the text "Hello World" appears in your Run
console window, everything is set up correctly – congratulations!
You don’t need to understand the SetupChecker code.
This class just verifies that your IDE is correctly configured to compile & run Java 8 projects.
|
1.2.2. JUnit
In this introductory example, you are asked to program a simple calculator.
The CalculatorTest
class contains JUnit tests that serve as a specification, i.e. they declare how your Calculator
implementation shall behave.
JUnit is a wide-spread framework to write unit tests in Java. Generally speaking, unit tests assert that a unit of code behaves according to expectations (in Java, a unit corresponds to a class). Unit testing has a lot of benefits and learning to use JUnit is well worth the effort.
However, in this course you will not actually need to write JUnit tests. The tests are provided to guide you towards a correct implementation of each exercise, i.e. they drive your implementation, hence the term Test-Driven Development.
While the CalculatorTest
class contains a full suite of tests, the Calculator
class itself does not contain the correct implementation yet, it only contains method stubs.
It is a convention to name the test class after the class which is being tested: The class CalculatorTest contains all tests for the class Calculator .
To keep the main code (Calculator ) well separated from the test code (CalculatorTest ), the top-level src folder is split into two distinct folder hierarchies (test vs main ).
|
The CalculatorTest
can be run via context menu:
This will execute all tests in a JUnit-specific runner.
As expected, they all fail because the Calculator
is not yet implemented:
1.2.3. Code!
Now is the time to actually work on the exercises.
Roll back your sleeves and implement the Calculator
class (do not change the CalculatorTest
class).
Inside Calculator
, delete all lines which look like this
throw new UnsupportedOperationException();
and replace them with the correct calculation logic. Verify the correctness of your implementation by re-running the JUnit tests. You are done once all tests are green
1.3. Resources
2. Data Structures
In "Programming I" you learned how to use primitive data types like boolean
, int
, or float
.
You also learned how to store multiple values in the form of arrays, e.g. boolean[]
, int[]
, or float[]
.
An array represents a very basic data structure, i.e. a particular way of organizing data in a program.
In this chapter you will learn about other data structures, namely List
, Set
, and Map
, which offer certain advantages over using basic arrays.
Choosing the right data structure is crucial to making programs efficient in terms of execution speed and memory usage.
2.1. The Array Data Structure
Data structures group multiple objects into a single unit. In Java, the most basic container that can hold a fixed number of values is an array:
// creates an array of strings and initializes it with three values
String[] fruit = {"Apple", "Orange", "Banana"};
String someFruit = fruit[1]; // index-based access
System.out.println(someFruit); // prints "Orange"
2.2. Array Alternatives
It seems that arrays are simple to use and need very little memory – so why ever use a different data structure?
Imagine that your program needs to read values from a file. When you write the program, you do not know how many values the file will contain, so how big should you initialize your array?
Imagine that your program needs to read some text and output all unique characters used in that text. When using an array to store the characters, how would you make sure that you do not store the same character twice? Checking the complete array every time you encounter a character will make your program very slow. And how big should you initialize your array?
Imagine that your program needs to read some text and create a histogram for all characters used in that text, i.e. it should gather statistics in the form of 'a: 33', 'b: 0', 'c: 12'
etc.
Of course you can use two arrays, one to keep track of the characters, and one to keep track of the counts.
But how big should you initialize them?
If someone was asking just for the count of the n
characters, how would you look that up efficiently?
The following sections will explain List
, Set
, and Map
in more detail.
2.3. The List Interface
The List
interface provides a sequential data structure which grows dynamically whenever new elements are added.
Don’t worry if you are not familiar with the term "interface" – you will learn more about this concept in a future chapter. For the time being, think of interfaces as a simple contract: They specify a group of methods that an implementation of that interface must provide. |
The ArrayList
class represents a general-purpose implementation of the List
interface.
Here’s how an ArrayList
can be created, filled, and queried:
List<String> fruit = new ArrayList<>();
System.out.println(fruit.size()); // prints 0
fruit.add("Apple");
fruit.add("Orange");
fruit.add("Banana");
System.out.println(fruit.size()); // prints 3
String someFruit = fruit.get(1); // index-based access
System.out.println(someFruit); // prints "Orange"
The angle brackets used in the List<String>
declaration can be read as of element type String.
The concept behind this syntax is called "generics" or "type parameterization".
In our example, it guarantees that the add
and get
methods only allow elements of type String
.
Because we declare our fruit
variable to be of type List<String>
the compiler can tell that our ArrayList
must also be of element type String, therefore we don’t have to repeat the
element type but can use empty brackets <>
("diamond") instead.
As mentioned above, List
is an interface (a contract), while ArrayList
is a concrete implementation (a class
that fulfills the contract by supporting all required methods).
It is good practice to program to the interface rather than the implementation:
List<String> list = new ArrayList<>(); // good
ArrayList<String> anotherList = new ArrayList<>(); // bad
This assures that wherever you use your list
variable, you are only using methods that are actually declared by the List
interface.
If you later on decide to e.g. use a LinkedList
(an alternative implementation of the List
interface) rather than an ArrayList
, you only need to swap out the implementation in one spot.
The easiest way to iterate over all elements of a list is by using a so-called "for-each" loop:
List<String> list = Arrays.asList("Apple", "Orange", "Banana");
for (String element : list) {
System.out.println(element);
}
Using Arrays.asList("Apple", "Orange", "Banana") is a simple way to create a fixed-size list with some elements
|
Alternatively, all lists can also be converted to data streams, which can then be processed via The Java 8 Stream API.
You are now ready to tackle the first part of the exercises.
Get yourself a coffee, import the build.gradle
file contained inside prog-II-exercise-02-data-structures.zip, and get started with implementing the List101
class.
2.4. The Set Interface
The Set
interface provides a data structure that grows dynamically when new elements are added and makes sure that it never contains duplicates.
The HashSet
class is the most commonly used Set
implementation.
The following example illustrates how a HashSet
can be used:
Set<String> fruit = new HashSet<>();
System.out.println(fruit.size()); // prints 0
fruit.add("Apple");
fruit.add("Orange");
fruit.add("Banana");
System.out.println(fruit.size()); // prints 3
fruit.add("Orange");
System.out.println(fruit.size()); // still prints 3
System.out.println(fruit.contains("Apple")); // prints true
System.out.println(fruit.contains("Lemon")); // prints false
As mentioned before, the angle brackets in the Set<String> declaration can be read as of element type String
|
While we can also use a "for-each" loop to iterate over all elements, the HashSet
class makes no guarantees as to the order of the iteration:
Set<String> words = new HashSet<>(Arrays.asList("Apple", "Banana", "Orange"));
for (String word : words) {
System.out.println(word);
}
This will print elements in unpredictable order:
Apple Orange Banana
The LinkedHashSet
class represents a Set
implementation which guarantees that elements are returned in the order in which they were inserted.
However, this convenience comes at a certain price, i.e. a LinkedHashSet
uses more memory than a traditional HashSet
.
2.5. The Map Interface
The Map
interface provides a data structure which maps keys to values.
A map will never contain duplicate keys (i.e. the keys behave like a set), and each key can map to at most one value.
The HashMap
class is the most commonly used Map
implementation.
The following example illustrates how a HashMap
can be used:
Map<String, Integer> birthYears = new HashMap<>();
System.out.println(birthYears.size()); // prints 0
birthYears.put("Barack", 1961);
birthYears.put("Hillary", 2016);
birthYears.put("Ada", 1815);
System.out.println(birthYears.size()); // prints 3
birthYears.put("Hillary", 1947);
System.out.println(birthYears.size()); // still prints 3
System.out.println(birthYears.containsKey("Hillary")); // prints true
System.out.println(birthYears.get("Hillary")); // prints 1947
System.out.println(birthYears.containsKey("Donald")); // prints false
As mentioned before, the angle brackets in the Map<String, Integer> declaration specify the type of entries added to the map.
For maps, we need two type declarations, one for the keys and one for the values.
In this example, the key are of type String while the values are of type Integer , but you might as well construct a map where keys and values are of the same type (Map<String, String> ).
|
Each map offers three different views on its contents: it can be queried for a set of keys, a collection of all its values, or a set of its entries:
Map<String, Integer> birthYears = new HashMap<>();
birthYears.put("Barack", 1961);
birthYears.put("Hillary", 1947);
birthYears.put("Ada", 1815);
Set<String> keys = birthYears.keySet();
System.out.println(keys); // prints [Hillary, Barack, Ada]
Collection<Integer> values = birthYears.values();
System.out.println(values); // prints [1947, 1961, 1815]
Set<Map.Entry<String, Integer>> entries = birthYears.entrySet();
System.out.println(entries); // prints [Hillary=1947, Barack=1961, Ada=1815]
Iteration order is not guaranteed.
A LinkedHashMap can be used to return contents in key-insertion order.
|
The entrySet()
view is particularly helpful when iterating over the contents of a map:
Map<String, Integer> birthYears = new HashMap<>();
birthYears.put("Barack", 1961);
birthYears.put("Hillary", 1947);
birthYears.put("Ada", 1815);
for (Map.Entry<String, Integer> entry : birthYears.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
This will print the following lines (in unpredictable order):
Hillary -> 1947 Barack -> 1961 Ada -> 1815
If a Map
is queried for a key it does not contain, a null
value will be returned.
Working with null
values is very error prone and should be avoided whenever possible.
Java 8 offers a safe way of providing a default value for absent mappings:
Map<Integer, String> numbers = new HashMap<>();
numbers.put(0, "zero");
numbers.put(2, "two");
numbers.put(3, "three");
String defaultValue = "unknown";
System.out.println(numbers.getOrDefault(0, defaultValue)); // prints "zero"
System.out.println(numbers.getOrDefault(1, defaultValue)); // prints "unknown"
2.6. The Collections Helper
The List
, Set
, and Map
interfaces all belong to the
Java Collections Framework, which provides a variety of data structures ready to be used.
In addition, the framework also offers the
Collections
helper class.
Among its many utility methods, min
and max
are particularly helpful when working with numbers:
List<Integer> values = Arrays.asList(1, 99, -2, 42);
System.out.println(Collections.min(values)); // prints -2
System.out.println(Collections.max(values)); // prints 99
2.7. The Java 8 Stream API
The previous sections explained how List
, Set
, and Map
data structures all belong to a common hierarchy of collections.
Some collections allow duplicate elements and others do not.
Some are ordered and others unordered.
But generally speaking, a collection stores a number of elements in memory.
Java 8 introduced a further abstraction, namely the
Stream
interface – a data source which sequentially serves elements without storing them in memory.
The concept of streams is very powerful, but also quite complex.
The vast number of available online tutorials is a clear indication of this fact.
We will not cover streams in detail in this course, but this introductory section gives a little taste of their power.
In contrast to collections, streams do not store elements in memory but simply serve as a source of elements. Some streams provide elements by reading them from a collection, but others read them from the internet, or calculate them on the fly. Much like a water pipeline, where water only flows if the faucet is turned open, streams only serve elements as long as these elements are actually consumed.
At least three aspects of streams deserve special attention:
-
Streams offer a variety of methods to filter, transform, and collect elements. These methods can be chained in a very functional way, which leads to concise, but readable code.
-
Streams can be created from collections and converted back to collections.
-
Streams serve as a potentially infinite source of elements.
Let’s dive in with an example.
Given a list of words, produce a list of Integer
values which correspond to each word’s length:
Stream<String> words = Stream.of("hello", "new", "world", "of", "streams");
List<Integer> lengths = words.map(word -> word.length()).collect(Collectors.toList());
System.out.println(lengths); // prints [5, 3, 5, 2, 7]
In order to understand a sequence of instructions, it is often helpful to look at the types of intermediate results:
Stream<String> words = Stream.of("hello", "new", "world", "of", "streams");
List<Integer> lengths = words. // Starting with a Stream<String> ...
map(word -> word.length()). // ... transform each String to an Integer
collect(Collectors.toList()); // ... convert the final Stream<Integer> to a List<Integer>
Just for illustration purposes – calculating the maximum length of a filtered list of words:
Stream<String> words = Stream.of("Apple", "Orange", "Apricot", "Banana", "Orange");
int maxLength = words.
filter(word -> word.startsWith("A")). // [Apple, Apricot]
mapToInt(word -> word.length()). // [5, 7]
max().getAsInt(); // 7
System.out.println(maxLength); // prints 7
All collections offer a stream()
method which returns a sequential stream over their elements.
The following example shows how to create a stream from a List
, process its elements, and convert it to a Set
:
List<String> words = Arrays.asList("Apple", "Orange", "Apricot", "Banana", "Orange");
Set<String> filteredWords = words.stream().
filter(word -> word.contains("r")).
collect(Collectors.toSet());
System.out.println(filteredWords); // prints [Apricot, Orange]
The Collectors.toSet()
method internally creates a HashSet
to store the final elements.
That’s why the final Set
does not contain elements in order.
This can however be achieved by explicitly collecting elements inside a LinkedHashSet
:
List<String> words = Arrays.asList("Apple", "Orange", "Apricot", "Banana", "Orange");
Set<String> filteredWords = words.stream().
filter(word -> word.contains("r")).
collect(Collectors.toCollection(LinkedHashSet::new));
System.out.println(filteredWords); // prints [Orange, Apricot]
To round off this introductory stream section, the following example uses an infinite stream to create random dice rolls:
Random random = new Random();
Stream<Integer> diceRollingStream = Stream.generate(() -> random.nextInt(6) + 1);
List<Integer> threeDiceRolls = diceRollingStream.limit(3).collect(Collectors.toList());
2.8. Resources
Hanspeter Mössenböck: Sprechen Sie Java? |
Kapitel 15: "Generizität", Kapitel 23.1: "Collection-Typen" |
The Java Tutorials: Collections | |
Angelika Langer: Java Generics FAQs |
http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html |
Benjamin Winterberg: Java 8 Stream Tutorial |
http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ |
Oracle Technology Network: Processing Data with Java SE 8 Streams |
http://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html |
3. Input / Output
In this chapter you will learn how to read and write textual data. You will use the Scanner
class to read textual input, and the PrintWriter
class to output data back to file.
3.1. Reading Text with a Scanner
The Scanner
class allows to read textual input:
String input = "Mary had a little lamb";
Scanner scanner = new Scanner(new StringReader(input));
while (scanner.hasNext()) {
System.out.println(scanner.next());
}
This will print:
Mary had a little lamb
Each Scanner
instance can be configured in how it breaks input into tokens. Various hasNextXXX()
and nextXXX()
methods allow to parse different value types. The following example demonstrates how to parse double
values separated
by a dash (-
):
String input = "0.1-42.0-99.9";
Scanner scanner = new Scanner(new StringReader(input));
scanner.useDelimiter("-");
List<Double> values = new ArrayList<>();
while (scanner.hasNextDouble()) {
values.add(scanner.nextDouble());
}
System.out.println(values);
Multi-line input can be parsed by making use of the hasNextLine()
and nextLine()
methods:
String input =
"line 1\n" +
"line 2\n" +
"line 3";
Scanner scanner = new Scanner(new StringReader(input));
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
System.out.println("––––––");
}
Which will print:
line 1 –––––– line 2 –––––– line 3 ––––––
In all examples so far we were reading input from a String
variable. In reality, data is often read from a file on disk.
The File
class can be used to represent such a file.
Note that the actual scanning logic stays exactly the same as before. However, things can go wrong when working with files,
that’s why a try/catch
exception handling construct is needed:
File inputFile = new File("input.txt");
try (Scanner scanner = new Scanner(inputFile)) {
while (scanner.hasNext()) {
System.out.println(scanner.next());
}
} catch (FileNotFoundException e) {
System.err.println("Reading file failed: " + e.getMessage());
}
Have a look at the official Oracle Tutorial if you would like to learn more about the concept of exceptions. |
3.2. Creating & Formatting Text
When dealing with text, String
values are often created via successive concatenation, i.e. by
repeatedly appending fragments. This can most elegantly be achieved via the
StringBuilder
class:
StringBuilder result = new StringBuilder();
for (String name : Arrays.asList("Lisa", "Bob", "Hillary")) {
result.append(name).append("\n");
}
System.out.println(result);
Which will print:
Lisa Bob Hillary
The String.format
helper method allows to write out values in a specific format:
List<String> numbers = Arrays.asList("one", "two", "three");
StringBuilder result = new StringBuilder();
for (int i = 0; i < numbers.size(); i++) {
String formatted = String.format("%d %S", i, numbers.get(i));
result.append(formatted).append("\n");
}
System.out.println(result);
Which will print:
0 ONE 1 TWO 2 THREE
3.3. Writing Text with a PrintWriter
The PrintWriter
class can be used to write text
to a file:
File outputFile = new File("out.txt");
try (PrintWriter writer = new PrintWriter(outputFile)) {
writer.print("Hello World");
} catch (FileNotFoundException e) {
System.err.println("Writing file failed: " + e.getMessage());
}
As seen before, interacting with the File
class can lead to exceptions. The try/catch
clause is taking care of
properly handling the error scenario. In addition, constructing the PrintWriter
in the try()
statement makes sure
that it is properly flushed and closed once we are done working with it.
3.4. Resources
4. Object-Oriented Programming
The chapter on Data Structures explained how data can be stored in collections.
A List<String>
e.g. allows to keep track of text values.
But what if we want to store more heterogeneous values?
We cannot use a List
to keep track of a person’s name, a phone number, and a city or residence.
Even if we could stuff a mixture of text values and numbers into a list, we would actually prefer a List<Person>
, right?
But unfortunately, the Java standard library does not offer a Person
class.
The following sections explain how we can write classes like Person
ourselves.
Classes represent the core building blocks in Java.
As an object-oriented language, Java uses the concepts of composition and inheritance to create programs out of these building blocks.
4.1. Classes & Objects
In the real world, we tend to group things into different categories, e.g. "Emma" is a person, while "Milo" is a dog, and "Basel" is a city. In Java, person, dog, and city are known as classes, while "Emma", "Milo", and "Basel" are objects.
Taking the city as an example, suppose you would like to build a catalogue of cities.
Each city can be described by a name and a ZIP code.
The following code shows how to model this data structure with a custom City
class:
public final class City { (1)
private final String name; (2)
private final int zipCode; (2)
public City(String name, int zipCode) { (3)
this.name = name;
this.zipCode = zipCode;
}
public String getName() { (4)
return name;
}
public int getZipCode() { (4)
return zipCode;
}
}
1 | The class declaration |
2 | Two fields (sometimes also known as instance variables or properties) |
3 | A constructor (accepting two parameters or arguments, assigning them to the fields) |
4 | Two methods (specifically, two accessor methods) |
The public /private and final keywords ensure that the class is well encapsulated and immutable.
These aspects are not important in small programs.
In larger applications, they prevent certain categories of errors and make the code base more maintainable.
|
Let’s see how the City
class can be used – or in Java lingo: how it can be instantiated.
The terms "creating an object of", "instantiating", or "creating an instance of" are all synonymous. |
City basel = new City("Basel", 4000);
City zurich = new City("Zurich", 8000);
City muttenz = new City("Muttenz", 4132);
System.out.println(basel.getName()); // prints "Basel"
System.out.println(muttenz.getZipCode()); // prints 4132
The new
keyword creates an instance of a class by calling the constructor which matches the number and types of the given arguments.
4.2. Composition
Our City
class is composed of two fields, a name
and a zipCode
.
The name
field is of type String
, which is itself a class.
This is the simplest form of class re-use: We built a new class City
by composing it of an existing building block, the String
class.
The same approach can be used with custom classes.
The following class defines a Person
by re-using our own City
class:
public class Person {
private final String firstName;
private final String lastName;
private final City city;
public Person(String firstName, String lastName, City city) {
this.firstName = firstName;
this.lastName = lastName;
this.city = city;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public City getCity() {
return city;
}
}
When creating a Person
instance, an existing City
instance is passed to the Person
constructor:
City basel = new City("Basel", 4000);
Person gigi = new Person("Gisela", "Oeri", basel);
System.out.println(gigi.getFirstName()); // prints "Gisela"
System.out.println(gigi.getCity().getZipCode()); // prints 4000
The getCity().getZipCode() chain reflects the hierarchical nature of class composition.
|
Now our building blocks are complete enough to address the initial problem, the wish for a list of persons:
City basel = new City("Basel", 4000);
Person gigi = new Person("Gisela", "Oeri", basel);
Person eva = new Person("Eva", "Herzog", basel);
List<Person> persons = Arrays.asList(gigi, eva);
4.3. Inheritance
As seen in the previous section, the technique of composition is used to model "has a" relationships: A person has a city of residence, a city has a name. This section will explain inheritance, a technique to express an "is a" relationship.
Starting from our Person
class, we can declare a new class Student
which is a special kind of person:
public class Student extends Person { (1)
private final int semester; (2)
public Student(String firstName, String lastName, City city, int semester) { (3)
super(firstName, lastName, city); (4)
this.semester = semester; (5)
}
public int getSemester() { (6)
return semester;
}
}
1 | The extends keyword specifies that Student inherits all aspects of a Person |
2 | A field (in addition to all fields inherited from Person ) |
3 | A constructor to create Student instances |
4 | A call to the super constructor (which initializes the Person base class) |
5 | Initialization of the semester field from the constructor argument |
6 | An accessor method to the semester field |
In this inheritance hierarchy, Person is referred to as the base-class.
Person is the super-class of Student , while Student is a sub-class of Person .
|
Using the Student
class is straight forward.
Instances of Student
offer all functionality of the base-class Person
, plus additional functionality like the getSemester()
accessor:
City city = new City("Wherever", 9999);
Student harry = new Student("Harry", "Potter", city, 3);
System.out.println(harry.getFirstName()); // prints "Harry"
System.out.println(harry.getSemester()); // prints 3
One thing to note is that the class inheritance hierarchy defines how objects can be assigned to differently typed variables. The "is a" metaphor is a good indication of whether an assignment is allowed:
City city = new City("Wherever", 9999);
Student harry = new Student("Harry", "Potter", city, 3);
Person bill = new Person("Bill", "Gates", city);
Person person;
Student student;
person = bill; // bill is a person
person = harry; // harry is a person
student = harry; // harry is a student
// student = bill; // DOES NOT COMPILE – bill is NOT a student
While it is very convenient to inherit the properties and behavior of a base-class, it is sometimes desirable to deviate in certain aspects.
The concept of overriding allows a sub-class to change the behavior of certain methods.
Let’s change the Student
class to return last names with a " (s)" suffix:
public class Student extends Person {
private final int semester;
public Student(String firstName, String lastName, City city, int semester) {
super(firstName, lastName, city);
this.semester = semester;
}
public int getSemester() {
return semester;
}
@Override
public String getLastName() {
return super.getLastName() + " (s)";
}
}
The @Override marker is called an annotation.
It marks the intention of overriding a method in the base-class.
If the Person class was changed to no longer offer a getLastName() method, this annotation would allow the compiler to detect this inconsistency.
|
4.3.1. The Object root class
Let’s look at a simple class which does not explicitly extend another class:
public final class Dog {
private final String name;
public Dog(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
In Java, all classes implicitly extend the Object
class.
In other words, even though Dog
does not declare an inheritance relationship via extends
, it still automatically extends Object
.
Class
Object
is the root of the class hierarchy. Every class hasObject
as a superclass. All objects, including arrays, implement the methods of this class.
Thus, all of your classes automatically inherit 11 methods declared by the Object
class.
Most of these methods are irrelevant in basic Java programs, but the toString()
method is important, so let’s look at it in further detail.
The toString()
method returns a textual representation of an object.
You can call toString()
like any other method, but most notably, it is automatically invoked whenever you pass an object to System.out.println()
:
Dog dog = new Dog("Milo");
System.out.println(dog); // prints 'ch.fhnw.ima.oop.Dog@511d50c0'
As illustrated in the above example, the base implementation returns weird looking gibberish (it is a mixture between the name of the class and its hash code, but that’s not important for the time being).
Since toString()
is a normal method, you are free to override it and provide a better textual representation of your object:
public final class Dog {
private final String name;
public Dog(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Dog (Name: " + name + ")";
}
}
The generated output now looks much friendlier:
Dog dog = new Dog("Milo");
System.out.println(dog); // prints 'Dog (Name: Milo)'
4.4. Enum Types
Now that you know how to define your own classes via composition and/or inheritance, let’s have a look at a special kind of data modeling problem for which Java provides a simple solution.
Imagine a game like Pac-Man, where a character is moving either up, right, down, or left. Somewhere in your game, you are tracking the character’s direction. What class or data-type would you choose for that purpose?
You might be tempted to use an int
type and declare 4 named constants:
// BAD SOLUTION
public static final int UP = 0;
public static final int RIGHT = 1;
public static final int DOWN = 2;
public static final int LEFT = 3;
private int direction;
public void setDirection(int direction) {
this.direction = direction;
}
This is quite readable, but very error prone: The type system can’t prevent usage of a "wrong" value. Consequently, a program that compiles just fine would break at runtime:
setDirection(UP); // ok
setDirection(42); // oops
What we would need is a type that just allows the four values UP
, RIGHT
, DOWN
, LEFT
and nothing else.
Java provides a special-purpose type to represent such fixed enumerations, the enum
type:
// GOOD SOLUTION
public enum Direction { (1)
UP, RIGHT, DOWN, LEFT (2)
}
1 | The enum keyword makes Direction extend the Enum base class |
2 | All values are constants and are thus, by convention, in uppercase letters |
Usage of an enum is straight-forward:
private Direction direction;
public void setDirection(Direction direction) {
this.direction = direction;
}
Note how enums improve code readability: Just by looking at the type of the direction
variable we already know something about its meaning (which was not the case with int
constants).
Java is a typed language.
By using proper types to model our data we are taking advantage of the compiler.
Errors can be caught at compile time and don’t cause bugs at runtime.
By using the Direction
enum, it is impossible to create a compiling program that contains a "wrong" direction value:
setDirection(Direction.UP);
setDirection(Direction.DOWN);
// setDirection(42); IMPOSSIBLE (does not compile)
Each enum
type automatically offers a variety of methods.
Most importantly, the values()
method returns all possible constants, and the name()
method returns a constant’s name:
for (Direction direction : Direction.values()) {
System.out.println(direction.name());
}
If you’re getting bored or overwhelmed by all this reading, you might want to write some code for a change. At this point you know just enough about object-oriented programming to get started with the first exercise. |
4.5. Interfaces
Suppose you were writing a fun little game in which unicorns played a major role. You picked up the new concepts explained in the previous sections and came up with this nice and simple class:
public class Unicorn extends Horse {
private final String name;
public Unicorn(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void glowInRainbowColors() {
System.out.println("Glitter");
}
}
As you can see, someone else already wrote a Horse
class – you were able to inherit most of its functionality, keeping your new Unicorn
class nice an simple.
Since you were done so quickly, you put all your energy and focus into listing your lovingly named unicorns in a fancy way:
~ •*¨*• ~ ~ •*¨*• ~ ★★★ Mystery ★★★ ★★★ Rainbow ★★★ ★★★ Jessica ★★★ ~ •*¨*• ~ ~ •*¨*• ~
This is the code that you wrote to generate the output:
public static void main(String[] args) {
List<Unicorn> unicorns = Arrays.asList(
new Unicorn("Mystery"),
new Unicorn("Rainbow"),
new Unicorn("Jessica"));
System.out.println(fancyUnicornNames(unicorns));
}
private static String fancyUnicornNames(List<Unicorn> unicorns) {
StringBuilder fancyNames = new StringBuilder();
fancyNames.append("~ •*¨*• ~ ~ •*¨*• ~\n");
for (Unicorn unicorn : unicorns) {
fancyNames.append("★★★ ").append(unicorn.getName()).append(" ★★★\n");
}
fancyNames.append("~ •*¨*• ~ ~ •*¨*• ~");
return String.valueOf(fancyNames);
}
Unfortunately, life is not all rainbows and unicorns.
Soon you were forced to extend your game and introduce dragons.
Dragons are not related to horses nor unicorns, thus you couldn’t extend any existing class.
For a second you were tempted to introduce a common base-class (e.g. LivingCreature
) in which you could nicely handle the name
field and the getName()
accessor in order not to duplicate this code.
However, Unicorn
already extends Horse
, and multiple inheritance is not supported in Java.
You decided to bite the bullet and write a Dragon
class from scratch:
public class Dragon {
private final String name;
public Dragon(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void fly() {
System.out.println("Flying higher and higher");
}
public void spitFire() {
System.out.println("Sparks & Flames!");
}
}
But wouldn’t it be nice if the list of dragon instances could be output in the same fancy style as the unicorns? Some quick copy/pasting, a bit of renaming – tada!
public static void main(String[] args) {
List<Dragon> dragons = Arrays.asList(
new Dragon("Toothless"),
new Dragon("Stormfly"),
new Dragon("Hookfang")
);
System.out.println(fancyDragonNames(dragons));
}
private static String fancyDragonNames(List<Dragon> dragons) {
StringBuilder fancyNames = new StringBuilder();
fancyNames.append("~ •*¨*• ~ ~ •*¨*• ~\n");
for (Dragon dragon : dragons) {
fancyNames.append("★★★ ").append(dragon.getName()).append(" ★★★\n");
}
fancyNames.append("~ •*¨*• ~ ~ •*¨*• ~");
return String.valueOf(fancyNames);
}
Hopefully you noticed how fancyUnicornNames
and fancyDragonNames
are almost identical.
While copy/pasting might still work for two methods, it obviously doesn’t scale – soon enough you will end up with fancyKnightNames
, fancyFairyNames
, and fancyMonsterNames
.
The tiniest bug requires the same fix in 10 different places.
Taking a step back, things are actually really simple: Inside the fancyNames
rendering code, you don’t really care whether you output a unicorn’s name, a dragon’s name, or a monster’s name.
In other words, you don’t really care whether your fancyNames
method gets a List<Unicorn>
, a List<Dragon>
, or even a List<Monster>
.
All you care about, is that whatever is in the list has a getName()
method.
4.5.1. Using an Interface to Create Reusable Code
Interfaces solve exactly this problem: They let you express a contract in order to write reusable code relying on that contract.
In our case, the contract solely requires a getName()
method:
public interface Named { (1)
String getName(); (2)
}
1 | The interface keyword is used to define an interface |
2 | A method signature (note the absence of a body, i.e. the method does not have a default implementation) |
Now that the Named
interface exists, we change the Unicorn
and Dragon
classes to implement it.
Implementing an interface is like making an official promise to fulfill a contract.
The compiler makes sure that this promise is actually kept.
This is how the Unicorn
class must be changed (same for Dragon
):
public class Unicorn extends Horse implements Named { (1)
private final String name;
public Unicorn(String name) {
this.name = name;
}
@Override (2)
public String getName() {
return name;
}
public void glowInRainbowColors() {
System.out.println("Glitter");
}
}
While extending multiple classes is not supported in Java, it would be perfectly legal to implement multiple interfaces. |
Now the fancyNames
method can be rewritten based on the new Named
interface:
public class FancyNames {
public static void main(String[] args) {
List<Unicorn> unicorns = Arrays.asList(
new Unicorn("Mystery"),
new Unicorn("Rainbow"),
new Unicorn("Jessica"));
System.out.println(fancyNames(unicorns));
List<Dragon> dragons = Arrays.asList(
new Dragon("Toothless"),
new Dragon("Stormfly"),
new Dragon("Hookfang")
);
System.out.println(fancyNames(dragons));
}
private static <T extends Named> String fancyNames(List<T> namedItems) { (1)
StringBuilder fancyNames = new StringBuilder();
fancyNames.append("~ •*¨*• ~ ~ •*¨*• ~\n");
for (Named named : namedItems) { (2)
fancyNames.append("★★★ ").append(named.getName()).append(" ★★★\n"); (3)
}
fancyNames.append("~ •*¨*• ~ ~ •*¨*• ~");
return String.valueOf(fancyNames);
}
}
1 | Accepts a list with items that implement the Named interface [1] |
2 | Iteration logic relies on the fact that all list items implement the Named interface |
3 | Actual usage of getName() , as declared by the Named interface |
To summarize: Introducing the Named
interface helped implementing the fancyNames
method in a way that is reusable for unicorns, dragons and all future implementations of the Named
interface.
4.5.2. Using an Interface to Achieve Encapsulation
Remember the List
, Set
, and Map
interfaces from the introductory section on Data Structures?
All of them are perfect illustrations of another important aspect of interfaces: Interfaces allow to hide implementation details.
In simple words, they separate what can be done (e.g. the List
interface) from how it is done (e.g. the ArrayList
implementation).
This separation makes programs easier to understand and maintain.
The complexity of the how is hidden behind the interface, making it e.g. possible to program against the List
interface without ever having to deal with the array-related complexity encapsulated in the ArrayList
class.
In the Data Structures section you were encouraged to always code to the List
interface rather than the ArrayList
implementation.
By now it should be clear that this has at least two benefits:
-
Oracle is completely free to re-implement, optimize, or otherwise change the
ArrayList
class – as long as it still properly implements theList
interface, no changes will be required in your code. -
On the other hand, you are also completely free to switch to another
List
implementation – if you want to use aLinkedList
rather than anArrayList
, you only need to swap out the implementation in one spot.
4.5.3. Working with Interfaces in Java 8
Everything you learned about interfaces so far is deeply baked into the Java language and works with versions as old as Java 5. With the latest version, Java 8, some very attractive language changes were made. At least two of them make working with interfaces even more pleasurable:
Default Methods
Traditionally, interfaces do not contain method bodies, but only declare method signatures:
public interface Named { (1)
String getName(); (2)
}
1 | The interface definition |
2 | A method signature without body |
With Java 8 it is possible to provide default implementations for interface methods:
public interface Named {
default String getName() {
return "Unknown";
}
}
A class implementing this interface then becomes very minimal:
public class Creature implements Named {
}
It fulfills the interface contract because it automatically inherits the default implementation, i.e. it is possible to call getName
on a Creature
instance:
Creature creature = new Creature();
System.out.println(creature.getName()); // prints "Unknown"
Of course Creature could still override getName as if no default implementation existed.
|
Lambda Expressions
Let’s revisit the Named
interface without the default method implementation again:
public interface Named { (1)
String getName(); (2)
}
1 | The interface definition |
2 | A method signature without body |
Methods without a body are called abstract methods, and interfaces with a single abstract method are called SAM types (Single Abstract Method types).
Lambda expressions, a new feature in Java 8, represent a convenience construct to make working with SAM types simpler:
Named myNamed = () -> "Lara Croft";
Note that the Named myNamed
part is a standard variable type/name declaration, only the part to the right of the =
sign is the lambda expression.
Let’s look at another example to get a better feel for SAM types and lambdas:
public interface Calculation {
double calc(double a, double b);
}
Calculation
again has a single, abstract method.
Consequently, a lambda expression can be used to provide an implementation:
Calculation addition = (a, b) -> a + b;
A lambda expression has everything that a method has: an argument list, which is the (a, b)
part, and a body, which is the a + b
part after the arrow symbol.
Just like methods, lambda expressions allow to encapsulate not just simple one-liners, but also more complex constructs:
Calculation fancy = (x, y) -> {
double fx = Math.sin(x);
double fy = Math.cos(y);
return fx + fy;
};
Curly braces and a return statement are needed for lambda expressions that encompass more than one statement.
|
Up to now we have only looked at how lambda expressions can be defined. We will now look at how they are used.
Actually, you have already met lambda expressions in an earlier section, namely when working with the The Java 8 Stream API:
Stream<String> words = Stream.of("hello", "new", "world", "of", "streams");
List<Integer> lengths = words.map(word -> word.length()).collect(Collectors.toList());
System.out.println(lengths); // prints [5, 3, 5, 2, 7]
The map
method takes the java.util.function.Function
SAM type as its argument, for which we provide an ad-hoc implementation in the form of the word → word.length()
lambda expression.
Lambda expressions allow to pass code fragments around like data, typically as arguments to other methods. |
4.6. Resources
Java4Kids: Chapter 3 |
https://yfain.github.io/Java4Kids/#_meet_classes_the_main_language_constructs |
Java4Kids: Chapter 5 |
https://yfain.github.io/Java4Kids/#_interfaces_lambdas_abstract_and_anonymous_classes |
Angelika Langer: Lambda Tutorial |
5. Functional Programming
For most humans, new concepts are easier to understand if they can be related to things they already know.
One of the appeals of object-oriented programming (OOP) is that objects have obvious analogies in real life.
Persons, dogs, and cities are very familiar concepts, and it’s easy to grasp how a Person
class models certain aspects of a real-life person.
Unfortunately, concepts like assignments or method calls often have no analogy in real life. Functional programming (FP) is a programming style that addresses this problem: Just like OOP focuses on the concept of objects, functional programming (FP) focuses on the concept of mathematical functions to describe computer programs. Because most of us know math better than we know programming, FP concepts are often easy to understand because they are so similar to math.
Note that OOP and FP are not exclusive: Certain aspects of a program are best expressed with objects, while others are easier to program using functions. Java 8 is a perfect language to combine the best of two worlds! Let’s see what FP brings to the table…
5.1. Mathematical Functions
If you look at this simple Java assignment through the eyes of a mathematician, it is utter nonsense:
x = x + 1
Similarly, the following Java method doesn’t have anything to do with math:
int add(int a) {
int c = a + this.b;
this.b = c;
return c;
}
To put them in contrast, let’s look at two typical math functions and explore their common characteristics:
f(x) = sin(x)
f(a,b) = a + b
These functions
-
have a set of input (
x
,a
andb
) -
each have one output
-
given the same input, always produce the same output
All three points sound really obvious in the context of mathematical functions. Yet, our Java methods often diverge from these rules in many ways.
Let’s look at our initial example again:
int add(int a) {
int c = a + this.b;
this.b = c;
return c;
}
Ideally, a Java method declares its input and output as part of its signature. Looking at
int add(int a)
could give the impression that our example method has one input a
and one output (the return value).
But this is not the case!
In fact, this method
-
pretends to have one input (
a
) but actually has two (a
andthis.b
) -
pretends to have one output (the return value) but actually has two (assigning
this.b = c
) -
given the same input
a
, may produce a different result (depending on the value ofthis.b
)
Obviously, it is not similar to a math function at all, which makes it really hard to understand.
The method has side effects: It reads values which are not declared as input parameters and writes values in addition to returning a result. It is these side effects that make the method hard to understand and cause it to deviate from the math function characteristics.
The following add
method is much easier to read because it has no side effects and is thus similar to a math function:
static int add(int a, int b) {
return a + b;
}
The static keyword makes it explicit that the function does not read nor write any fields
|
To summarize: In functional programming, methods explicitly declare their input parameters, return one result, and do not rely on side effects.
5.2. Programs as Data Pipelines
Let’s look at another example method:
public static int countFourLetterWords(File file) {
List<String> words = readWordsFromFile(file);
List<String> fourLetterWords = filterByLength(words, 4);
int count = count(fourLetterWords);
return count;
}
You probably have a pretty good understanding of what’s going on here, right?
Even though the methods readWordsFromFile
, filterByLength
, and count
are not shown in detail, it is still quite obvious what they are doing.
This is due to the fact that all of them have good names, very explicit input parameters, return one result, and don’t rely on side effects.
Functional programming uses functions as the fundamental building blocks of a program. Just like in mathematics, where simple functions can be combined to express more complex calculations, Java methods can be combined to form a pipeline. Data flows through the method calls until the final result is returned.
Functional programming obviously makes code very readable.
5.3. Preventing Programming Errors
Let’s go back to our initial add
example:
int add(int a) {
int c = a + this.b;
this.b = c;
return c;
}
This example may seem a little far fetched and contrived – seriously, who writes cryptic code like this?!
Actually, side effects occur very often in object-oriented programming!
Accessor and mutator methods like getName()
and setName(String name)
only work because of side effects.
Fortunately, setters and getters are so easy to implement that there’s no need to get rid of their side effects completely (though some FP purists disagree on this).
In general, side effects in non-trivial methods make implementations hard to read but also hard to write correctly. The following example will illustrate how removing side effects can prevent programming errors.
Let’s dive in with a Container
class, which models a vessel with contents and a maximal capacity:
public class Container {
private static final int CAPACITY = 100;
private int contents;
public Container(int contents) {
this.contents = contents;
}
public int getContents() {
return contents;
}
public void add(int amount) {
if (contents + amount > CAPACITY) {
throw new IllegalArgumentException("Exceeding capacity");
}
contents += amount;
}
public void remove(int amount) {
if (contents < amount) {
throw new IllegalArgumentException("Insufficient contents");
}
contents -= amount;
}
public void transferTo(int amount, Container toContainer) {
remove(amount);
toContainer.add(amount);
}
}
Note how add
and remove
both alter the container’s contents as a side effect.
Moreover, both may throw an exception, another form of side effect.
Specifically, add
checks that the container cannot exceed its capacity, while remove
assures that the requested amount is limited by the current contents.
The method transferTo
uses add
and remove
to transfer a specific amount
from this container to another container.
This is how the Container
class can be used:
Container a = new Container(80);
Container b = new Container(80);
// transfer 20 from container a to container b
a.transferTo(20, b);
System.out.println(a.getContents()); // prints 60
System.out.println(b.getContents()); // prints 100
Interesting things happen when an impossible transfer is requested:
Container c = new Container(80);
Container d = new Container(80);
try {
// the following call will fail
c.transferTo(70, d); (1)
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // prints "Exceeding capacity" (2)
}
System.out.println(c.getContents()); // prints 10 (3)
System.out.println(d.getContents()); // prints 80
1 | An impossible transfer which exceeds capacity of target container d |
2 | As expected, an exception is thrown by the nested call to add |
3 | Programming Error: Even though the transfer failed, the amount is still deduced from container c |
Of course we could invest time in fixing the transferTo
implementation and making sure not to alter container states in case of errors.
But it is a matter of fact that the various side effects make this implementation non-trivial.
Instead of wasting time and energy, let’s instead look at how the same problem could be solved in a much more robust way – functional programming to the rescue!
For starters, we will change the add
and remove
methods to no longer alter the contents by side effect, but instead return a new Container
instance with the changed contents:
public class Container {
private static final int CAPACITY = 100;
private final int contents;
public Container(int contents) {
this.contents = contents;
}
public int getContents() {
return contents;
}
public Container add(int amount) {
if (contents + amount > CAPACITY) {
throw new IllegalArgumentException("Exceeding capacity");
}
return new Container(contents + amount);
}
public Container remove(int amount) {
if (contents < amount) {
throw new IllegalArgumentException("Insufficient contents");
}
return new Container(contents - amount);
}
}
As a next step, we want to improve the transfer scenario. Specifically, we want to make its input and output much more explicit:
-
A transfer needs two containers, a from container and a to container
-
A successful transfer also returns two containers, the from container now has less contents, while the to container now has more contents
In other words, we need a pair of containers as input, and also return a pair of containers.
Let’s introduce a Transfer
class which models a pair of containers:
public class Transfer {
private final Container from;
private final Container to;
public Transfer(Container from, Container to) {
this.from = from;
this.to = to;
}
public Container getFrom() {
return from;
}
public Container getTo() {
return to;
}
}
The new transfer
method now looks like this:
public Transfer transfer(int amount, Transfer transfer) {
Container updatedFrom = transfer.getFrom().remove(amount);
Container updatedTo = transfer.getTo().add(amount);
return new Transfer(updatedFrom, updatedTo);
}
Note that it only reads state from its input parameter.
Consequently, it is decoupled from the Container
object, which we can make explicit by moving it to its own class:
public class ContainerService {
public static Transfer transfer(int amount, Transfer transfer) {
Container updatedFrom = transfer.getFrom().remove(amount);
Container updatedTo = transfer.getTo().add(amount);
return new Transfer(updatedFrom, updatedTo);
}
}
Again, using the static modifier makes it explicit that no instance state is read nor written.
|
This is how transfers can be made with our new design:
Container a = new Container(80);
Container b = new Container(80);
Transfer input = new Transfer(a, b);
Transfer result = ContainerService.transfer(20, input);
System.out.println(result.getFrom().getContents()); // prints 60
System.out.println(result.getTo().getContents()); // prints 100
How will it behave if an invalid transfer is requested?
Container c = new Container(80);
Container d = new Container(80);
try {
Transfer input = new Transfer(c, d);
// the following call will fail
Transfer result = ContainerService.transfer(70, input); (1)
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // prints "Exceeding capacity" (2)
}
System.out.println(c.getContents()); // prints 80 (3)
System.out.println(d.getContents()); // prints 80
1 | An impossible transfer which exceeds capacity of target container d . |
2 | Code flows to the catch clause, result will thus not be available (which makes sense). |
3 | Fixed: If the transfer fails, the amount is no longer deduced from container c |
Phew, this was hard – but the code now works as expected! The functional programming principles of explicitly declaring input parameters and preventing side effects made the code more robust.
5.4. Map / Filter / Reduce
Often, functional programming is associated with the map/filter/reduce design pattern. We have met this pattern in The Java 8 Stream API, here it is again:
Stream<String> words = Stream.of("hello", "world", "java", "is", "cool");
long numberOfFourLetterWords = words.
map(word -> word.length()). // map
filter(length -> length == 4). // filter
count(); // reduce to single result
System.out.println(numberOfFourLetterWords); // prints 2
But what is so "functional" about streams? In fact, the same problem could also be solved without using streams:
List<String> words = Arrays.asList("hello", "world", "java", "is", "cool");
long numberOfFourLetterWords = 0;
for (String word : words) {
if (word.length() == 4) {
numberOfFourLetterWords++;
}
}
System.out.println(numberOfFourLetterWords); // prints 2
This second style is known as imperative programming: Control structures like for
and if
determine how a sequence of statements is executed.
In contrast, streams allow to express the same intentions without using control structures, solely by using functions.
Extracting the lambda expressions to local variables makes this more visible:
Stream<String> words = Stream.of("hello", "world", "java", "is", "cool");
// A function from String -> Integer
Function<String, Integer> mapFunction = word -> word.length();
// A function from Integer -> Boolean
Predicate<Integer> filterFunction = length -> length == 4;
long numberOfFourLetterWords = words.
map(mapFunction).
filter(filterFunction).
count();
System.out.println(numberOfFourLetterWords);
In summary, using the functional approach of map/filter/reduce allows programmers to focus on the heart of the computation rather than on the details of loops, branches, and control flow.
Because the details of loops, branches, and control flow are no longer controlled by the programmer, code written in a functional style is very suitable for parallel execution, e.g. on a distributed cluster (Google’s MapReduce technology is the most prominent example).
5.5. Summary
Functional programming uses functions as the fundamental building blocks of a program. In Java, such functions are built by writing methods or lambda expressions which explicitly declare their input parameters and avoid any side effects. Functional programming makes programs easier to read and prevents certain types of errors.
5.6. Resources
Kris Jenkins: What Is Functional Programming? |
http://blog.jenkster.com/2015/12/what-is-functional-programming.html |
Google MapReduce |
6. Graphical User Interfaces
So far, the programs that we wrote in this course communicated with the world mainly through reading and writing files. In this chapter you will learn how to create a richer user experience through the use of a Graphical User Interface (GUI).
6.1. Introduction
A typical computer consists of…
-
a place (the memory) where you can store data and instructions (a program, or as the hipsters call it today, an app),
-
a little machine (the CPU) that can read the instructions and then load the data and convert it into some piece of useful information,
-
and some devices to communicate with the world, for example a keyboard to type in data or a screen to display the information.
In the beginning, you were able to run a single program that had direct access to all the devices of the computer. Of course no one had the patience to wait for a single program to execute. So they wrote a special program (the operating system) that pretends to run several programs at the same time, by giving each of them in turn a little slice of time and access to the devices. No-one noticed and everyone was happy.
In order to share the screen among the programs, they came up with the idea to allocate a rectangular area of the screen to each program. The program can then draw graphical symbols into that area. They also invented a device that allows users to position a marker on the screen, and to trigger an event at that position by pressing a button on the device. It was called a mouse, and the name stuck even though its resemblance with a little rodent is not so obvious anymore.
These rectangular areas are decorated with a thin frame so that they look like windows. Some symbols are also placed on that frame that allow direct manipulation with the mouse for useful actions such as resizing, minimizing, or closing of a window. A window can be made active by the user, e.g. by clicking on it with the mouse. The window is then in focus and the operating system delivers user events (e.g. mouse clicks, finger swipes, key presses) that happen in this window to the program that is associated with it.
This concept of windows associated with apps, graphical symbols drawn into the windows, and input devices that produce events, is called a graphical user interface (GUI).
On smaller computers, especially those that are carried around a lot and used to place phone calls while other people are trying to work or have a little peace while commuting back from a hard day at work on the train, the windows are usually full screen, switching between windows is done with dedicated buttons, and interaction through directly touching the screen with the fingers. The basic concepts are the same though.
There are efforts to merge the two approaches, mainly by trying to coerce the functionalities on the bigger screens towards working the same way as on the small mobile touch-based screens, pissing off those former users, which ironically are the ones that develop the applications for the small mobile screens in the first place, but this is another story. |
A GUI library allows a program to use the pixels inside these windows, process user events, and use the functionalities offered by the operating system.
A GUI library provides components that can be placed inside a window. Each window has a top-level container that is used to hold all the components, and to connect it with the operating system.
There are different kinds of components:
-
some components are like widgets (e.g., buttons, sliders, menus, labels, text fields, etc.)
-
some components are drawing areas that you can draw into (e.g., lines, circles, rectangles, etc.)
-
some components are containers that contain other components, and that are used to organize the layout of those components
Here are some examples of typical widgets:
Here are some examples of typical containers:
All components are placed inside such container components. Containers are components themselves and can be placed inside containers.
A layout manager provides rules about how to arrange the components inside containers. Typical types of layouts are: grid, box (rows, columns), flow, absolute
Once all components are in place, the program enters an endless loop and waits for events. The operating system and the GUI components generate events if the users interact with them (e.g., mouse clicks, keyboard presses). A program can register a piece of code that gets triggered when there is an event, and that knows how to update the GUI components and the program logic.
In summary, here is how a GUI works:
-
Create components
-
Create some GUI components, group them if desired
-
Define what should happen if events are detected inside a component
-
-
Place the components
-
Ask the operating system for a window
-
Create a container inside this window
-
Place the components into the container
-
Tell the container how to arrange them
-
Make the window visible
-
-
Wait for events
-
Enter an infinite loop and wait for events
-
If an event happens then dispatch it to the component
-
Let the component execute the actions that are defined for this event
-
There are many GUI libraries for the various programming languages, here are some prominent examples:
-
Java: Swing, JavaFX
-
Python: TkInter
-
C++: Qt, WxWidgets
-
C#: WPF
-
Objective-C: Cocoa
6.2. Building GUIs with Java
When programming with Java, there are two options for creating GUIs: Swing and JavaFX. Swing is the original GUI library that came bundled with Java. In recent years, a more modern GUI library was developed and since Java 8 is now also bundled with Java. Swing is not developed any further, and it is generally discouraged to start developing new applications based on Swing. In this course we will learn how to use the JavaFX GUI library.
JavaFX is part of Java 8 SE and is ready to use. The Javadoc documentation for JavaFX however is separate from the Java API documentation. See the Resources section for links.
JavaFX follows the principles outlined in the introduction section.
The top-level concept is represented by the class Stage
. It is a somewhat funny name, but essentially it represents a window provided by the operating system. A Stage
thus can have a title, and provides functionality to influence the decoration (frame, transparency, full screen, etc.) and behavior (resizable, always on top, etc.) of the window.
GUI components can not be directly placed onto a Stage
. We need a container for that, a Scene
.
It determines the size of the rectangle that the window occupies on the screen, and other attributes of the graphics system (anti-aliasing, depth buffer, etc.).
More importantly though, the Scene
is the top of the component hierarchy, which is also called a scene graph for that reason. A Scene
therefore has exactly one Parent
node.
There are two types of nodes, branch nodes (where the tree branches, can contain other nodes) and leaf nodes. The following diagram shows some selected classes of the JavaFX component hierarchy.
But let’s write a simple JavaFX program now.
In order to take care of the infinite event loop, JavaFX provides a class called Application
. It contains the parts of initializing the graphics system and managing the event loop. In order to use this functionality, our program can simply extend the Application
class and inherit from it. Our code to generate and layout the GUI components, and to define how to react to events then goes in the method start()
that we override. The Application
class calls start()
at the right moment, just before entering the event loop.
In order to bootstrap and run the whole thing, the main()
method of our program calls launch()
.
Enough now, show me the code!
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class HelloJavaFX extends Application {
@Override
public void start(Stage stage) {
// Create a GUI component
Button button = new Button("Click me...");
// Define what should happen if an event is detected on this component
button.setOnAction(
actionEvent -> button.setStyle("-fx-background-color: red;")
);
// Create a container into which to place the component
Pane pane = new Pane();
pane.getChildren().add(button);
// Create the scene
Scene scene = new Scene(pane, 300, 200);
// Add the scene to the stage, decorate and show it
stage.setScene(scene);
stage.setTitle("Hello JavaFX!");
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Great, a window with a button!
6.3. Creating and Positioning GUI Components
Now that we have the basic GUI infrastructure in place, let’s have a look at how to create GUI components and how to position them inside a window.
First, we add a second button.
Button button = new Button("Click me...");
Button helpButton = new Button("Help");
Pane pane = new Pane();
pane.getChildren().add(button);
pane.getChildren().add(helpButton);
Scene scene = new Scene(pane, 300, 200);
stage.setScene(scene);
stage.setTitle("Hello JavaFX!");
stage.show();
It works, but they are on top of each other. Not very useful, we need a way to position them inside the window.
The Button
provides methods to define the exact size and position, relocate()
and setPrefSize()
. Let’s try those…
Note that in the interest of brevity, we leave away the part about adding the Scene
to the Stage
from now on. It’s always the same anyway.
Button button = new Button("Click me...");
button.relocate(20, 20);
button.setPrefSize(100, 100);
Button helpButton = new Button("Help");
helpButton.relocate(50, 80);
helpButton.setPrefSize(200, 50);
Pane pane = new Pane();
pane.getChildren().add(button);
pane.getChildren().add(helpButton);
Scene scene = new Scene(pane, 300, 200);
While this gives us full control, it seems rather cumbersome to build up a rich GUI by having to specify each and every pixel individually, and we want our GUI to adapt dynamically to changes in the window size, "responsive" so to speak.
Up to now, we used a Pane
as a container for our buttons. This class does not provide any support for laying out components. There are however a number of descendants of Pane
that are smarter about that. They keep track of all the components that get added to the container, and automatically assign them a position in the window based on some layout strategy. The position is dynamically adapted as new components are added, or the container’s size changes. This is called layout management.
The class StackPane
is such a container component that performs layout management. Let’s see what happens if we use it instead of the vanilla Pane
.
Button button = new Button("Click me...");
Button helpButton = new Button("Help");
StackPane pane = new StackPane();
pane.getChildren().add(button);
pane.getChildren().add(helpButton);
Scene scene = new Scene(pane, 300, 200);
The two buttons are now positioned automatically in the center of the window. And they stay there if the size of the window changes. The layout adapts.
Wonderful, but who wants buttons that are on top of each other?
Let’s try another layout strategy, a FlowPane
. In order to make our GUI a bit richer, we add two more components, two text labels.
Button button = new Button("Click me...");
Button helpButton = new Button("Help");
Label label = new Label("My first button:");
Label label2 = new Label("The second button:");
FlowPane pane = new FlowPane();
pane.getChildren().addAll(label, button, label2, helpButton);
Scene scene = new Scene(pane, 300, 200);
Try resizing the window in various ways, to discover the layout strategy of the FlowPane
. The components are simply placed one next to the other, like words on a line of text. When there is no more space then it wraps around.
The containers and their layout strategies can be configured in various ways. Some properties such as the horizontal and vertical gaps between components are specific to a container and have their own methods (e.g., setHgap()
, setVgap()
). Some properties are more generic and can be changed through the generic setStyle()
method.
A FlowPane can also be put inside a StackPane, so that it is centered in the window and can be inset from the border.
It is not always easy to figure out how to change a desired property, and what properties are available in the first place. The JavaFX API documentation and your favorite search engine are your friends here.
Button button = new Button("Click me...");
Button helpButton = new Button("Help");
Label label = new Label("My first button:");
Label label2 = new Label("The second button:");
FlowPane flowPane = new FlowPane();
flowPane.setHgap(10);
flowPane.setVgap(5);
flowPane.setStyle(
"-fx-background-color: #86d96c;" +
"-fx-border-color: darkorange;" +
"-fx-border-width: 3;" +
"-fx-padding: 30;" +
"-fx-border-insets: 50;" +
"-fx-background-insets: 50;"
);
flowPane.getChildren().addAll(label, button, label2, helpButton);
StackPane stackPane = new StackPane(flowPane);
StackPane.setMargin(flowPane, new Insets(20));
Scene scene = new Scene(stackPane, 300, 200);
Another very useful pair of containers are HBox and VBox. They represent simple row and column layouts, and can be nested to produce sophisticated layouts with many components. Divide and conquer!
To debug layout issues, it has proven useful to temporarily color the backgrounds of the individual containers.
Button button = new Button("Click me...");
Button helpButton = new Button("Help");
Label label = new Label("My first button:");
Label label2 = new Label("The second button:");
HBox firstLine = new HBox();
firstLine.getChildren().addAll(label, button);
firstLine.setAlignment(Pos.CENTER);
firstLine.setSpacing(10);
firstLine.setPadding(new Insets(5, 5, 5, 5));
firstLine.setStyle("-fx-background-color: cornflowerblue;");
HBox secondLine = new HBox();
secondLine.getChildren().addAll(label2, helpButton);
secondLine.setAlignment(Pos.CENTER);
secondLine.setSpacing(10);
secondLine.setPadding(new Insets(5, 5, 5, 5));
secondLine.setStyle("-fx-background-color: seagreen;");
VBox vBox = new VBox();
vBox.getChildren().addAll(firstLine, secondLine);
vBox.setAlignment(Pos.CENTER);
vBox.setSpacing(10);
vBox.setPadding(new Insets(5, 5, 5, 5));
vBox.setStyle("-fx-background-color: darkorange;");
StackPane pane = new StackPane();
pane.getChildren().add(vBox);
Scene scene = new Scene(pane, 300, 200);
Here are the relevant sections in the official tutorials:
6.4. Handling User Events
When you use your computer’s input devices (mouse, keyboard, etc.) then all sorts of events are triggered. Which GUI component the events are delivered to by the GUI library depends on many factors, but for most practical purposes, it’s the component that currently has the focus (i.e., the mouse pointer is over it, the text field is selected, etc.).
JavaFX provides a sophisticated system of event filters and event handlers, but for standard applications, there are simple convenience methods.
Event handling code can be registered directly on a component by means of the setOnXXX()
methods, where XXX is the type of event that should be handled. Examples are:
-
setOnAction()
-
setOnMousePressed()
-
setOnKeyPressed()
Here is a simple example of a GUI that lets users enter a text into an input field, converts it into all uppercase letters, and then displays the result. The Clear button clears the input field. Note how the input field changes background color as the mouse pointer moves over it.
// Create the GUI components
Label inputLabel = new Label("Type in some text and press Enter:");
TextField inputTextField = new TextField();
Button clearButton = new Button("Clear");
Label label = new Label("You wrote:");
Label resultLabel = new Label();
// Define what events should be handled, and what to do about them
inputTextField.setOnAction(actionEvent -> {
resultLabel.setText(inputTextField.getText().toUpperCase());
label.setStyle("-fx-font-weight: bold");
label.setTextFill(Color.RED);
});
clearButton.setOnAction(actionEvent -> {
inputTextField.setText("");
resultLabel.setText("");
label.setStyle("-fx-font-weight: normal");
label.setTextFill(Color.GREY);
});
inputTextField.setOnMouseEntered(event -> inputTextField.setStyle("-fx-background-color: lightblue"));
inputTextField.setOnMouseExited(event -> inputTextField.setStyle("-fx-background-color: white"));
// Position the components
HBox firstLine = new HBox();
firstLine.getChildren().addAll(inputLabel, inputTextField, clearButton);
firstLine.setAlignment(Pos.CENTER);
firstLine.setSpacing(10);
firstLine.setPadding(new Insets(5, 5, 5, 5));
HBox secondLine = new HBox();
secondLine.getChildren().addAll(label, resultLabel);
secondLine.setAlignment(Pos.CENTER);
secondLine.setSpacing(10);
secondLine.setPadding(new Insets(5, 5, 5, 5));
VBox vBox = new VBox();
vBox.getChildren().addAll(firstLine, secondLine);
vBox.setAlignment(Pos.CENTER);
vBox.setSpacing(10);
vBox.setPadding(new Insets(5, 5, 5, 5));
StackPane pane = new StackPane();
pane.getChildren().add(vBox);
// Create the scene and kick it off
Scene scene = new Scene(pane, 500, 200);
stage.setScene(scene);
stage.setTitle("Hello JavaFX!");
stage.show();
Here are the relevant sections in the official tutorials:
6.5. Shapes’n’Properties
So far we have looked at Regions. This corresponds to the leftmost branch in our sketch of the JavaFX component hierarchy.
The two other branches are Shape
and Canvas
.
In this section we will look at Shape
.
We will get to Canvas
in the next chapter.
The main difference that we can see from looking at the component hierarchy is that Shape
does not sub-class Parent
.
Therefore, Shape
and its children can not act as containers.
This is because they represent basic graphic shapes such as Line
, Rectangle
, Circle
, or the generic Polygon
.
Shapes have common properties such as the stroke (color, thickness, etc.) to draw the outline, and the pattern (color, pattern, etc.) to fill the inside.
Shapes can be added to a pane with a layout manager, just as we have seen with the other descendants of Node
.
More commonly though, we want to place them at exact absolute positions (e.g., to draw some data-driven graph that is made up from generic shapes).
For this, the plain Pane
comes in handy, since it does not perform any layout at all.
Here is a simple circle:
// Create the shape (a circle at position 150,150 and radius 100)
Circle circle = new Circle(150, 150, 100);
Pane basicPane = new Pane();
basicPane.setPrefSize(500, 500);
basicPane.setStyle("-fx-background-color: lightblue");
basicPane.getChildren().add(circle);
// Layout the components
VBox vBox = new VBox(basicPane);
// Add scene to stage and show it
Scene scene = new Scene(vBox, 500, 500);
stage.setScene(scene);
stage.setTitle("Hello JavaFX!");
stage.show();
Try resizing the window. The circle stays put.
Now let’s make things more interactive, and use a Slider
to change the radius of the circle.
We need to obtain the current value of the slider each time it moves a bit to the right or to the left.
So far we were able to use the setOnAction()
convenience method to handle events on components.
For the slider this does not work, since the movement of the slider does not simply trigger an action like a button, but changes a value.
When we have a closer look at the documentation of the Slider
class, then we notice that the value of the slider is not a simple double value, but a DoubleProperty
.
Properties are like simple values on steroids.
They are classes that wrap the simple values and provide additional functionality, in particular the possibility to observe them and get notified when the value changes.
The following code shows how to do that. Four alternative versions are shown, from the most explicit to the most concise. They all do exactly the same though. It’s just a different syntax.
// Create the slider
Slider slider = new Slider(0, 200, 100);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
// Create the shape
Circle circle = new Circle(150, 150, slider.getValue());
Pane basicPane = new Pane();
basicPane.setPrefSize(500, 500);
basicPane.setStyle("-fx-background-color: lightblue");
basicPane.getChildren().add(circle);
// Define the logic
// Version 1: Explicitly create an instance of the ChangeListener class and add it to the property
ChangeListener<Number> changeListener = new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number t1) {
circle.setRadius(slider.getValue());
}
};
slider.valueProperty().addListener(changeListener);
// Version 2: Create an anonymous instance and add it to the property
slider.valueProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
circle.setRadius(slider.getValue());
}
});
// Version 3: Directly add the code in the form of a lambda expression
slider.valueProperty().addListener((observableValue, oldValue, newValue) -> {
circle.setRadius(slider.getValue());
});
// Version 4: The most concise, a single line lambda expression
slider.valueProperty().addListener((observableValue, oldValue, newValue) -> circle.setRadius(slider.getValue()));
// Layout the components
VBox vBox = new VBox(basicPane, slider);
// Add scene to stage and show it
Scene scene = new Scene(vBox, 500, 500);
stage.setScene(scene);
stage.setTitle("Hello JavaFX!");
stage.show();
Because observing a property, and then adapting another property if there is a change, is something that is done quite often, there is additional functionality to make the developer’s life easier.
Properties can be directly bound to another property through the use of the bind()
method.
In our example, since the radius of the circle is also a property, we can directly bind it to the slider value.
See below for how short this has become now:
// Create the slider
Slider slider = new Slider(0, 200, 100);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
// Create the shape
Circle circle = new Circle(150, 150, slider.getValue());
Pane basicPane = new Pane();
basicPane.setPrefSize(500, 500);
basicPane.setStyle("-fx-background-color: lightblue");
basicPane.getChildren().add(circle);
// Define the logic
circle.radiusProperty().bind(slider.valueProperty());
// Layout the components
VBox vBox = new VBox(basicPane, slider);
// Add scene to stage and show it
Scene scene = new Scene(vBox, 500, 500);
stage.setScene(scene);
stage.setTitle("Hello JavaFX!");
stage.show();
Finally, let’s add some mouse interaction, just because we can. And because it’s fun.
// Create the slider
Slider slider = new Slider(0, 200, 100);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
// Create the shape
Circle circle = new Circle(150, 150, slider.getValue());
Pane shapePane = new Pane();
shapePane.setPrefSize(500, 500);
shapePane.setStyle("-fx-background-color: lightblue");
shapePane.getChildren().add(circle);
// Define the logic
circle.radiusProperty().bind(slider.valueProperty());
// Center circle on the pane
circle.centerXProperty().bind(shapePane.widthProperty().divide(2));
circle.centerYProperty().bind(shapePane.heightProperty().divide(2));
circle.setOnMouseEntered(event -> {
circle.setStrokeWidth(5);
circle.setStroke(Color.RED);
});
circle.setOnMouseExited((MouseEvent event) -> circle.setStrokeWidth(0));
circle.setOnMouseClicked(event -> {
if (circle.getFill().equals(Color.YELLOW)) {
circle.setFill(Color.BLACK);
} else {
circle.setFill(Color.YELLOW);
}
});
// Layout the components
VBox vBox = new VBox(shapePane, slider);
// Add scene to stage and show it
Scene scene = new Scene(vBox, 500, 500);
stage.setScene(scene);
stage.setTitle("Hello JavaFX!");
stage.show();
Here are the relevant sections in the official tutorials:
-
Using JavaFX Properties and Binding (Caution: quite technical)
6.6. Custom Drawing with a Canvas
Up to here we have covered all the branches in the sketch of the JavaFX component hierarchy, except the rightmost: the Canvas
.
It’s not a Parent
and it’s not a Shape
.
What is it?
The Canvas
is a component that provides an empty rectangular image that you can write to.
It provides a GraphicsContext
that performs the rendering. You can call the methods of the GraphicsContext
to define its rendering state, and to issue drawing commands.
The basic steps are thus:
-
create a
Canvas
-
get a
GraphicsContext
from it -
issue drawing commands to draw basic geometric objects like lines and rectangles, etc.
The Canvas
can be added to the scene graph (e.g., layout containers like the panes) like any other component that extends Node
.
What is the difference to drawing with Shape
as we have seen in the previous section (e.g., Circle
, Rectangle
, etc.)?
If we use a Shape
node for drawing then each Shape
is an individual object that extends the Node
class.
With this comes a lot of functionality such as geometric transformations, bounding rectangles, styling, and event listening infrastructure.
Useful, but sometime also a burden.
If we use a Canvas
for drawing then there is only a single object. Drawing commands that are issued get converted to pixels on the canvas immediately. A shape drawn like this therefore looses its identity after it is drawn. It can not be addressed and modified any more. It’s just pixels. If you want to change the color of a circle then you have to change the state of the rendering engine, and draw it again.
Why would anyone want to do that?
-
For drawing complex shapes
-
For performance reasons if there are a large number of shapes that don’t really need an identity
-
If the data model underlying the drawing changes often (e.g. because objects appear or disappear)
The Canvas
works like a state machine.
It has rendering attributes (e.g., fill color, stroke color) that determine the current state of the rendering engine.
All drawing commands are drawn using the current state.
The state stays like it is until it is changed.
Here is a simple example that draws a couple of lines onto the canvas:
// Create the canvas
Canvas canvas = new Canvas(300, 300);
// Get the graphic context
GraphicsContext gc = canvas.getGraphicsContext2D();
// Draw a red line and then two blue lines, change the rendering state in-between
gc.setStroke(Color.RED);
gc.strokeLine(200, 100, 100, 200);
gc.setStroke(Color.BLUE);
gc.strokeLine(100, 100, 200, 200);
gc.strokeLine(150, 100, 250, 200);
// Create the infrastructure
Pane rootPane = new Pane();
rootPane.getChildren().add(canvas);
Scene scene = new Scene(rootPane);
stage.setScene(scene);
stage.setTitle("Hello JavaFX!");
stage.show();
The documentation of the GraphicsContext class has a list of all rendering attributes. The method summary also tells you about all the available drawing commands.
Let’s try this out on a more interesting example. We want to create a number of points that are randomly distributed in the plane. When we move the mouse around in this plane, then we want to highlight all the points that are within a certain range of the mouse pointer.
We model this problem with a very simple data structure, a List<Point2D>
that holds the x and y coordinates of our points.
Point2D is an existing class in the javafx.geometry package
|
Here is how we create the points:
private List<Point2D> createPoints(int count, int width, int height) {
Supplier<Point2D> randomPointCreator = () -> {
double x = Math.random() * width;
double y = Math.random() * height;
return new Point2D(x, y);
};
Stream<Point2D> randomPoints = Stream.generate(randomPointCreator);
return randomPoints.limit(count).collect(toList());
}
The Point2D
class already knows how to calculate the distance to another point.
Finding all the points that are within a given range of a center point is thus very easy:
private List<Point2D> findPointsWithinRange(List<Point2D> points, Point2D center, double range) {
Predicate<Point2D> isWithinRange = point -> center.distance(point) < range;
return points.stream().filter(isWithinRange).collect(toList());
}
Now that the domain logic is in place, we can turn our attention to the drawing. First the points. Note how we don’t have to create any drawing objects (i.e., shapes). We simply iterate over our data model, and issue the appropriate drawing commands to get a custom drawing of the data (the points in this case). The custom drawing is very simple in this example, a filled circle, but we could replace this on the fly with something more elaborate.
private void drawPoints(GraphicsContext gc, List<Point2D> points) {
gc.setFill(Color.GREY);
for (Point2D point : points) {
gc.fillOval(point.getX(), point.getY(), POINT_SIZE, POINT_SIZE);
}
}
Next, we overdraw the highlighting for the things that are within range of the mouse.
private void drawProbing(GraphicsContext gc, List<Point2D> points, Point2D cursorPosition, double range) {
List<Point2D> pointsWithinRange = findPointsWithinRange(points, cursorPosition, range);
gc.setStroke(Color.RED);
for (Point2D point : pointsWithinRange) {
gc.strokeLine(cursorPosition.getX(), cursorPosition.getY(), point.getX(), point.getY());
}
}
It turns out that we also need to draw the background, as this is not cleared automatically between drawings. Of course.
private void drawBackground(GraphicsContext gc) {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
}
Now we are done and we can assemble everything into an application. It just redraws everything when the mouse is moved over the canvas. That’s it.
Note how the core application code reads almost like a story. This is because we were careful about isolating functionality into separate methods, with method and variable names that tell what they do.
// Create the canvas and get the graphic context
Canvas canvas = new Canvas(WIDTH, HEIGHT);
GraphicsContext gc = canvas.getGraphicsContext2D();
// Create the data model (the points)
List<Point2D> points = createPoints(POINT_COUNT, WIDTH, HEIGHT);
// Draw the points initially
drawBackground(gc);
drawPoints(gc, points);
// Refresh the display when the mouse is moved over the canvas
canvas.setOnMouseMoved(event -> {
drawBackground(gc);
drawPoints(gc, points);
Point2D cursorPosition = new Point2D(event.getX(), event.getY());
drawProbing(gc, points, cursorPosition, PROBING_RANGE);
});
// Create the infrastructure
Pane rootPane = new Pane();
rootPane.getChildren().add(canvas);
Scene scene = new Scene(rootPane);
stage.setScene(scene);
stage.setTitle("Hello JavaFX Drawing!");
stage.show();
We use static constants to avoid magic numbers and concentrate everything that can be customized in one single place.
private static final int WIDTH = 500;
private static final int HEIGHT = 500;
private static final int PROBING_RANGE = 50;
private static final int POINT_COUNT = 200;
private static final int POINT_SIZE = 3;
Finally, here is the result:
Try it out! It is mesmerizing to move the mouse around and follow the spider…
Here are the relevant sections in the official tutorials:
6.7. Resources
JavaFX API Documentation (on-line) | |
JavaFX API Documentation (Download) |
http://www.oracle.com/technetwork/java/javase/documentation/jdk8-doc-downloads-2133158.html |
Java Client Technologies Tutorials |
http://docs.oracle.com/javase/8/javase-clienttechnologies.htm |
7. Appendix
7.1. JavaFX Chart Tips & Tricks
This section provides some help when working with JavaFX Chart
components.
Individual topics are inspired by the requirements of the prog-II-project.
7.1.1. Charts with points & lines
The ScatterChart
component can be used to display data points in a series.
Unfortunately, it does not support line connections.
One simple approach to creating a chart with points and toggleable lines, is to stack a ScatterChart
and a LineChart
.
Toggling the visibility of the LineChart
will then show or hide the line as required.
The StackPane
class provides layout management to stack multiple layers on top of each other.
Note that the chart in the top layer must have a transparent background in order for the underlying chart to be visible:
LineChart<Number, Number> lineChart = createLineChart(model);
ScatterChart<Number, Number> scatterChart = createScatterChart(model);
scatterChart.lookup(".chart-plot-background").setStyle("-fx-background-color: transparent");
StackPane stackPane = new StackPane(lineChart, scatterChart);
Alternatively, one could use a LineChart
with two redundant data series (one for the points, one for the line).
With this approach, fancier CSS is needed to control the visibility to the points and lines.
Further details are provided on Stack Overflow.
7.1.2. Line connection order
By default, a LineChart
sorts its values by the natural order of its x-axis.
Often, it is desirable to connect the points in the order in which they were added to the data series.
This can be achieved by turning off axis sorting:
lineChart.setAxisSortingPolicy(LineChart.SortingPolicy.NONE);
7.1.3. Controlling point appearance through custom nodes
The ScatterChart
component uses default shapes to display individual data points.
While it is possible to style these shapes through CSS, it is also possible to use any Node
class for point rendering.
Using a Circle
node allows to programmatically control the radius and color of data points:
XYChart.Series<Number, Number> series = new XYChart.Series<>();
for (int i = 0; i < valueCount; i++) {
Double x = xValues.get(i);
Double y = yValues.get(i);
XYChart.Data<Number, Number> dataPoint = new XYChart.Data<>(x, y);
Circle circle = new Circle(); (1)
circle.setRadius(10); (2)
circle.setFill(Color.PINK); (3)
dataPoint.setNode(circle); (4)
series.getData().add(dataPoint);
}
1 | Create a custom Circle |
2 | Control its radius |
3 | Control its color |
4 | Tell JavaFX to use the custom circle for rendering |
As usual, using circle.radiusProperty().bind(..) and circle.fillProperty().bind(..) instead of the simple setters allows to dynamically update the values based on the state of e.g. a Slider or a ColorPicker .
|
7.1.4. Auto-ranging axes
By default, a NumberAxis
automatically adjusts its range based on the current data series.
Unfortunately, the axis has a setting to always include the 0
value, which can lead to confusing auto-ranging behavior.
It might thus be desirable to turn off this setting:
NumberAxis axis = new NumberAxis();
axis.setForceZeroInRange(false);