Java Programming Exercises Volume Two Java Standard Library
Java Programming Exercises Volume Two Java Standard Library
Take the next step in raising your coding skills and dive into the intricacies of Java Standard Libraries.
You will continue to raise your coding skills, and test your Java knowledge on tricky programming tasks,
with the help of the pirate Captain CiaoCiao. This is the second of two volumes which provide you with
everything you need to excel in your Java journey, including tricks that you should know in detail as a
professional, as well as intensive training for clean code and thoughtful design that carries even complex
software.
Features:
With numerous best practices and extensively commented solutions to the tasks, these books provide the
perfect workout for professional software development with Java.
Java Programming Exercises
Volume Two: Java Standard Library
Christian Ullenboom
Designed cover image: Mai Loan Nguyen Duy, Rheinwerk Verlag GmbH
©2023 Christian Ullenboom. First published in the German language under the title “Captain CiaoCiao erobert Java”
(ISBN 978‑3‑8362‑8427‑1) by Rheinwerk Verlag GmbH, Bonn, Germany.
Reasonable efforts have been made to publish reliable data and information, but the author and publisher cannot
assume responsibility for the validity of all materials or the consequences of their use. The authors and publishers
have attempted to trace the copyright holders of all material reproduced in this publication and apologize to
copyright holders if permission to publish in this form has not been obtained. If any copyright material has not been
acknowledged please write and let us know so we may rectify in any future reprint.
Except as permitted under U.S. Copyright Law, no part of this book may be reprinted, reproduced, transmitted, or
utilized in any form by any electronic, mechanical, or other means, now known or hereafter invented, including
photocopying, microfilming, and recording, or in any information storage or retrieval system, without written
permission from the publishers.
For permission to photocopy or use material electronically from this work, access www.copyright.com or contact the
Copyright Clearance Center, Inc. (CCC), 222 Rosewood Drive, Danvers, MA 01923, 978‑750‑8400. For works that are
not available on CCC please contact mpkbookspermissions@tandf.co.uk
Trademark notice: Product or corporate names may be trademarks or registered trademarks and are used only for
identification and explanation without intent to infringe.
DOI: 10.1201/9781003495550
Typeset in Times
by codeMantra
Introduction 1
Previous Knowledge and Target Audience 1
Working with the Book 2
The Suggested Solutions 2
Use of the Book 3
Required Software 3
Used Java Version in the Book 4
JVM 4
Development Environment 4
Conventions 4
Helping Captain CiaoCiao and Bonny Brain 5
v
vi Contents
2 Mathematics 30
The Class Math 30
Quiz: Rule of Thumb ⭑ 30
Check If Tin Tin Cheated on Rounding ⭑ 31
Huge and Very Precise Numbers 32
Calculate Arithmetic Mean of a Large Integer ⭑ 32
Number by Number over the Phone ⭑ 33
Develop Class for Fractions and Truncate Fractions ⭑⭑ 34
Suggested Solutions 35
Quiz: Rule of Thumb 35
Check If Tin Tin Cheated on Rounding 36
Calculate Arithmetic Mean of a Large Integer 37
Number by Number over the Phone 38
Develop Class for Fractions and Truncate Fractions 38
Epilogue 307
Code Golf Stack Exchange 307
Project Euler 307
Daily Programmer 307
Rosetta Code 308
About the Author
Christian Ullenboom s tarted his programming journey at the tender age of ten, typing his first lines of
code into a C64. After mastering assembler programming and early BASIC extensions, he found his call‑
ing on the island of Java, following his studies in computer science and psychology. Despite indulging in
Python, JavaScript, TypeScript, and Kotlin vacations, he remains a savant of all things Java.
For over 20 years, Ullenboom has been a passionate software architect, Java trainer (check out http://
www.tutego.com), and IT specialist instructor. His expertise has resulted in a number of online video
courses and reference books:
Christian Ullenboom has been spreading Java love through his books for years, earning him the coveted
title of Java Champion from Sun (now Oracle) way back in 2005. Only a select few—about 300 world‑
wide—have achieved this status, making him a true Java superstar.
As an instructor, Ullenboom understands that learning by doing is the most effective way to master a
skill. So, he has compiled a comprehensive catalog of exercises that accompany his training courses. This
book features a selection of those exercises, complete with documented solutions.
His roots are in Sonsbeck, a small town in the Lower Rhine region of Germany.
xiii
Introduction
Many beginners in programming often ask themselves, “How can I strengthen my skills as a developer?
How can I become a better programmer?” The answer is simple: study, attend webinars, learn, repeat,
practice, and discuss your work with others. Many aspects of programming are similar to learning new
skills. Just as a book can’t teach you how to play a musical instrument, watching the Fast and the Furious
movie series won’t teach you how to drive. The brain develops patterns and structures through repeated
practice. Learning a programming language and a natural language have many similarities. Consistent
use of the language, and the desire and need to express and communicate in it (just as you need to do so
to order a burger or a beer), leads to gradual improvement in skills.
Books and webinars on learning a programming language are available, but reading, learning, prac‑
ticing, and repeating are just one aspect of becoming a successful software developer. To create effective
software solutions, you need to creatively combine your knowledge, just as a musician regularly practices
finger exercises and maintains their repertoire. The more effective your exercises are, the faster you will
become a master. This book aims to help you progress and gain more hands‑on experience.
Java 21 declares more than 2300 classes, about 1400 interfaces, around 140 enumerations, approxi‑
mately 500 exceptions, and a few annotation types and records are added to this. However, in practical
terms, only a small subset of these types proves to be relevant. This book selects the most important types
and methods for tasks, making them motivating, and following Java conventions. Alternative solutions
and approaches are also presented repeatedly. The goal is to make non‑functional requirements clear
because the quality of programs is not just about “doing what it should.” Issues such as correct indentation,
following naming conventions, proper use of modifiers, best practices, and design patterns are essential.
The proposed solutions aim to demonstrate these principles, with the keyword being Clean Code.
The book is centered around tasks and fully documented solutions, with detailed explanations of Java
peculiarities, good object‑oriented programming practices, best practices, and design patterns. The exer‑
cises are best solved with a textbook, as this exercise book is not a traditional textbook. A useful approach
is to work through a topic with a preferred textbook before attempting the exercises that correspond to it.
DOI: 10.1201/9781003495550-1 1
2 Java Programming Exercises
The first set of tasks are designed for programming beginners who are new to Java. As you gain more
experience with Java, the tasks become more challenging. Therefore, there are tasks for both beginners
and advanced developers.
The Java Standard Edition is augmented by numerous frameworks and libraries. However, this
exercise book does not cover specific libraries or Java Enterprise frameworks like Jakarta EE or Spring
(Boot). There are separate exercise books available for these environments. Additionally, the book does
not require the use of tools like profiling tools, as these are beyond the scope of the book.
1 star ★: Simple exercises, suitable for beginners. They should be easy to solve without much
effort. Often only transfer of knowledge is required, for example, by writing down things that
are in a textbook differently.
2 stars ★★: The effort is higher here. Different techniques have to be combined. Greater creativity
is required.
3 stars ★★★: Assignments with three stars are more complex, require recourse to more prior
knowledge, and sometimes require research. Frequently, the tasks can no longer be solved with
a single method, but require multiple classes that must work together.
REQUIRED SOFTWARE
While solving a task with just a pen and paper is possible in theory, modern software development
requires the proper use of tools. Knowing programming language syntax, object‑oriented modeling,
and libraries is just the tip of the iceberg. Understanding the JVM, using tools like Maven and Git for
version management, and becoming proficient in an IDE are all crucial aspects of professional software
development. Some developers can even perform magic in their IDE, generating code and fixing bugs
automatically.
4 Java Programming Exercises
JVM
If we want to run Java programs, we need a JVM. In the early days, this was easy. The runtime environ‑
ment first came from Sun Microsystems, later from Oracle, which took over Sun. Today, it is much more
confusing. Although a runtime environment can still be obtained from Oracle, the licensing terms have
changed, at least for Java 8 up to Java 16. Testing and development are possible with the Oracle JDK,
but not in production. In this case, Oracle charges license fees. As a consequence, various institutions
compile their own runtime environments from the OpenJDK, the original sources. The best known are
Eclipse Adoptium (https://adoptium.net/), Amazon Corretto (https://aws.amazon.com/de/corretto), Red
Hat OpenJDK (https://developers.redhat.com/products/openjdk/overview) and others such as those from
Azul Systems or Bellsoft. There is no specific distribution that readers are required to follow.
Development Environment
Java source code is just plain text, so technically a simple text editor is all you need. However, relying
solely on Notepad or vi for productivity is like trying to win a race on a tricycle. Modern integrated
development environments support us with many tasks: color highlighting of keywords, automatic code
completion, intelligent error correction, insertion of code blocks, visualization of states in the debug‑
ger, and much more. It is therefore advisable to use a full development environment. Four popular IDEs
are: IntelliJ, Eclipse, Visual Studio Code, and (Apache) NetBeans. Just like with Java runtime environ‑
ments, the choice of IDE is left to the reader. Eclipse, NetBeans, and Visual Studio Code are all free
and open‑source, while IntelliJ Community Edition is also free, but the more advanced IntelliJ Ultimate
Edition will cost you some cash.
Halfway through the book, we delve into implementing project dependencies using Maven in a few
places.
CONVENTIONS
Code is written in fix width font, filenames are italicized. To distinguish methods from attributes,
methods always have a pair of parentheses, such as in “the variable max contains the maximum” or “it
returns max() the maximum”. Since methods can be overloaded, either the parameter list is named, as in
equals(Object), or an ellipsis abbreviates it, such as in “various println(…) methods”. If a group
of identifiers is addressed, * is written, like print*(...) prints something on the screen.
In the suggested solutions, there are usually only the relevant code snippets, so as not to blow up the
book volume. The name of the file is mentioned in the listing caption, like this:
VanillaJava.java
class VanillaJava { }
• Introduction 5
Sometimes, we need to flex our terminal muscles and execute programs from the command line (also
known as console or shell). Since each command‑line program has its own prompt sequence, it is symbol‑
ized here in the book with a $. The user’s input is set in bold. Example:
$ java ‑version
java version "17.0.5" 2022‑10‑18 LTS
Java(TM) SE Runtime Environment (build 17.0.5+9‑LTS‑191)
Java HotSpot(TM) 64‑Bit Server VM (build 17.0.5+9‑LTS‑191, mixed mode,
sharing)
If the Windows command line is explicitly meant, the prompt character > is set:
> netstat ‑e
Interface Statistics
Received Sent
• java.util.Formatter
• java.lang.String
• java.util.regex.Pattern
• java.util.regex.Matcher
• java.util.Scanner
FORMAT STRINGS
There are different ways in Java to format strings, numbers, and temporal data as a string. In the package
java.text, you can find the classes MessageFormat, DateFormat, and DecimalFormat as well
as the class Formatter and in String the method String.format(…). The next tasks can be solved
using the formatting strings from Formatter in a rather easy way.
$ ascii
Usage: ascii [‑adxohv] [‑t] [char‑alias...]
‑t = one‑line output ‑a = vertical format
‑d = Decimal table ‑o = octal table ‑x = hex table ‑b binary table
‑h = This help screen ‑v = version information
Prints all aliases of an ASCII character. Args may be chars, C \‑escapes,
English names, ^‑escapes, ASCII mnemonics, or numerics in decimal/octal/hex.
6 DOI: 10.1201/9781003495550-2
1 • Advanced String Processing 7
Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex
0 00 NUL 16 10 DLE 32 20 48 30 0 64 40 @ 80 50 P 96 60 ' 112 70 p
1 01 SOH 17 11 DC1 33 21 ! 49 31 1 65 41 A 81 51 Q 97 61 a 113 71 q
2 02 STX 18 12 DC2 34 22 " 50 32 2 66 42 B 82 52 R 98 62 b 114 72 r
3 03 ETX 19 13 DC3 35 23 # 51 33 3 67 43 C 83 53 S 99 63 c 115 73 s
4 04 EOT 20 14 DC4 36 24 $ 52 34 4 68 44 D 84 54 T 100 64 d 116 74 t
5 05 ENQ 21 15 NAK 37 25 % 53 35 5 69 45 E 85 55 U 101 65 e 117 75 u
6 06 ACK 22 16 SYN 38 26 & 54 36 6 70 46 F 86 56 V 102 66 f 118 76 v
7 07 BEL 23 17 ETB 39 27 ' 55 37 7 71 47 G 87 57 W 103 67 g 119 77 w
8 08 BS 24 18 CAN 40 28 ( 56 38 8 72 48 H 88 58 X 104 68 h 120 78 x
9 09 HT 25 19 EM 41 29 ) 57 39 9 73 49 I 89 59 Y 105 69 i 121 79 y
10 0A LF 26 1A SUB 42 2A * 58 3A : 74 4A J 90 5A Z 106 6A j 122 7A z
11 0B VT 27 1B ESC 43 2B + 59 3B ; 75 4B K 91 5B [ 107 6B k 123 7B {
12 0C FF 28 1C FS 44 2C , 60 3C < 76 4C L 92 5C \ 108 6C l 124 7C |
13 0D CR 29 1D GS 45 2D ‑ 61 3D = 77 4D M 93 5D ] 109 6D m 125 7D }
14 0E SO 30 1E RS 46 2E . 62 3E > 78 4E N 94 5E ^ 110 6E n 126 7E ~
15 0F SI 31 1F US 47 2F / 63 3F ? 79 4F O 95 5F _ 111 6F o 127 7F DEL
However, their Aye Phone doesn’t have such a widescreen, and the first two blocks are not visible char‑
acters anyway.
Task:
• Write a program that prints all ASCII characters from position 32 to 127 in the same formatting
as the Unix program ascii does.
• At position 127, write DEL.
8 Java Programming Exercises
Aligned Outputs ⭑
Captain CiaoCiao needs a table of the following type for a listing:
Task:
Later, we will also use regular expressions to specify separators and decompose strings.
Task:
Example:
Task:
• Given is a line from the scan with numbers from the format shown. Convert the numbers to an
integer.
• There could be missing spaces after the last digit, and there could be several spaces between
the large characters.
Example:
If you want to play around with the strings, you can find a way at https://patorjk.com/software/taag/#p=
display&f=Alphabet&t=0123456789.
Task:
Example:
We raid the harbor at 11:00 PM and meet on the amusement mile at 1:30 AM.
Bonny Brain doesn’t like that; she wants only the 24‑hour count of Military Time.
Task:
• Write a converter that converts strings with AM/PM (case‑insensitive, even with periods) to
Military Time. As a reminder, 12:00 AM is 00:00, and 12:00 PM is 12:00.
Examples:
The lines are separated with a line break. There are four valid separator symbols or sequences:
LF is the abbreviation for “line feed” and CR for “carriage return”; in old teleprinters, CR moved the
carriage to the first column, and LF pushed the paper up.
Traditionally, DOS and Microsoft Windows use the combination \r\n, while Unix systems use \n.
Task:
• Break a newline‑separated string into four lines, and assign the lines to the variables name,
street, city, and country.
• If a fourth line with the country name is not given, let country be "Drusselstein".
• Reassemble the line as a CSV line separated by semicolons.
Examples:
erehW did eht etarip esahcrup sih kooh? tA eht dnah‑dnoces pohs!
Task:
1. Break the string into words. Separators of words are spaces and punctuation marks.
2. Turn over all the words one by one.
3. Output the words one after the other, separated by a space. The punctuation marks and other
separators are not critical.
Example:
• "erehW did eht etarip esahcrup sih kooh? tA eht dnah‑dnoces pohs!"
→ "Where did the pirate purchase his hook At the hand second shop"
Goldy Goldfish has the task of checking the relation signs <, > and =.
Task:
• Write a program that gets a string like the one in the example and returns true if all relation
signs are correct, and false otherwise.
Examples:
The lines are still described with numbers. Thus, A2 stands for cell 1–2.
14 Java Programming Exercises
Since Captain CiaoCiao has its difficulties with A1 notation, the specification is to be converted back
to numeric columns and rows.
Task:
Examples:
• parseA1Notation("A1") → [1, 1]
• parseA1Notation("Z2") → [26, 2]
• parseA1Notation("AA34") → [27, 34]
• parseA1Notation("BZ") → [0, 0]
• parseA1Notation("34") → [0, 0]
• parseA1Notation(" ") → [0, 0]
• parseA1Notation("") → [0, 0]
20.091612,‑155.676695
23.087301,‑73.643472
21.305452,‑71.690421
Task:
• Create a CSV file manually. It should contain several lines with coordinates; a comma sepa‑
rates the coordinates.
• A Java program should read the CSV file and output an HTML file with SVG for the polygon
course on the screen.
• Use the class Scanner to parse the file. Make sure to initialize the Scanner with
useLocale(Locale.ENGLISH) if your locale is not English by default.
A simple lossless compression is run‑length encoding. The idea is to combine a sequence of identical
symbols so that only the number and the symbol are written. The graphic format GIF, for example, uses
this form of compression. Therefore, images with many monochrome lines are also smaller than, for
example, images in which each pixel has a different color.
The next task is about run‑length encoding. Suppose a string consists of a sequence of . (dot) and ‑
(minus sign), such as:
‑‑....‑‑‑‑‑‑‑‑..‑
To shorten the length of strings, we can first write the symbol followed by the number of symbols. The
string with 17 characters could be shortened to the following string with 9 characters:
‑2.4‑8.2‑
Task:
Extensions:
SUGGESTED SOLUTIONS
First, the program writes the table header, in which the string “Dec Hex” for the six columns is set six
times in a row with spacing.
The generated table has 16 rows, which generates a loop. In principle, we could also dynamically
calculate the number of rows from the start and end values and the number of columns (six in our case).
However, we know that if we start at position 32, and end at 127, with 6 columns we need 16 rows.
The inner loop writes all columns for a given row. At the top left is the first element, the space char‑
acter. To the right, the character increases not by one, but by 16, which is, therefore, the loop counter.
In the next line, we don’t start at 32 but at 33, the pattern here is the following: the start value of the
inner loop is 32 + row, so 32 plus the row number. Altogether, the loop ends when the ASCII code has
reached 127.
Within the inner loop’s body, the character is required to be displayed as a string. Essentially, each
character is outputted as a string. The conditional operator verifies whether the character at positions 127
is the DEL character; all other characters are converted to a string of length 1 through the Character
method. The format string consists of three components: the first two parts access the first format argu‑
ment and begin by displaying the character’s position as a decimal number, followed by its number in
hexadecimal format. The integer is left‑padded with white space, while the hexadecimal number doesn’t
1 • Advanced String Processing 17
require any width information, as it always contains two digits when starting with 32. The third block
contains the character as a string and the second format argument. Finally, a line feed is added at the end
of the line.
Output of PrintAsciiTable
Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex Dec Hex
32 20 48 30 0 64 40 @ 80 50 P 96 60 ' 112 70 p
33 21 ! 49 31 1 65 41 A 81 51 Q 97 61 a 113 71 q
34 22 " 50 32 2 66 42 B 82 52 R 98 62 b 114 72 r
35 23 # 51 33 3 67 43 C 83 53 S 99 63 c 115 73 s
36 24 $ 52 34 4 68 44 D 84 54 T 100 64 d 116 74 t
37 25 % 53 35 5 69 45 E 85 55 U 101 65 e 117 75 u
38 26 & 54 36 6 70 46 F 86 56 V 102 66 f 118 76 v
39 27 ' 55 37 7 71 47 G 87 57 W 103 67 g 119 77 w
40 28 ( 56 38 8 72 48 H 88 58 X 104 68 h 120 78 x
41 29 ) 57 39 9 73 49 I 89 59 Y 105 69 i 121 79 y
42 2A * 58 3A : 74 4A J 90 5A Z 106 6A j 122 7A z
43 2B + 59 3B ; 75 4B K 91 5B [ 107 6B k 123 7B {
44 2C , 60 3C < 76 4C L 92 5C \ 108 6C l 124 7C |
45 2D ‑ 61 3D = 77 4D M 93 5D ] 109 6D m 125 7D }
46 2E . 62 3E > 78 4E N 94 5E ^ 110 6E n 126 7E ~
47 2F / 63 3F ? 79 4F O 95 5F _ 111 6F o 127 7F DEL
Aligned Outputs
com/tutego/exercise/string/PaidOrNotPaid.java
if ( names.length != paid.length )
throw new IllegalArgumentException(
"Number of names and paid entries are not the same, but "
+ names.length + " and " + paid.length );
int maxColumnLength = 0;
for ( String name : names )
maxColumnLength = Math.max( maxColumnLength, name.length() );
First, the method checks if the two arrays have an equal number of elements, and if not, an
IllegalArgumentException is thrown. Accessing length generates the desired
NullPointerException if the arrays are null.
Since it is unknown in advance how wide the first left column will be, we have to go over all the
strings and determine the maximum length. Using this maximum maxColumnLength we can build a
format string. The format string gets a format specifier that determines the width of a string, padded with
spaces. The format string has a leading minus sign, so this gives a left‑aligned string that is padded on the
18 Java Programming Exercises
right with spaces up to the maximum length maxColumnLength. In addition, the format string contains
a space to the right column of four spaces.
The right column contains either “paid” or “not paid”. That is, the string “paid” always occurs,
and only the word “not” is to be set dependent on a boolean value. This is done by the condition opera‑
tor, which either returns an empty string or the string “not” as a format argument for the format string.
In the last example, it is about finding and not about a complete match, so the find() method of Matcher
is used. In principle, tests of existence can also be formulated by .*FIND.* and matches(…), but match‑
ing makes a little more work for the regex engine than just providing the first find and not having to look
to the end.
1 • Advanced String Processing 19
The task can be solved with a one‑liner because the Matcher method results() returns a
Stream<MatchResult>, and for Stream objects the count() method determines the number of
occurrences.
If we take a closer look at the big numbers, we quickly realize that each large number contains the actual
number itself. The large zero contains 0, the large one contains 1, and so on. So, we don’t have to evaluate
several lines, it’s enough to take any line. We just take the first one.
After extracting the first line, we can search for the substring that makes up the single digits. The
blanks interfere a bit, so they are removed in the first step. A new String is created from the first line,
in which all spaces have been removed, and then the digits of each major character are aligned. For
example, if the line starts with 000, we know that the first digit in the result must be 0. We can simply
use replaceAll(…) to replace the sequence 000 with 0. For example, if there are two zeros in a row,
0000 correctly becomes 00.
Since not only three zeros have to be replaced by zero, but also two ones by a one, and there are ten
different replacements, we store the individual strings in an array beforehand. A loop runs through the
array, and in the body, there are repeated calls to replaceAll(…), which replaces all partial strings
from the search with the loop index, so that, for example, 000 becomes "" + 0, i.e., "0". At the end, we
convert the number to an integer and return the result.
If the incoming string is null, empty, or contains foreign characters, there will be exceptions in the
following. This behavior is fine.
20 Java Programming Exercises
The proposed solution proceeds in three steps: Building the pattern object, matching the string, and
replacing the match group with lowercase strings. The regex string must describe a sequence of uppercase
letters. Upper case letters, over the entire Unicode alphabet, determine \p{javaUpperCase}. We want
to have at least three uppercase letters in a row, which {3,} takes care of. Whether to keep the pattern
precompiled in a static variable depends entirely on how often the method is called. In general, it is a good
strategy to keep the Pattern objects in memory because translation always costs a little execution time.
On the other hand, you reference an object, and if you rarely need it, you don’t have to.
The method compile(…) gives us a Pattern object, and the matcher(…) method gives us a
Matcher object. We can well use the replaceAll(Function<MatchResult, String>) method,
which can perform a transformation of the found strings. The argument passed is a function that maps a
MatchResult to a string. Our function accesses the group, converts the string to lowercase, and returns
it. replaceAll(…) finds all places with the selected uppercase letters and calls this function mul‑
tiple times.
At the heart of the program is a regular expression that captures times. This regular expression consists of
four parts, which are also split into four lines in the code. The first part captures the hours, the part cap‑
tures the minutes, the third part is an optional white space between the time, and the last part, AM/PM.
1. The hours consist of at least one integer, the second integer being optional if, for example, 1
AM is written. In principle, we could write the expression a bit more precisely so that something
like 99 AM is not recognized, but we do not make that check here. The hours themselves are in
round brackets, a group named ?<hours>. All regular expression groups can be named so that
we can access them more easily later by that name and not have to use a group index.
2. The minute’s part is optional, that is, it is enclosed in round brackets altogether, and there is a
question mark at the end. The inside starts with a ?:, which is a small regular expression opti‑
mization so that this group is inaccessible via the API later. If hours and minutes are specified
at the same time, a colon separates them. The minutes themselves are also a named group, and
they consist of two decimal numbers. Again, we do not make any checks about possible ranges
of validity.
3. The third part is a white space, which is optional.
4. The last part must capture different notations of AM/PM. A dot could be placed between the
two symbols, perhaps even mistakenly just after a letter, so say A.M or AM. So that we don’t
have to specify case, we include a special pattern flag that checks case independently, so it
doesn’t matter whether we write AM, am, Am, or pM.
The convertToMilitaryTime(String) method gets the string with the time information as a
parameter. The Pattern object was stored as a constant, and the matcher(…) method connects the
Pattern with the string method parameter. The result is a Matcher. This type can do all the work
with replaceAll(Function<MatchResult, String> replacer): the method runs over all the
matches for us, calls our Function, and we can access the match from the MatchResult and replace
it with a string.
Regarding the Function: first, we access the group for the hours and convert it to an integer.
Converting an integer will not throw an exception because our regular expression ensures that only digits
occur. The peculiarity with minutes is that they can be missing. So, we have to go back to the group min‑
ute and ask if it exists at all. If it does not exist, we assign the variable minutes with 0; otherwise, we
convert the string with the minutes into an integer and set the variable minutes with it.
Now we evaluate the group ampm and declare a variable isTimeInPm, which becomes true if the
time is given in PM. For AM, the variable remains false. This variable helps with the conversion. If
isTimeInPm is true, then the time is “post meridiem”, i.e., afternoon, and 12 hours must be added. It
may happen that the text mistakenly enters 23 PM, for example; in this case, we want to correct the error
and do not add 12. Moreover, if the time of the hours is equal to 12 o`clock, we also do not correct. The
next check is specifically for 12:xx, which becomes 00xx clock. So, if isTimeInPm equals false,
then it is the time “ante meridiem”, that is the morning, and we subtract 12.
After the two variables hours and minutes are set, we generate a string with the hours and min‑
utes and use String.format(…) so that we get the times with only one digit padded with a 0. This is
the return of the Function.
In the center of the job is the class StringTokenizer, which is useful whenever.
1. Not (all) tokens are to be extracted in advance like with split(…), but step by step.
2. The separators consist of single characters, but no strings.
Similarly, we create an instance of StringTokenizer, but it’s important to note that the two specified
characters are not treated as a combined separator; instead, they are considered as individual characters
that can serve as a separator for the tokens in any combination.
With the method nextToken() we ask for the lines three times, and since we don’t know if there is
a fourth line, we look ahead with hasMoreTokens(). If there is a token, we consume it. Otherwise, we
choose the desired default country.
Since StringTokenizer is an Enumeration<Object>, in principle we could have used
hasMoreElements() and nextElement(), but the latter method has the awkward Object return
type.
The reversing of the string is done by the reverse(…)‑ method of the StringBuilder. We then out‑
put the result to the screen, separated by a space at the end.
Now we have to split the sentence and recognize the words.
com/tutego/exercise/string/PrintReverseWords.java
String string = "erehW did eht etarip esahcrup sih kooh? tA eht dnah‑dnoces pohs!";
System.out.println();
At the center is the regular expression [\p{punct}\s]+. It captures a sequence of punctuation marks,
parentheses, etc., and white space separating words. We make use of predefined character classes. It uses
\p{punct} for a character out of
!"#$%&'()*+,‑./:;<=>?@[\]^_'{|}~
and \s for the white space; it is the character class [ \t\n\x0B\f\r]. For a connection, we put both
into a new character class, and since characters can occur any number of times in a row, we add a plus.
Java supports splitting with regular expressions via the Pattern‑Matcher combination, and two
well‑known facades are the Scanner and the split(…) method. Both variants are shown in the pro‑
posed solution. The Scanner is always good if the number of matches can be large because with the
split(…) method the answer is always an array with all words. The Scanner implements Iterable
and at forEachRemaining(…) we put a method reference to the helper method printWordRe‑
versed(…) for the Consumer, which writes each word reversed on the screen.
while ( scanner.hasNext() ) {
String operator = scanner.next();
int number2 = scanner.nextInt();
if ( isValidRelation( number1, operator, number2 ) )
number1 = number2;
else
return false;
}
return true;
}
The actual method checkRelation(String) gets the string and checks the relations. However, the
method falls back on its own private method isValidRelation(int, String, int) so we want
to start with that. isValidRelation(…) gets a number, a comparison operator, and another number. It
checks if the comparison operator with the two numbers returns a true result. If so, the answer is true; if
the comparison is false, the answer is false just as in the case of a misplaced symbol because we evalu‑
ate only <, > and =.
24 Java Programming Exercises
The method checkRelation(…) builds a Scanner with the passed String and now uses a
combination of nextInt(), hasNext(), and next() to process tokens from the Scanner. At the
beginning, there must always be a number, which means we can initialize a variable number1 with the
first number. The data stream could be empty now, but if the Scanner has the next symbols, this will be
a comparison operator, which we will refer to. After the comparison operator comes to a second integer,
which we also read in and store in number2. Now we call our method isValidRelation(…), and this
method returns true if the comparison was fine. Then number2 will become the new number1, so in
the next loop pass, number2 will be assigned the following number. If isValidRelation(…) returns
false, then we can abort the method because at this point the comparison is false. If there was no break‑
out from the loop, all comparisons were correct, and the method ends with return true.
Let us consider the cell AA34 mentioned in the task as an example. In the first step, we need to separate
the column from the row. The column here would be AA, the row 34. We then need to convert the column
AA to the numeric representation, 27. Two separate methods handle these two steps. The main method
parseA1Notation(String) first extracts the row and column, and then calls an internal method
parseColumnIndex(String), which converts the column to a numeric value for us after the A1
notation.
Let’s start with the parseColumnIndex(String) method. We’ll take a few examples to make it
easier to read the calculation pattern.
What we can read is the following:
To convert the whole thing now into an algorithm, we use the Horner scheme. Let us illustrate this with
an example:
The Horner scheme is important for us because we don’t need to calculate powers anymore. If we go one
place further to the right, we multiply the old result by 26 and repeat the scheme for the other places. This
is precisely what the method parseColumnIndex(String) does. A loop runs over all characters,
extracts them, and queries the numeric representation with Character.getNumericValue(char).
This is defined not only for digits but also for letters. For the letter 'a' the result is 10, the same as for
'A'. For 'Z' it is 35. If we subtract 9 from this, we get the range of values 1–26. We take the old result,
multiply it by 26, and add the numeric representation of the letter. The next step is to calculate the new
numeric value of the next character, multiply the last result by 26, and again add the value of the last letter.
This performs the calculation exactly as we planned it.
The method parseA1Notation(String) has little work to do. First, we compile a Pattern that
extracts the column and row—since the column is all letters and the row is all digits, we can easily capture
that via the groups in the regular expression. If the string is wrong, and if we don’t have two matches, we
return an array of {0, 0}, signaling incorrect input. If there are two match groups, we take the column
information from the first one and convert it to an integer using our own parseColumnIndex(String)
method. The second string, according to the regular expression, is a valid string of digits, which Integer.
parseInt(String) converts to an int. The numeric column and row go into a small array, and that
goes back to the caller.
while ( scanner.hasNextDouble() ) {
double x = scanner.nextDouble();
if ( ! scanner.hasNextDouble() )
throw new IllegalStateException( "Missing second coordinate" );
26 Java Programming Exercises
double y = scanner.nextDouble();
svg.append( x ).append( "," ).append( y ).append( " " );
}
The file consists of sequences of integers separated by either a semicolon or a newline, generally speaking
by arbitrary white space. We need to find a tokenizer that we can feed with a regular expression that stands
for just these separators. Since we want to read from a file and process that with regular expressions, the
class Scanner is a good choice. This is also how the proposed solution does it.
The Scanner is connected to an input stream, the file we would like to read. The character encoding
is explicitly set to UTF‑8.
Then the Scanner is initialized with a regular expression for the delimiters. These delimiters are
set by the Scanner method useDelimiter(…). It is important to set the Locale.ENGLISH because
by default the Scanner is preconfigured with the Locale by the operating system, and if that is, for
example, German, the Scanner expects floating‑point numbers with a comma as separator. But the
source always has English formatted numbers.
After the Scanner is prepared, the program can produce the output. It starts with the SVG container
and the polygon start tag. The Scanner method hasNext() helps to iterate through the file. When the
hasNext() method returns a token, we always expect pairs. We can read the first integer, and now there
must be a second integer. But if the Scanner cannot give a new token, this is an error, and we raise an
exception. If the second number exists, it will also be read in. The pair can then be placed in the SVG
container.
At the end of the loop, we close the polygon tag and print the SVG element to the screen. For the
output, we don’t need to pay attention to the language for append(double) because the formatting of
the double is automatically in English.
if ( string.isEmpty() )
return "";
lastChar = currentChar;
}
}
result.append( lastChar );
if ( count > 1 )
result.append( count );
return result.toString();
}
if ( token.isEmpty() )
return "";
if ( token.length() == 1 )
return token;
while ( pattern.find() )
result.append( decodeToken( pattern.group() ) );
return result.toString();
}
In the first step, we want to compress the string. To achieve this, we first query whether the string contains
any text at all; if not, we are quickly done with the task.
To see how many of the same characters occur in the text in a row, we use a variable lastChar for
the last character seen so that we can compare a new character with the last character. In addition, we
note the number of same characters in count. Since this result is freshly built, we add a variable result
of the data type StringBuilder.
The for loop goes over each character and stores it in the auxiliary variable currentChar. Now,
two things can happen: The character currentChar just read can be the same as the previous character
in lastChar, or it can be a different character. We have to handle this difference.
The initial check determines if the previously observed character matches the currently read char‑
acter. If it’s a match, we simply increase the counter and continue the loop. However, if the newly read
character differs from the previous one, the local compression process concludes. We begin by writing
this character to the buffer. Then, we address the count: if more than one identical character was previ‑
ously counted, we input that count into the data stream. But if there was just one character, we don’t add
1 to the buffer, in accordance with the task’s instructions. After finalizing the character‑counter pair, it’s
essential to reset the counter to 1. We are almost done with the loop. The moment a new character is found,
we set lastChar to exactly the current character currentChar.
When we are done with the loop, we still have a character in lastChar and count. We therefore
perform the same query as before in the case distinction, and append the counter to the string if the coun‑
ter is greater than 1.
28 Java Programming Exercises
In the method for unpacking, we fall back on the pattern that results from the compression. There
are always pairs of a string and a number, with the special case that the pair is single, and the number
is missing. Using a regular expression, we run the entire string and look at all the pairs. To keep the
method from getting too big, we use a helper method called decodeToken(String) that takes a pair
and expands it.
First, the method must find out whether the token consists of only one character or of several charac‑
ters. If the string consists of only one character, then it must have been our symbol, and it comes into the
output. If the string is longer than 1, then there is a length encoding from the second position upward. With
substring(1) we extract the string and convert it to an integer, so that the repeat(int) method of
String can generate us exactly this number of characters token.charAt(0). With substring(1)
we extract the string and convert it to an integer, so that the repeat(int) method of String can gener‑
ate us exactly this number of characters token.charAt(0).
TABLE 1.4 Some characters with their positions in the Unicode standard
CHARACTER θ A a Ä ß ä ñ
Code point (decimal) 48 65 97 196 223 228 241
The natural ordering of strings is lexicographic, meaning that the position of the character in the Unicode
alphabet counts. Let’s illustrate this with some characters.
From the table, you can see that the digits are first, followed by the uppercase letters and then the
lowercase letters. The umlauts are far behind the capital letters and not sorted between the upper and
lower case letters.
Umlauts are regular letters in German, which do not come after “Z” in the order. The standard DIN
5007‑1 describes under “Ordering of character sequences (ABC rules)” two sorting procedures:
DIN 5007, variant 1 (used for words, for example, in dictionaries)
DIN 5007, variant 2 (special sorting for lists of names, such as in telephone directories)
There are similar rules for other languages. The lexicographical order does not support this. Therefore,
the Unicode Collation Algorithm describes how the sorting looks like in different national languages. For
an introduction, see also https://en.wikipedia.org/wiki/Unicode_collation_algorithm.
In Java Collator objects are available. They are initialized with a Locale object. In addition
to the language, so‑called levels can be passed; the Javadoc gives further examples under the heading
“Collator strength value”.
Mathematics
2
Integers and floating‑point numbers already appeared in the first exercises; we were using the common
mathematical operators to crunch numbers. But Java does not have an operator for everything, and so
the class Math offers further methods and constants; early on we used Math.random(), for example.
Throughout this chapter’s upcoming tasks, our focus will be on various rounding techniques, particularly
exploring the methods offered by the Math class. In addition, the java.math package provides two
classes that can be used to represent arbitrarily large numbers—there are tasks for these as well.
Prerequisites
• java.lang.Math
• java.math.BigInteger
• java.math.BigDecimal
The Java library supports different possibilities for conversion and rounding, among other things with
• Math.rint(double) → double.
• BigDecimal and setScale(..) → BigDecimal.
• NumberFormat, configured with setMaximumFractionDigits(…), then format(…)
→ String.
• DecimalFormat, configured with setRoundingMode(…), then format(…) → String.
• Formatter with a format string, then format(…) → String.
Four of the methods originate from Math, but when it comes to gigantic and precise floating‑point num‑
bers or string representation, other classes are involved. Since the methods differ slightly in the result, the
following task should make these differences clear once again.
Task:
-2.5
-1.9
-1.6
-1.5
-1.1
1.1
1.5
1.6
1.9
2.5
• Given an array of floating‑point numbers (positive and negative) and the sum converted to an
integer by Tin Tin.
• Captain CiaoCiao wants to find out which rounding was used to form the integer of the sum.
Therefore, the elements in the array are to be summed and compared to Tin Tin’s sum. The
rounding is done after the numbers are added.
• Implement a method RoundingMode detectRoundingMode(double[] numbers,
int sum) that gets a double array of numbers and the sum of Tin Tin and checks which
rounding mode was used.
• To allow the rounding mode to be represented, introduce an enumeration type:
enum RoundingMode {
CAST, ROUND, FLOOR, CEIL, RINT, UNKNOWN;
}
32 Java Programming Exercises
• Which rounding is bad for Captain CiaoCiao and good for Tin Tin? Which variation could Tin
Tin use to cheat?
Example:
Notes:
• There is an enumeration type RoundingMode in the java.math package, but for our case,
it does not fit the task.
• It may well happen that more than one rounding mode fits—such as when the sum of the
floating‑point values itself gives an integer—then the method is free to choose one of the round‑
ing modes.
• Calculate the arithmetic mean of two long values so that there is no overflow and wrong
results. The result should be a long again.
2 • Mathematics 33
Task:
Example:
T
«interface»
Number
Comparable
«final»
Fraction
SUGGESTED SOLUTIONS
In summary:
We can read that for the numbers Tin Tin chose, she gets the most out of rounding off all the sums.
Rounding off makes the numbers smaller, and she gets the difference in each case, whether the numbers
are negative or positive.
2 • Mathematics 37
The proposed solution is composed of two methods. The public method detectRoundingMode(…)
sums the elements of the array and calls the private method detectRoundingMode(double, int)
to determine the actual rounding mode. The method sequentially compares the integer value with the dif‑
ferent rounded variants, and if there is a match, the enumeration element is returned. If the totals and the
rounded value do not match at all, the return is RoundingMode.UNKNOWN.
In the first step, we build a new object of type BigInteger for the parameters x and y using the factory
method valueOf(long). In the second step, the sum is built and in the third step, the sum is divided by 2.
For the division, we also have to use a BigInteger object for the divisor. Since the divisor is constant, the
solution defines a constant—the class BigInteger has constants for 0, 1, and 10, but not for 2.
When averaging integers, there is always the question of rounding. The addition of two even or two
odd numbers always results in an even number, but the sum of an even number with an odd number is
odd. If odd numbers are divided by two, there is a remainder, and the question is whether to round up or
down, round the result to 0, or round to plus or minus infinity—there are quite a few possibilities. The
BigInteger method divide(…) behaves like the division of two integers, it is rounded toward 0.
A result is a number, not greater than Long.MAX _ VALUE and not smaller than Long.MIN _
VALUE, so converting the number from the BigInteger with longValue() gives no loss.
38 Java Programming Exercises
We can solve the task using strings or math. Using a temporary string, the task is easy to solve.
In the first step, we run the array, take all the numbers one by one, and append them to a
StringBuilder. If we assume that each number consists on average of two digits, we can esti‑
mate approximately how big the result will be; therefore, we use the parameterized constructor of
StringBuilder with a capacity.
After appending all the digits, StringBuilder is converted to a String and this feeds the con‑
structor of BigInteger. Here, the actual conversion of the string into a BigInteger begins.
Of course, the task can also be solved without a temporary StringBuilder. Let us take the call
as an example
If at the end we want to create the BigInteger with 12322989779, we could first build the BigInteger
with 123, then multiply the number with 100, and add 22, resulting in 12322. In the next step, we take
the result, multiply it by 1000 and add 989. Then we multiply again by 100, add 77, multiply the result
by 10 and add 9. That is, we always multiply the old value by 10n, where n is the number of digits in the
coming number.
import java.math.BigInteger;
// shortcut if denominator == 1
if ( denominator == 1 ) {
this.numerator = numerator;
this.denominator = 1;
}
2 • Mathematics 39
else {
// try to simplify every fraction
int gcd = gcd( numerator, denominator );
// might be 1, but divide anyway
this.numerator = numerator / gcd;
this.denominator = denominator / gcd;
}
}
private static int gcd( int a, int b ) {
return BigInteger.valueOf( a )
.gcd( BigInteger.valueOf( b ) )
.intValue();
}
public Fraction( int value ) {
this( value, 1 );
}
public Fraction reciprocal() {
return new Fraction( denominator, numerator );
}
public Fraction multiply( Fraction other ) {
return new Fraction( Math.multiplyExact( numerator, other.numerator ),
Math.multiplyExact( denominator, other.denominator )
);
}
@Override
public int intValue() {
return numerator / denominator;
}
@Override
public long longValue() {
return (long) numerator / denominator;
}
@Override
public double doubleValue() {
return (double) numerator / denominator;
}
@Override
public float floatValue() {
return (float) numerator / denominator;
}
@Override
public int compareTo( Fraction other ) {
return Double.compare( doubleValue(), other.doubleValue() );
}
@Override
public boolean equals( Object other ) {
if ( other == this )
return true;
return other instanceof Fraction otherFraction
&& numerator == otherFraction.numerator
&& denominator == otherFraction.denominator;
}
@Override
public int hashCode() {
return numerator + Integer.reverse( denominator );
}
@Override
40 Java Programming Exercises
Let’s start with the constructor Fraction, which takes the numerator and denominator. The denomina‑
tor must never be 0; this is punished with an exception. We could in principle build our own Exception
class, but an ArithmeticException fits perfectly.
The numerator and denominator can be negative or positive, there are four cases:
• If the numerator and denominator are both negative, we can reverse the sign so that both values
become positive.
• If the denominator is negative and the numerator is positive, we can shift the sign to the
numerator.
Both cases can be dealt with a control flow statement: if the denominator is negative, we flip both signs.
A negative denominator then becomes positive. If the numerator was also negative, the numerator becomes
positive, so a negative numerator and negative denominator thus both become positive. If the denominator
was negative and the numerator was positive, we thus shift the sign to the numerator because the numera‑
tor becomes negative.
The numerator and denominator are now prepared, and if the denominator is 1, then the fraction
does not need to be truncated, and we save the following work and leave the constructor. Otherwise, we
continue in the else block.
The last part of the constructor deals with the reduction of the fraction. The task briefly explained
how to proceed here: we need the greatest common divisor. The constructor delegates here to its method
gcd(int, int), which first builds a BigInteger for two numbers, then lets it calculate the greatest
common divisor, and then returns it as an integer. In our scenario, the parameter types are int so the divi‑
sor will be smaller in any case, with intValue() we read from the BigInteger. After the constructor
asks for the greatest common divisor, we divide the numerator and denominator by it, initializing our
instance variables. In principle, gcd could also be 1, so we don’t need a division, but we save this special
case distinction here because a division by 1 is not expensive.
The second constructor takes only one numerator, and in that case, the denominator is 1. We delegate
to our constructor, but in general, we can do the storing ourselves, since we don’t need the special treat‑
ment for a negative denominator, and the denominator is not 0 either.
The first of the two methods, reciprocal(), creates a new Fraction object by turning the
denominator into the numerator and the numerator into the denominator. The method multiply(…)
multiplies its numerator with the numerator of the passed object and symmetrically does the same for
the denominator. Multiplication quickly produces large numbers and the value range of int could be
blown up, so Math.multiplyExact(…) makes sure that the product fits into an int; otherwise, an
exception follows.
2 • Mathematics 41
The next category of methods comes from the base type Number. We divide the numerator by the
denominator and return the result at the different methods.
The next method compareTo(…) comes from the Comparable interface. We make it simple and
calculate the quotient and compare our quotient with the passed fraction.
In the next step, we override the method equals(…) and hashCode(). A good hash code will
return a different hash code if the numerator or denominator changes. The realization of hashCode()
does this by flipping the bits of the denominator. If the number of required bits of the numerator and
denominator together does not exceed 32 (the number of bits of an int), we can detect each change by a
different hash code.
The last method is toString(). If the numerator is 0, we don’t even have to look at the denominator
and return the string 0. If the denominator is 1, we can return only the numerator. Otherwise, we return
the numerator and denominator separated by a fraction bar.
Locale, Date, and Time
3
In almost all exercises we have screen outputs, and whoever writes in everyday life, for example, Chinese,
Spanish, or Arabic, will probably prefer program outputs in this language as well. However, in some
places, a wrong format might appear, e.g., when decimal places in floating‑point numbers are not sepa‑
rated correctly. The decimal separator is only one of many examples of how different the standards
are in the countries: currencies sometimes precede and then follow the number; for dates, the format is
year‑month‑day in some countries, day‑month‑year in others, and then month‑day‑year again.
This chapter focuses on exercises around internationalization (how to make programs language‑
independent in principle) and localization (adaptation to a specific language). After all, if our software is
to be successful, it must, of course, run anywhere on the planet at any time of day. Java can easily accom‑
modate many language specifics, and we’ll look at that in the exercises, so that Captain CiaoCiao and
Bonny Brain can also do their business anywhere and everyone understands their “language”.
Prerequisites
• java.util.Locale
• java.time.LocalDate
• java.time.LocalDateTime
• java.time.format.DateTimeFormatter
• java.time.format.FormatStyle
• java.time.format.DateTimeFormatter
• java.time.Duration
Apply Country‑/Language‑Specific
Formatting for Random Number ⭑
Bonny Brain is preparing a new email scam: bitcoins are to be “sold” well below their price. It prepares
subject lines for this, which look like this, for example:
Of course, the crew is planning a worldwide scam, and that’s where it’s important to format the number
according to the rules of the different countries.
The printf(…) method is overloaded, as is String.format(…):
Exercise:
• Create a random number of type double between 10 000 (inclusive) and 12 000 (exclusive);
decimal places are desired.
• To format a floating‑point number with two decimal places and a thousand separator, employ
the String.format(String format, Object... args) method.
• Get all Locale objects of the system, and use them as arguments to the String.
format(Locale l, String format, Object... args) method, so that the fl oating‑
point number is formatted “locally” in each case. Output the string.
44 Java Programming Exercises
As a general rule, it can be stated that all methods implementing language‑dependent formatting, or pars‑
ing strings, usually accept a Locale object as a parameter. There may be language‑dependent methods
without parameters in addition, but these are often overloaded methods that internally query the default
language, and then pass to the method with the explicit Locale parameter.
For a date with time part, there are three possibilities at once; however, Date and Calendar are no
longer popular since Java 8 because they are causing several problems. However, these data types can still
be found in many written examples, especially online. We should stay away from these “old” types, and
therefore this section specifically trains how to use the current data types from java.time.
Exercise:
Example:
• Write a program that lists all Fridays that fall on the 13th for a given year.
• Bonus: for this, write a TemporalAdjuster that returns the next Friday the 13th for a
Temporal object.
46 Java Programming Exercises
Examples:
• For the year 1925, the output may look like this:
1925‑02‑13
1925‑03‑13
1925‑11‑13
• For 2024:
2024‑09‑13
2024‑12‑13
• Write a program that evaluates a string in the format above, finds the average party duration,
and prints it out.
• The program does not need to consider time zones, leap seconds, or other special cases—a day
can be exactly 24 hours long.
Example:
2025‑10‑10
2025‑12‑2
1/3/1976
1/3/25
Tomorrow
Today
Yesterday
1 day ago
2234 days ago
3 • Locale, Date, and Time 47
Exercise:
SUGGESTED SOLUTIONS
Apply Country‑/Language‑Specific
Formatting for Random Number
com/tutego/exercise/util/RandomInEveryLocalePrinter.java
The solution consists of four steps: in the first part, we generate a random number. In the second part, we
query all registered Locale objects as an array containing all supported languages of the Java library.
We can traverse this array in the third step and output it in the fourth step.
System.out.printf(…) expects a formatting string that puts the random number as a fl oating‑point
number, just like the language name. The part in the formatting string for the number is %,.2f—the
comma indicates the desire for a thousand separator, .2 indicates the two decimal places.
The important parameter in printf(…) is the first one, which says that a Locale instance can be
passed, which determines the formatting of the floating‑point number in the following.
DateTimeFormatter[] dateTimeFormatter = {
formatterShort,
formatterMedium,
formatterLong,
formatterFull,
formatterShort.withLocale( Locale.CANADA_FRENCH ),
formatterMedium.withLocale( Locale.CHINESE ),
formatterLong.withLocale( Locale.ITALIAN ),
formatterFull.withLocale( Locale.of( "th" ) )
};
for ( DateTimeFormatter formatter : dateTimeFormatter )
System.out.println( date.format( formatter ) );
Assuming the current Locale is US, the program’s output for the year 2023 would be:
2023‑09‑19
9/19/23
Sep 19, 2023
September 19, 2023
Tuesday, September 19, 2023
2023‑09‑19
2023年9月19日
19 settembre 2023
วันอังคารที่ 19 กันยายน ค.ศ. 2023
The temporal data types override the toString(…) method, but it cannot be parameterized
with a formatting type. Instead, LocalDate has the format(…) method, which can be passed a
DateTimeFormatter. There are three common ways to get a DateTimeFormatter:
The ofLocalizedDate(…) method expects a parameter of type FormatStyle and the proposed
solution builds four of these DateTimeFormatter instances and then calls the format(…) method
with just these formats.
The method name ofLocalizedDate(…) already contains a hint about the localization. With
withLocale(…) any DateTimeFormatter can be associated with a Locale. All temporal data
types are immutable; the return of the method is a new object with a Locale set.
1. Starting from beaufortBday we have to build a LocalDate with May 27th of this year.
2. The day of the week must be retrieved.
For the construction of a LocalDate object, the proposed solution shows three variants:
1. In the first variant, we use a wither, i.e., a method with the prefix with instead of set, which
returns a new object with a changed value. Java’s temporal data types are immutable, so there are
no setters. withYear(int) returns a new LocalDate object with a changed year. For the cur‑
rent year, we can use the Year data type. The static method now() returns the current year, and
since withYear(int) needs an integer, getValue() is needed on the Year object.
2. Instead of using a wither method (a method with a prefix with that returns new object), we
can use the static factory method of(…) to compose a new LocalDate object by taking the
current year and retrieving the month and day from beaufortBday.
3. In the third variant, we query LocalDate.now() for the current date with day, month, year,
but get a new LocalDate object with the month set first and then a new LocalDate object
with the day of the month using the two wither methods. This variant is not optimal because
two temporary LocalDate objects are created, which then end up in the automatic garbage
collection again.
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern( "EEEE" /*, Locale.GERMANY */ );
System.out.println( beaufortBdayThisYear.format( formatter ) );
The proposed solution shows different variants of how to output the day of the week.
3. Another variant does not work via DayOfWeek, but uses the format(…) method provided by
LocalDate. This must be passed a DateTimeFormatter, and if we use the pattern EEEE,
then that stands for the day of the week. A language for the day of the week can be specified
optionally, if it is missing, the default language of the operating system is taken as default.
package java.time.temporal;
import java.time.DateTimeException;
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto( Temporal temporal );
}
By implementing the TemporalAdjuster interface, we can well increase the reusability of code.
Firstly, the implementations are applied to a variety of date or time objects, and secondly we can well
collect the implementations, just as Java SE provides useful implementations in TemporalAdjusters.
The implementation gets a temporal object (e.g., LocalDate or LocalDateTime) and also returns
a Temporal object; it may look like this:
com/tutego/exercise/time/NextFridayThe13th.java
The functional interface is implemented by a lambda expression, so at the end there is a reference in the vari‑
able nextFriday13th. The basic idea is to move from Friday to Friday via TemporalAdjusters.
next(…) and check if the day of the month is then 13. If it is, the date is returned.
The usage can look like this:
com/tutego/exercise/time/NextFridayThe13th.java
The main program works by looping until the current year is exited. In detail:
• Since the input is always a year (the type Year is appropriate here), atDay(1) upgrades the Year
to a LocalDate object, resulting in the 1/1 of the year. With with(TemporalAdjuster)
our TemporalAdjuster can be used directly, so that nextDate is initialized with the first
13th day of the month, which falls on a Friday.
3 • Locale, Date, and Time 51
• The loop is terminated when the year of thisYear and nextYear is no longer the same.
• The for‑update returns the next 13th day of the month that falls on a Friday.
• In the body, the date is printed.
var formatter =
DateTimeFormatter.ofPattern( "yyyy‑MM‑dd, HH:mm" );
var scanner = new Scanner( input ).useDelimiter( " ‑ |\\n" );
var totalDuration = Duration.ZERO;
int lines;
for ( lines = 0; scanner.hasNext(); lines++ ) {
String start = scanner.next();
String end = scanner.next(); // potential NoSuchElementException
// potential DateTimeParseException
var startDateTime = LocalDateTime.parse( start, formatter );
var endDateTime = LocalDateTime.parse( end, formatter );
Several steps are necessary to solve the task. Firstly, the program has to extract all start and end values
from the large string. Then the strings have to be transferred to the corresponding temporal data types.
Then, the differences between these start and end times must be calculated and summed. Finally, we
divide the sum by the number of entries, and the task is solved.
The detection of the start and end time points can be done by the Scanner. The Scanner is ini‑
tialized with white space as a separator by default. We change this and set a minus sign and a newline as
separators. Repeated calls to the next() method will return the start and end values, succeeding.
Once we have extracted the string, we pass it to the parse(…) method of the LocalDateTime
class. Since our string does not follow an ISO standard (International Organization for Standardization),
we must pass a DateTimeFormatter to parse(…). We built this before with the method ofPat‑
tern(…), where the passed string corresponds exactly to the pattern we used to format the date with the
time information. The symbol strings YYYY, MM, dd, etc., can be taken from the Javadoc.
With two LocalDateTime objects, we can determine the difference—we do not do this manually
but resort to the Duration class. There are two classes in the Java Date–Time API that are used for the
difference of temporal data types: the Duration class stores the intervals of two‑time values in fractions
of seconds, while Period is used for date values and works based on days. Period can be used well if,
for example, leap days are to be considered in the difference; with Duration a day is exactly 24 hours
long, which corresponds to 86 400 seconds. For our calculation, Duration is just right.
Conveniently, a Duration object can be built directly with the between(…) method, and you pass
the start and end values. Like all temporal data types, Duration is immutable. To add the differences,
52 Java Programming Exercises
we access the plus(…) method and store the result back in the totalDuration variable. Finally, we
count up one more variable for the total number of date–time pairs, and it goes back to the loop test to see
if any more dates follow.
Two runtime errors can occur in the program: once because there is an odd number of date–times in
String, and of course because the formatting of the date–time specification is wrong. We do not catch
these exceptions; they cause the program to abort.
If all values are correct, the program ends with the calculation of the average duration. The method
dividedBy(…) helps to divide the total summed duration by the number of occurring date–time pairs.
The resulting object of type Duration is then asked for the number of hours and minutes, and the whole
result is formatted.
In our case, such a configured DateTimeFormatter cannot be used because relative information like
“yesterday”, “today”, or “tomorrow” cannot be expressed. We could in principle produce a part via such a
configured DateTimeFormatter, but the proposed solution proceeds differently.
com/tutego/exercise/time/ParseDatePattern.java
Ultimately, our task is to transform a String into a LocalDate. But this is nothing else than a map‑
ping, which we can express in Java by the data type Function. We can write a Function that tries
to recognize a certain format. In the best case, the mapping works, and we can map a String to a
LocalDate; in the worst case, there is an Exception, or we program this Function to return null.
The reason for this generalization, which may seem unusual at first glance, is that it allows us to collect
all function objects later and try them out one by one. In programming, we should always consider
whether we can generalize things—from the specific to the general.
We can identify three different types of mappings:
• The first three mappings try to recognize a pattern directly and make use of the
DateTimeFormatter.
• The second type of function detects whether today, yesterday, or tomorrow was in question. We
fall back on the local variable now, which is initialized when the method is called. For testing
this is inconvenient, so in practice, one would write an internal method for test cases where one
can introduce a LocalDate as a reference point.
• The third Function is the most complex because it has to detect a relative reference with
a given number of days. Several methods work together here. The Scanner can recognize
a string and can simply return the place of discovery for the number of days. The result is in
a MatchResult: we extract the specification of the days, convert it to an integer, get a new
LocalDate object where this number of days has been reduced, and return the result. If the
pattern did not match, we get the return null.
For each possible format, there is now a Function, and we add new mappings if more formats are to
be recognized—this is busywork. The actual recognition works by traversing all mappings; the functions
are applied to the string in order until a function returns a valid result. To accomplish this, the func‑
tions are first added to a list. If the application fails and there is an exception, the catch block catches
the exception and selects the next function from the list. The relative references all answer null, which
through the Optional.of(…) leads to a NullPointerException, which is caught so that the next
function can be used.
In the end, there are only two outputs: either a function responded with a valid LocalDate, and we
get out of the try block with return, or there were only exceptions, and the for loop could not detect
a candidate—in which case the method is exited with an Optional.empty().a
Concurrent
Programming
with Threads
4
Programs often utilize threads as a programming aid, and operating systems provide them as a feature.
If you are using Windows and check the task manager, you’ll see that there are roughly 3000 running
threads. In this chapter, we aim to increase this number further to execute our own threads. However, the
exercises should not solely focus on creating threads; concurrency requires careful attention to ensure
proper access to shared resources, necessitating coordination among the threads.
Prerequisites
• java.lang.Thread
• java.lang.Runnable
• java.util.concurrent.TimeUnit
• java.util.concurrent.Callable
• java.util.concurrent.Executor
• java.util.concurrent.Executors
• java.util.concurrent.Future
• java.util.concurrent.locks.Lock
• java.util.concurrent.lock.ReentrantLock
• java.util.concurrent.Semaphore
• java.util.concurrent.locks.Condition
• java.util.concurrent.CyclicBarrier
• java.util.concurrent.CountDownLatch
CREATE THREADS
When the Java virtual machine (JVM) starts, it creates a thread named main. This thread executes the
main(…) method, and it has executed our programs in all previous exercises. We want to change that in
the next exercises. We want to create more threads and let them execute program code.
java.lang
«interface»
Runnable
run(): void
Thread
+MAX_PRIORITY: int
+MIN_PRIORITY: int
+NORM_PRIORITY: int
+Thread()
+Thread(target: Runnable)
+Thread(target: Runnable, name: String)
+Thread(name: String)
+Thread(group: ThreadGroup, target: Runnable)
+Thread(group: ThreadGroup, target: Runnable, name: String)
+Thread(group: ThreadGroup, target: Runnable, name: String, stackSize: long)
+Thread(group: ThreadGroup, target: Runnable, name: String, stackSize: long, inheritThreadLocals: boolean)
+Thread(group: ThreadGroup, name: String)
+activeCount(): int
+currentThread(): Thread
+dumpStack(): void
+enumerate(tarray: Thread[]): int
+getAllStackTraces(): Map<Thread, StackTraceElement[]>
+getDefaultUncaughtExceptionHandler(): Thread.UncaughtExceptionHandler
+setDefaultUncaughtExceptionHandler(eh: Thread.UncaughtExceptionHandler): void
+holdsLock(obj: Object): boolean
+interrupted(): boolean
+onSpinWait(): void
+sleep(millis: long): void
«static» «static»
+sleep(millis: long, nanos: int): void
State UncaughtExceptionHandler
+yield(): void
+checkAccess(): void
+getContextClassLoader(): ClassLoader
+getId(): long
+getName(): String
+getPriority(): int
+getStackTrace(): StackTraceElement[]
+getState(): Thread.State
+getThreadGroup(): ThreadGroup
+getUncaughtExceptionHandler(): Thread.UncaughtExceptionHandler
+interrupt(): void
+isAlive(): boolean
+isDaemon(): boolean
+isInterrupted(): boolean
+join(): void
+join(long millis): void
+join(long millis, int nanos): void
+run(): void «override»
+setContextClassLoader(cl: ClassLoader): void
+setDaemon(on: boolean): void
+setName(name: String): void
+setPriority(newPriority: int): void
+setUncaughtExceptionHandler(eh: Thread.UncaughtExceptionHandler): void
+start(): void
+toString(): String
• Threads always execute code of type Runnable in Java. Runnable is a functional inter‑
face, and there are two ways to implement functional interfaces in Java: classes and lambda
expressions. Write two implementations of Runnable; one using a class, one using a lambda
expression.
• Put a loop with 50 repetitions in both Runnable implementations. One Runnable should
output “wink” on the screen, the output of the other one should be “wave flag”.
• Create a Thread object, and pass the Runnable. Then start the threads. Do not start fifty
threads, but only two!
Extension: The run() method of each thread should contain the statement System.out.
println(Thread.currentThread());. What will be displayed?
Suppose Captain CiaoCiao has a few more arms to wave. How many threads can be created until the
system comes to a standstill? Observe the memory usage in the Windows Task Manager. Can you estimate
how much a thread “costs”?
• Write a program with two Runnable implementations that in principle wink-and-wave flags
indefinitely, unless there is an interruption. The run() method should therefore use Thread.
currentThread().isInterrupted() to test whether there was an interruption, and then
exit the loop.
• Build a delay into the loop. Copy the following code:
try { Thread.sleep( 2000 ); } catch ( InterruptedException e ) {
Thread.currentThread().interrupt(); }
• The main program should respond to input with JOptionPane.showInputDialog
(String) so that the commands endw stop winking and endf stop flag waving.
Parameterize Runnable ⭑⭑
A glance at the following code shows that the two Runnable implementations are very similar, differing
only in the screen output:
// Runnable 1
class Wink implements Runnable {
@Override public void run() {
for ( int i = 0; i < 50; i++ )
4 • Concurrent Programming with Threads 57
// Runnable 2
Runnable flagWaver = () -> {
for ( int i = 0; i < 50; i++ )
System.out.printf( "Wave flag; %s%n", Thread.currentThread() );
// ^^^^^^^^^
};
• Re-implement the sleep program in Java so that one can write comparable to the example on
the command line:
$ java Sleep 22
The Java program should then sleep for 22 seconds and if there are subsequent program
invocations in a script, for example, they will be delayed.
• The Java program should be able to be given the sleep time in various formats on the command
line. If only an integer is passed, then the waiting time is in seconds. Suffixes behind the integer
should be allowed for different durations:
• s for seconds (default).
• m for minutes.
• h for hours.
58 Java Programming Exercises
• d for days.
If more than one value is passed, they are summed up to give the total waiting time.
• Various things can go wrong with the call, for example, if no number is passed or the number
is too large. Check if the values, ranges, and suffixes are correct. Optional: in case of an error,
exit the program with an individual exit code via System.exit(int).
Example:
Tip: structure the program so that the three essential parts are recognizable:
Catch Exceptions ⭑
The distinction between checked exceptions and unchecked exceptions is important because if unchecked
exceptions are not caught, this can escalate to the point where they end up at the executing thread, which
is then terminated by the virtual machine. This is done automatically by the runtime environment, and we
kindly get a message on the standard error channel, but we can’t revive the thread anymore.
4 • Concurrent Programming with Threads 59
The processing takes place in a cascade: if there is an unchecked exception, the JVM first looks
to see if an UncaughtExceptionHandler is set on the individual thread. If not, it looks for an
UncaughtExceptionHandler in the thread group and then looks for a global handler to inform.
Exercise:
java.lang java.util.concurrent
V
«interface» «interface»
Runnable Callable
So far, we have always built threads ourselves and used only Runnable. The following exercises
will be about thread pools and also about Callable.
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
Exercise:
synchronized is a convenient keyword, but limited in capability. The Java Concurrency Utilities
provide more powerful data types. For “locking” exclusively executed program parts, the interface java.
util.concurrent.locks.Lock and various implementations exist, such as ReentrantLock,
ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock.
62 Java Programming Exercises
java.util.concurrent.locks.ReentrantLock
«interface»
Lock java.io
lock(): void
lockInterruptibly(): void «interface»
newCondition(): Condition Serializable
tryLock(): boolean
tryLock(long time, TimeUnit unit): boolean
unlock(): void
ReentrantLock
class FriendshipBook {
private final StringBuilder text = new StringBuilder();
TimeUnit.SECONDS.sleep( 1 );
System.out.println( book );
Exercise:
Exercise:
• Create a Semaphore with as many seats as there can be guests at the table with the captains
at the same time.
• Model a guest as record Guest, which implements Runnable. All guests have a name.
• Guests are waiting for a seat. It does not have to be fair, so the guest who has been waiting
for the longest is not necessarily next to the table.
• The program should do a screen output for a guest who would like to come to the table, for a
guest who has been seated, and for a guest who is leaving the table.
• Start two threads, each representing two pirates; give each thread a name.
• A random pirate starts cursing and gets an endless insult contest going.
• The curses should be random from a given collection of curses.
• Before the actual curse, a pirate may take a “pause for thought” of up to one second.
• Create a class Paintbox. This class should get a constructor and accept the maximum num‑
ber of free pens.
66 Java Programming Exercises
enum HandSign {
SCISSORS, ROCK, PAPER;
The enum HandSign declares three enumeration elements for scissors, stone, paper. The static method
random() returns a random hand sign. The beats(HandSign) method is similar to a comparator
method: it compares the current hand sign with the passed hand sign and returns 0 if both hand signs are
equal, +1 if the own hand sign is higher than the passed sign, and -1 otherwise.
Exercise:
• Run a starter thread that triggers a snick-snack game every second. For repeated execution, a
ScheduledExecutorService can be used.
• A player is represented by a Runnable that chooses a random hand signal and puts the choice
into a data structure of type ArrayBlockingQueue with add(…).
• After a player picks a hand sign, the await() method is to be called on a previously con‑
structed CyclicBarrier.
• The constructor of CyclicBarrier shall get a Runnable which determines the winner at
the end of the game. The Runnable takes out of the ArrayBlockingQueue the two hand
signs with poll(), compares them, and evaluates the winner and loser. At the first position of
the data structure is player 1, at the second position is player 2.
• Create ten threads that wait for Bonny Brain’s signal. Thereafter, the threads start and take
between a freely chosen number of 10 and 20 seconds to run. In the end, the threads should
write their time into a common data structure, so that the name of the thread (runner name) is
noted with the runtime.
• Bonny Brain starts the runners in the main thread and at the end outputs all run times sorted
in ascending order with the runner names.
68 Java Programming Exercises
If several threads are to come together in one place, a CountDownLatch can be used well for this. The
CountDownLatch is initialized with an integer (a counter) and provides two central methods:
SUGGESTED SOLUTIONS
winkerThread.start();
flagWaverThread.start();
In the body of the run() method and the lambda expression, we find a simple loop with the desired out‑
puts. After building the Runnable instances, they need to be connected to the thread. To achieve this,
the Runnable objects are passed to the constructor of Thread. The constructor is overloaded several
times; one variant allows setting a name, which we do for the flag wavers. Building the thread instances
does not start a thread; this requires a call to the Thread method start().
Wink; Thread[Thread-0,5,main]
Wave flag; Thread[flag waver,5,main]
Wave flag; Thread[flag waver,5,main]
Wave flag; Thread[flag waver,5,main]
Wink; Thread[Thread-0,5,main]
Wink; Thread[Thread-0,5,main]
Wink; Thread[Thread-0,5,main]
Wink; Thread[Thread-0,5,main]
…
4 • Concurrent Programming with Threads 69
The output shows the toString() representation of Thread, which consists of the thread name, thread
priority, and thread group. From the output, you can see that thread-0 (which is the automatically
assigned name) and flag waver alternate. Each time they are called, the output will look slightly dif‑
ferent—concurrent programs are typically nondeterministic.
winkerThread.start();
flagWaverThread.start();
String message = "Submit 'endw' or 'endf' to end the threads or cancel to end
main thread";
for ( String input;
(input = JOptionPane.showInputDialog( message )) != null; ) {
if ( input.equalsIgnoreCase( "endw" ) )
winkerThread.interrupt();
else if ( input.equalsIgnoreCase( "endf" ) )
flagWaverThread.interrupt();
}
For a thread to respond to an interrupt, the interrupt flag must be deliberately polled in the Runnable.
This query is handled by the isInterrupted() method. We put the query in a while loop because
we want to execute our operation as long as the flag is not yet set. In the body of both loops is a console
output and a waiting time of 2 seconds. There is a special feature to note when sleeping: if we sleep and
then get interrupted, the sleep(…) method first throws an InterruptedException, and second, it
resets the interrupt flag. So, we have to set the interrupt flag again in the catch block and fall back to
the interrupt() method. This is also exactly the method we use in the main(…) method to signal a
selected thread to terminate itself.
There is a significant difference between killing a thread with stop() and setting a flag: when
calling the stop() method, the thread is killed hard, and it can be in any state. Setting an interrupt flag
requires the active cooperation of the thread. The thread must independently request the flag and exit the
run() method without any exception.
70 Java Programming Exercises
Parameterize Runnable
The run() method has no return and no parameter list. Therefore, the run() method must take param‑
eters in some other way. This can be done by using variables that the run() method can access.
Two suggested solutions:
com/tutego/exercise/thread/ParameterizedRunnable.java
If we write a new class that implements Runnable, we can give it a constructor that takes states. We
can store these states in instance variables. If we then call the constructor, the values are set when the
Runnable object is built, and if the thread later calls the run() method, the implementation of run()
can access the values.
com/tutego/exercise/thread/ParameterizedRunnable.java
};
}
The second proposed solution uses a factory method. As a reminder, factories are object creators and
alternatives to the constructor, and in that case, the parameterization is not done by a constructor, but by
the method. Lambda expressions can access local variables, and parameter variables are one of them.
A method can return a lambda expression in the body, and since this can fall back on the parameters, we
have thus also created a parameterized Runnable.
// Also a unit?
String unit = matcher.group( 2 );
if ( unit == null )
return seconds;
switch ( unit ) {
case "s": break;
case "m": seconds = TimeUnit.MINUTES.toSeconds( seconds ); break;
case "h": seconds = TimeUnit.HOURS.toSeconds( seconds ); break;
case "d": seconds = TimeUnit.DAYS.toSeconds( seconds ); break;
default:
System.err.printf( "sleep: invalid interval unit '%s'%n", arg );
System.exit( 4 );
}
return seconds;
}
long seconds = 0;
for ( String arg : args )
seconds += parseSleepArgument( arg );
Parsing the arguments takes up the most space. We move the parsing out to a
parseSleepArgument(String) method, which returns the wait time in seconds. A regular expres‑
sion helps to recognize a decimal number of any length followed by a nondecimal character. Both parts
are enclosed in round brackets so that we can later access exactly this number and unit via the groups.
If the regular expression does not match, we print an error message and exit the program with
System.exit(int). With a return not equal to 0 we express an error. Error-free programs generally
return 0.
If there were two groups in the string, we continue. We convert the first match group to the number
of seconds. This can lead to an exception if the number is too large and does not fit into a long. We have
excluded letters in a number by choosing the regular expression, but a regular expression cannot restrict
72 Java Programming Exercises
the size. A NumberFormatException will alert us if an error occurs; then there will also be an out‑
put, and the program will terminate.
The number has been recognized, but does a unit follow? We extract the second match group, and if it
is null, we can exit the method because no unit was specified. If it is not null, we have a character and
check which character it is. When the answer was s, we abort the switch-case and do not need to do
any conversion. If it was an m, h, or d, the constants in the TimeUnit enumeration help to convert it to
seconds. When none of the four characters were used, an error message is also displayed, and the applica‑
tion is ended. In the best case, the seconds are returned.
The main(…) method also first performs a test to see if any arguments were supplied before continu‑
ing. If not, an error message is shown and System.exit(…) terminates the program with an error code.
This is more correct than using return to exit the main(…) method because that would result in an exit
code of 0, which signals something wrong to a calling program on the shell.
The extended for loop runs all arguments from the command line, we don’t need an index. We
transfer each string to our parseSleepArgument(…) method and sum up the result in the seconds
variable. Finally, the sleep(long) method puts the main thread to sleep for the specified number of
seconds. The sleep(…) method throws an InterruptedException, a checked exception that we
must handle; however, we don’t have to put anything in catch because there is no outside thread to
interfere with us.
@Override
public void run() {
try {
FileTime oldLastModified = Files.getLastModifiedTime( path );
while ( true ) {
TimeUnit.MILLISECONDS.sleep( 500 );
Since the run() method of the Runnable interface has no parameter list, we have to transfer pos‑
sible values or return values differently. The solution is simple: if we have a class that implements the
Runnable interface, then we can use a parameterized constructor so that we can introduce the state from
outside. This is precisely what the constructor of FileChangeWatcher does—it takes a file name and
a consumer. The constructor performs a test to check if null is passed accidentally, and then throws an
exception; otherwise, the constructor stores the states in private instance variables. The factory method of
the Paths class automatically throws a NullPointerException, so we can save ourselves a separate
null test on the filename.
The central method run() is called by the thread. The first thing we do is get the time when the file
was last modified. This is the first reference point we will later compare to. All this happens in an infinite
loop because we don’t stop after one check or one detected change.
The body of the loop starts with a wait. We let the thread rest for 30 seconds so as not to put a perfor‑
mance bottleneck because of the tight loop. Then the time of the last change is polled again and compared
with the old time. If the time is not the same, then the file has changed. We have to inform our consumer in
that case, call the accept(…) method on the passed callback object and hand over the path. One should
be aware that the call is synchronous and blocking. That is, if the callback method works for a very long
time, our thread will not be able to continue watching the file either.
After calling the consumer, we set the old date to the current date and start comparing again in the
loop.
The try-catch block surrounds both the loop and the initial time query to handle exceptions that
may occur in several places. Exceptions can be thrown by input/output methods and sleep. All exceptions
are caught outside the loop as a design decision. Alternatively, one could catch exceptions inside the infinite
loop, allowing the file change check to continue even if exceptions are thrown. However, if the file is deleted,
the thread should stop running. In case of exceptions, they are encapsulated in a RuntimeException
and thrown, causing the thread to abort. Threads terminated by RuntimeException can be detected
using a Thread.UncaughtExceptionHandler. The next exercise will cover this topic.
A somewhat invisible place for an exception is the callback operation, which can also throw a
RuntimeException. If left unhandled, it would lead to the death of the thread; however, we don’t
do anything else either … We could also consider separating our exceptions from the exceptions in the
Consumer, or reporting them with a second callback object.
Catch Exceptions
com/tutego/exercise/thread/GlobalExceptionHandlerDemo.java
Thread indexOutOfBound =
new Thread( () -> System.out.println( (new int[0])[1] ) );
indexOutOfBound.setUncaughtExceptionHandler( ( t, e ) -> {} );
indexOutOfBound.start();
An enum implements an UncaughtExceptionHandler, which together with the single static vari‑
able INSTANCE results in a singleton. The enumeration element implements the uncaughtExcep‑
tion(…) method from the functional interface. When activated, the JVM passes the method a reference
to the dying thread and the unhandled exception. The type is throwable, which means that an error
can also be reported.
The main(…) method globally sets the UncaughtExceptionHandler, which consequently
gives for all threads. The first thread will throw an ArithmeticException by dividing by 0, which
our globally set UncaughtExceptionHandler will report directly.
The second thread will also be aborted by an exception, but here we have set a local
UncaughtExceptionHandler via a lambda expression, which reports nothing.
The main thread also causes trouble because the constructor of the URI class will throw an excep‑
tion with this argument. We did not catch and handle this by a try-catch block, but the main(…)
method forwards it to the JVM. This also activates the set UncaughtExceptionHandler.
Iterator<String> names =
Arrays.asList( "Polly Zist", "Jo Ghurt", "Lisa Bonn" ).iterator();
String[] gifs = {
"Dragon", "Pomsies", "Coat", "Tablet", "Doll", "Art Station",
"Bike", "Card Game", "Slime", "Nerf Blaster" };
for ( String gift : gifs ) {
Thread.sleep( ThreadLocalRandom.current().nextInt( 1000, 2000 ) );
crew.submit( new DistributeGift( gift ) );
}
}
The Runnable is the action that is executed by the threads. Java provides two interfaces in the frame‑
work for program code to be executed: Runnable and Callable, where Callable is only used when
programs want to return something in the background, this is not necessary in our case.
We can implement the Runnable using a lambda expression. However, in this case, it’s not possible
because each Runnable needs to be uniquely associated with a String, which is the gift. If the lambda
expression could access the data through a variable, it would be a different scenario. We use a record that
implements Runnable and accepts the gift to distribute through its constructor. The Runnable dis‑
plays an output on the screen, pauses for a moment, and then completes its execution.
In the next step, we build a thread pool. Whether the Runnable is run by a brand-new or existing
thread is insignificant. The creation of threads costs significantly more compared to normal object cre‑
ation. To build a thread pool, we can use the parameterless method Executors.newCachedThread‑
Pool() or a special variant that allows us to build and parameterize the thread of the pool itself. This is
not required by the task, and it has more cosmetic reasons because this way we can set the name of the
thread. To be able to adapt as much of the infrastructure as possible, we query the defaultThread‑
Factory(), create a thread via newThread(…), and can then set the name of the thread with the
familiar Thread method setName(…). The crew member name is obtained from the Iterator via
the next() method, and since the task does not require more than three threads in the thread pool, no
exception occurs.
The last part is the bumping of the work packages. The program runs through the array and always
creates new Runnable objects with the submitted gifts. submit(…) passes the Runnable to the
thread pool, which selects a free thread or creates one at the beginning and thus distributes the gift.
The implementation of the Callable interface offers no surprises. The parameterized constructor takes
a URL object and stores it in a private variable, making the URL accessible later in the call() method.
The implementation of the call(…) method is not much different from the template, except for the dif‑
ference that we do not catch exceptions, but can easily pass them up from the call() method. Then, if
an exception occurs, get(…) is interrupted by an ExecutionException.
A usage could look like this:
com/tutego/exercise/thread/PageLastModifiedCallableDemo.java
try {
System.out.println(
executor.submit( callable ).get( 1, TimeUnit.MICROSECONDS )
);
}
catch ( InterruptedException | ExecutionException | TimeoutException e ) {
e.printStackTrace();
}
try {
ZonedDateTime wikiChangedDateTime = dateTimeFuture.get();
System.out.println( wikiChangedDateTime );
System.out.println(
Duration.between( wikiChangedDateTime,
ZonedDateTime.now( ZoneId.of( "UTC" ) ) )
.toMinutes()
);
}
executor.shutdown();
The second try-catch block is for the first Callable sent. The dateTimeFuture is queried for
the result with a blocking get(), call, and it’s likely that a few milliseconds have already elapsed, used
by the runtime environment for managing the prior requests. When we examine the catch blocks, we
see that when there’s a time constraint, we must address an additional TimeoutException. However,
when using the get() method without parameters, we only need to handle InterruptedException
and ExecutionException.
For the calculation of the difference, we use the method between(…) of the class Duration. The
static method gets two arguments: once the time from the Future and the current time. We must remem‑
ber not to just ask for the current time with now(), but we must ask for the current time in the UTC time
zone. Otherwise, unless coincidentally, the program itself runs in a UTC ± 0 environment, the difference
would be incorrect.
TWhheen fyloou wlearugsh ,ne tehd eysu nalshli nlea uganhd .I Wnheeend
Cyaoput acirny ,C iyaoouC icaroy taloo nbee .h
_,.-'~'-.,__,.-'~'-.,__,.-'~'-.,__,.-'~'-.,__,.-'~'-.,_
appy
_,.-'~'-.,__,.-'~'-.,__,.-'~'-.,__,.-'~'-.,__,.-'~'-.,_
Appending letters is a critical section that must be protected. synchronized blocks are somewhat
outdated, and we want to use Lock objects. For our use case, the ReentrantLock implementation is
suitable.
com/tutego/exercise/thread/WriteInFriendshipBook.java
Threads coordinate with each other via the Lock objects. When one thread enters the critical section,
another thread will have to wait until the critical section is unlocked. To enter and exit the block, there are
two central methods in Lock: lock() and unlock(). The constructor of ReentrantLock is over‑
loaded, and the parameterized variant uses a boolean parameter to determine whether the Lock is fair
or not. Fair in this context means that the threads that wait for the longest are allowed to enter the released
block first. Otherwise, the behavior is nondeterministic. This is also one of the differences from the syn‑
chronized keyword, where the virtual machine can choose any thread. Whether the allocation is fair or
not depends on the implementation of the JVM, and the fairness in synchronized is not controllable.
The proposed solution has adapted the source code a bit. One place is outside the Author class dec‑
laration because the Lock object must be available to all threads, since that is what the threads coordinate
against. The second change is inside the run() method because the operation to be saved is writing to the
journal. Before the loop the block is closed with lock(),all characters are appended, the separator is writ‑
ten, and finally unlock() is called again, which releases the block for the next thread. An unlock()
should always be in a finally block because if a Lock is requested, it should always be released, even
if there is a return or an exception that exits the method. finally blocks are always processed, regard‑
less of whether there was an exception or not.
Besides the lock() method, there is a second method lockInterruptibly() which can be
interrupted by an interrupt from outside. lock() does not react to an interrupt from outside, which means
the catch of the InterruptedException is only valid for the sleep(…) method.
1. The declaration of the record, which implements Runnable and accesses the Semaphore.
2. The construction of names.
3. The construction of the Semaphore.
4. The creation of the Runnable instances and execution over a thread pool.
Since there are six seats at the table and two seats are already occupied by Bonny Brain and Captain
CiaoCiao, it leaves four seats that different guests can switch between.
All guests have a name, which is stored in the object. The name appears three times in the output:
1. At first, the name of the guest is printed because the guest is waiting and may not have a place
yet. A thread must first get permission from the Semaphore using acquire(); the method
blocks if the maximum number of four free seats has already been reached. Everyone waits for
a free seat before taking one, and the first four people are given seats right away, as can be seen
in the console output.
2. If acquire() is unblocked, the thread can spend time at the table with the captains together
with the other four threads.
3. After a waiting period, the finally block is processed and the name of the guest is printed
again because the guest now leaves the table. It is important to call the release() method on
the Semaphore, so that other waiting guests can come to the table.
A simple algorithm generates the demo data. A list is pre‑populated with some strings. This list is now
run with the original length, and new strings are generated starting with Admiral in the front, and more
strings starting with Commander.
Now only the Runnable instances have to be created and the ExecutorService can start them.
record Insulter(
String[] insults,
Lock lock,
Condition condition
) implements Runnable {
@Override public void run() {
while ( ! Thread.currentThread().isInterrupted() ) {
lock.lock();
try {
Thread.sleep( ThreadLocalRandom.current().nextInt( 1000 ) );
String name = Thread.currentThread().getName();
int rndInsult = ThreadLocalRandom.current().nextInt( insults.length );
System.out.println( name + ": " + insults[ rndInsult ] + '!' );
condition.signal();
condition.await();
}
}
}
}
}
String[] insults1 = {
"Trollop", "You have the manners of a trump",
"You fight like a cow cocky", "Prat",
"Your face makes onions cry",
"You are so full of s**t, the toilet's jealous"
};
String[] insults2 = {
"Wazzock", "I've spoken with rats more polite than you",
"Chuffer", "You make me want to spew",
"Check your lipstick before you come for me",
"You are more disappointing than an unsalted pretzel"
};
The solution relies on Condition, and that has two critical methods: for notifying other waiting threads
and for waiting for a notification.
The Insulter record in our scenario is a Runnable that receives various curses, a Lock object, and
a Condition object through its canonical constructor. The run() method has an infinite loop that can be
ended using an interrupt since insulting never ceases, and every action prompts a reaction. To use a Condition
object, lock() must mark a critical section because Condition objects originate from a Lock object.
After the time of thinking, a random curse word is selected and output, and then signal() is used
to inform the other thread. With the following await() we wait again in the thread for the signal of the
other thread.
The flow will be as follows: main(…) will start two Insulter threads, and one of the two threads
will enter the area first via the Lock object, then lock and claim the block exclusively for itself. The second
thread will come in a little later, and hang in the Lock and get no further. The first thread then starts curs‑
ing and finally signals the other waiting thread that it is done. After signaling, the first thread also waits
for a signal. It is important to understand that the associated lock is temporarily released so that the other
thread can run into the protected block, receive the signal, and then in turn perform the action. While one
thread waits in await(), the other thread performs its operation and later sets a signal again. This wakes
up the waiting thread again, it gets the lock back and continues its operation while the other side waits.
class Paintbox {
freeNumberOfPens = maximumNumberOfPens;
System.out.printf( "Paintbox equipped with %s pens%n", freeNumberOfPens
);
}
condition.await();
freeNumberOfPens ‑= numberOfPens;
}
catch ( InterruptedException e ) { Thread.currentThread().interrupt(); }
finally {
lock.unlock();
}
}
acquirePens(int numberOfPens) is called by the children who want to take a certain number of
pens from the paintbox. The operation takes place in a critical section, so Lock is used to lock the section
for other threads. A loop condition checks if the number of free pens is less than the number of desired
pens, and if it is, then it must wait. If a signal comes later, it must be asked repeatedly whether this condi‑
tion still applies. It is a programming error if an if statement is used instead of a loop. After the end of
the while loop, the number of free pins is reduced and the critical section is released again. Consider:
when waiting for a signal, the lock is temporarily released.
releasePens(int numberOfPens) is easier: it increases the number of free pens and then
signals other waiting threads that pens are available again. signalAll() signals all waiting threads.
Signaling, just like waiting, must occur in a locked section, which is terminated in a finally block with
unlock() as usual.
The record Child implements Runnable and takes the name of the child and the paintbox in the
constructor.
com/tutego/exercise/thread/Kindergarten.java
try {
TimeUnit.MILLISECONDS.sleep( random.nextInt( 1000, 3000 ) );
}
catch ( InterruptedException e ) { Thread.currentThread().interrupt();
}
paintbox.releasePens( requiredPens );
System.out.printf( "%s returned %d pens%n", name, requiredPens );
try {
TimeUnit.SECONDS.sleep( random.nextInt( 1, 5 + 1 ) );
}
catch ( InterruptedException e ) { Thread.currentThread().interrupt();
}
}
}
}
The run() method runs indefinitely in theory unless the thread is terminated externally with an inter‑
rupt. The following happens in the body of the while loop:
In the proposed solution, we have two different Runnable types. One Runnable evaluates the winner,
and the other Runnable executes the hand signal. The Runnable is implemented by lambda expres‑
sions and accesses a common data structure handSigns. Each player makes a random hand sign, places
it in the Queue, and waits for the barrier to end.
The barrier itself is initialized with size 2 and initialized with the Runnable determineWinner,
which is executed whenever it came to the second call of await() on the barrier. The determineWin‑
ner takes the two hand signs out of the queue, uses beats(…) to determine which player scored in which
way, and prints a console message about the winner, loser, or tie.
The ScheduledExecutorService helps to replay the actual game every second and run the
Runnable behind playScissorsRockPaper twice.
records.forEach(
(time, name) ‑> System.out.printf( "%s in %d ms%n", name, time )
);
The solution uses two CountDownLatch objects. The first startLatch we initialize with 1, and
when multiple threads are started, they wait for this start CountDownLatch to become 0. The second
CountDownLatch endLatch we initialize with 10, the number of athletes, and whenever an athlete
reaches the finish line, the endLatch is decreased.
For sorting by run times, we resort to a sorted associative memory. Since the threads write con‑
currently to the data structure, the data structure must support this type of access. For this, the pack‑
age java.util.concurrent offers various data structures. We use a ConcurrentSkipListMap,
an associative memory that can handle concurrent writes. The class implements the interface
ConcurrentNavigableMap.
The athletes are threads that are given a simple name via the constructor of the Thread class.
After the threads are started, they all wait at the startLatch to reach 0. The call startLatch.
countDown() fires the starting gun, and all the athlete threads start running. Each of the threads waits
a random time, then writes itself to the data structure and decrements the counter in endLatch. The
main program also waits with an endLatch.await() to unblock, and this is achieved by all athletes
decrementing the counter so that it becomes 0. After the end of the run, forEach(…) iterates over the
map and outputs the time along with the name.
Data Structures
and Algorithms 5
Data structures store essential information in the application. They are organized via lists, sets, queues,
and associative maps. In Java, the interfaces and classes around data structures are called Collection API.
Since there are so many types to choose from, the purpose of this chapter is to bring order to the confusion
and to illustrate the use of the corresponding collections through the exercises.
Prerequisites
• java.util.Collection
• java.util.Collections
• java.util.Iterable
• java.util.List
• java.util.ArrayList
• java.util.LinkedList
• java.util.ListIterator
• java.util.Set
• java.util.HasSet
• java.util.TreeSet
• java.util.LinkedHashSet
• java.util.SortedSet
• java.util.Map
• java.util.HashMap
• java.util.TreeMap
• java.util.SortedMap
• java.util.WeakHashMap
• java.util.Properties
• java.util.Queue
• java.util.Deque
• java.util.BitSet
• java.util.concurrent.SynchronousQueue
• java.util.concurrent.ForkJoinPool
DOI: 10.1201/9781003495550-6 85
86 Java Programming Exercises
As developers, we need to know interfaces and implementations, and to review, let’s look again at the
central types we’ll encounter more often in this chapter:
«interface» «interface»
Iterable Map
«interface»
SequencedMap HashMap
Collection
NavigableSet
TreeSet
FIGURE 5.1 UML diagram of selected data structures and type relationships.
5 • Data Structures and Algorithms 87
To note:
• Iterable is the most general interface, representing what can be traversed; Iterable pro‑
vides Iterator instances. Not only data structures are Iterable.
• Collection is the top interface that really represents data structures. It specifies methods for
adding elements to the collection or for deleting them.
• Under Collection are the actual abstractions, whether it is a list, set, or queue. Below are
the implementations.
• Some operations are not with the data types themselves, but are outsourced to a class
Collections. Similar applies to arrays, where there is also a utility class Arrays.
We want to build a decision tree for the classes and interfaces java.util.Set, java.util.List,
java.util.Map, java.util.HashSet, java.util.TreeSet, java.util.Hashtable, java.
util.HashMap, and java.util.TreeMap. The following considerations must be made in the selec‑
tion process:
If access is from a key to a value, this is generally an associative map, that is, an implementation of the
Map interface. Implementations of Map are HashMap, TreeMap, and the outdated class Hashtable.
However, lists are also special associative stores, where the index is an integer starting at 0 and ascend‑
ing. Lists work quite well whenever the key is a small integer and there are few spaces. The association of
arbitrary integers to objects does not work well with a list.
Duplicates are allowed in lists, but not in sets and associative stores. There are indeed requirements
that a set should note how often an element occurs, but this must be implemented itself with an associative
memory that associates the element with a counter.
All data structures allow fast access. The only question is what to ask for. A list cannot quickly
answer whether an element is present or not because the list must be traversed from front to back to do so.
With an associative store or set, this query is much faster because of the internal organization of the data.
This test of existence can be answered even somewhat faster for data structures that use hashing internally
than for data structures that keep elements sorted.
Lists can be sorted, and a traversal returns the elements in the sorted order. A TreeSet and a
TreeMap are also sorted by criteria. The data structures with the hashing method have no user‑defined
order of sorting.
Data structures can be divided into three groups: Data structures since Java 1.0, data structures
since Java 1.2, and data structures since Java 5. In the first Java version, the data structures Vector,
Hashtable, Dictionary, and Stack were introduced. These data structures are all thread‑safe, but
they are no longer used today. In Java 1.2, the Collection API was introduced; all data structures are not
thread safe. In Java 5, the new package java.util.concurrent has been introduced; all data struc‑
tures in it are safe against concurrent changes.
islands1.add( "Revillagigedo" );
islands1.add( "Clipperton" );
System.out.println( islands1.contains( "Clipperton" ) );
How can the output be explained? Does it perhaps deviate from the presumed behavior? Could it be due
to an important requirement that String fulfills, but not StringBuilder?
LISTS
For the exercises, let’s start with the simplest data structure, lists. Lists are sequences of information
where the order is maintained when appending new elements, and elements can occur multiple times.
Even null is allowed as an element.
For each crew, Captain CiaoCiao makes sure that there are as many cooks as musicians.
Exercise:
1. Magnetic declination.1
2. Speed of water current.
3. Weather.
4. Comments and general observations.
Bonny Brain is searching for a specific comment in a list of strings. To do so, they need to delete the first,
second, and third entries so that only the fourth entry, which contains the desired comment, remains.
Task:
Examples:
• A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4", "C1", "C2", "C3", "C4"
→ "A4", "B4", "C4".
• Empty list → nothing happens.
• "A1" → exception Illegal size 1 of list, must be divisible by 4.
Exercise:
Examples:
• If the list contains the numbers 1, 2, 3, 4, 5, the list stays that way.
• If the list contains the numbers 1, 2, 3, 2, 1, the sequence is shortened to 1, 2, 3.
Exercise:
Examples:
Rain, sun, rain, rain, hail, snow, storm, sun, sun, rain, rain, sun.
In the list, sun occurs three times in a row. That is what we want to know. Although rain occurs more
often in the list overall, that is not relevant to the solution.
Task:
Example:
Sum: 216,65 €
int[] numbers1 = { 1, 2, 3 };
System.out.println( Arrays.asList( numbers1 ).contains( 1 ) );
Integer[] numbers = { 1, 2, 3 };
System.out.println( Arrays.asList( numbers ).contains( 1 ) );
System.out.println( Arrays.asList( 1, 2, 3 ).contains( 1 ) );
5 • Data Structures and Algorithms 93
Task:
Examples:
• Gnocchi, zucchini, peppers, cream, broth, milk, butter, onion, tomato, salt, bell pepper →
gnocchi, zucchini, cheese, peppers, cheese, cream, broth, milk, butter, onion, cheese, tomato,
cheese, salt, bell pepper.
• Cheese → Cheese
class Ship {
private List<String> persons = new ArrayList<>();
void addName( String name ) { persons.add( name ); }
boolean contains( String name ) { return persons.contains( name ); }
@Override public String toString() {
return "" + persons;
}
}
There are 100 ships at the port, stored in a LinkedList<Ship>. Covid Cough is hiding in an unknown
ship, let’s simulate that:
Bonny Brain arrives at one of the many entrances to the harbor, and there are ships to her right and left:
The only access to the ships is given by the ListIterator. Keep in mind that the ListIterator can
only be used to go forward and backward, there is no random access!
5 • Data Structures and Algorithms 95
Task:
• Visit the ships with the ListIterator, and find Covid Cough.
• Is there a strategy to find the person most efficiently? It is known how many ships there are in
total, 100. Since the index is known where Bonny Brain enters the harbor, we also know how
many ships are to the left and right of the entrance.
MusicalChairs musicalChairs =
new MusicalChairs( "Laser", "Milka", "Popo", "Despot" );
musicalChairs.rotate( 2 );
System.out.println( musicalChairs ); // Popo, Despot, Laser, Milka
Task B:
Consider the case where the list might be empty in the solution.
enum Planet {
96 Java Programming Exercises
Task:
• Program a console application that builds a random sequence of all planets in the first step.
Consider how we can use the shuffle(…) method from java.util.Collections for this.
• Iterate over this random sequence of planets, and generate a console output that asks for the
diameter of those planets. As a choice, the recruit should be shown four diameters in kilome‑
ters, where one diameter is the correct one and three diameters are from different planets.
• If the recruit enters the correct diameter, a message appears on the screen; if the wrong diam‑
eter is entered, the console output reports the correct diameter.
Example:
139822 km
50724
Correct!
SETS
Sets contain their elements only once. They can be unsorted or sorted. Java provides the Set interface for
abstraction; two important implementations are HashSet and TreeSet.
A whole series of questions arise with sets:
Some of these operations can be answered directly using the Set data type. For example, there are the
methods isEmpty() or contains(…). Set operations, in particular, are not very well‑supported,
and programmers sometimes have to take workarounds for them. For subsets, for example, there is the
Collections method disjoint(Collection<?>, Collection<?>), but it returns a boolean
that says whether the two collections have no common element.
Let’s answer some questions with tasks.
Task:
• What is the percentage of similarity between the two? Also, what are some methods we could
use to answer this question?
Look up methods in Set to see if they can be used to form subsets or intersections.
A file with words can be transformed into a data structure like this:
// …
}
// etc.
}
The constructor indicates that the new iterator gets an existing iterator as a parameter. So, a call could
look like this:
List<String> names = …;
Iterator<String> UniqueIterator = new UniqueIterator( names.iterator( ) );
Example:
String[][] array = {
{ "red", "#FF0000" },
{ "green", "#00FF00" },
{ "blue", "#0000FF" }
};
Map<String, String> colorMap = convertToMap( array );
System.out.println( colorMap ); // {red=#FF0000, green=#00FF00, blue=#0000FF}
5 • Data Structures and Algorithms 101
// A .‑ N ‑. 0 ‑‑‑‑‑
// B ‑... O ‑‑‑ 1 .‑‑‑‑
// C ‑.‑. P .‑‑. 2 ..‑‑‑
// D ‑.. Q ‑‑.‑ 3 ...—
// E . R .‑. 4 ....‑
// F ..‑. S ... 5 .....
// G ‑‑. T ‑ 6 ‑....
// H .... U ..‑ 7 ‑‑...
// I .. V ...‑ 8 ‑‑‑..
// J .‑‑‑ W .‑‑ 9 ‑‑‑‑.
// K ‑.‑ X ‑..‑
// L .‑.. Y ‑.—
// M ‑‑ Z ‑‑..
Task:
Example:
String[] words = {
"Baby Shark", "Corona", "Baby Yoda", "Corona", "Baby Yoda", "Tiger King",
"David Bowie", "Kylie Jenner", "Kardashian", "Love Island",
"Bachelorette",
"Baby Yoda", "Tiger King", "Billie Eilish", "Corona"
};
System.out.println( importantGossip( words ) );
102 Java Programming Exercises
outputs
Keep in mind that it is not about the individual words like Baby or Yoda, but always about the whole
string, for example, Baby Yoda or Baby Shark.
For the background, let's use #89cff0 or #bcd4e6, and for the text, maybe
#fffaf0 or #f8f8ff.
She finds out that a specification like #RRGGBB stands for the red, green, blue (RGB for short) part of a
color, coded in hexadecimal. Fortunately, there are “translation tables” like https://gist.githubusercontent.
com/ullenboom/03a7ff2f742fe60752a975b1539d0273/raw/colors.csv, which contains lines like
amber,"Amber",#ffbf00,255,191,0
aqua,"Aqua",#0ff,0,255,255
blush,"Blush",#de5d83,222,93,131
wine,"Wine",#722f37,114,47,55
Occasionally, the file contains the color values only with three symbols, like in the example aqua with
0ff. In this case, the individual color values are doubled, so #RGB then becomes #RRGGBB.
Task:
1. Create a new class Color for the representation of colors. Each color has a name (String
name) and an RGB value (int rgb). Write—or generate via the IDE—the method
toString(). Add further methods if that is useful.
2. Create a new class ColorNames.
• Give the class an instance variable HashMap<Integer, Color> colorMap, so that
ColorNames can internally remember all Color objects in a Map; the keys of the Map
are the RGB values as integer, and the associated value is the corresponding Color object.
• Copy the file https://gist.githubusercontent.com/ullenboom/03a7ff2f742fe60752a975b15
39d0273/raw/colors.csv locally to disk.
• Create a constructor that reads the file. We can use Scanner for this, or read the file
completely with Files.readAllLines(Paths.get("colors.csv")), which returns
a List<String>.
• Split each line of the CSV source, extract the color name (second column) and RGB value
(third column). Tip: the color value can be converted to an integer using a Java method:
Integer.decode("#722f37") returns 7483191. Remember that color names can be
in the form #RGB and #RRGGBB.
• Transfer the color name and integer value to Color objects, and place them in the Map.
• Add a method decode(int rgb) that returns the associated Color object for an RGB
value.
Example:
• Create a new class with the main(…) method and copy the two lists into the program:
List<String> words = Arrays.asList( "house", "mouse", "horn", "cannon" );
List<String> missingLettersWords = Arrays.asList( "_ouse", "ho__", "ca__
on", "gun", "__e__", "_____" );
• Match each word from missingLettersWords with all possible words from the dictionary
words where the underscore symbolizes unknown characters.
• The length of the suggested words from the dictionary must be equal to the word length of the
unreadable word.
• At least one character must be given.
Example:
FIGURE 5.2 Possible routes from the start to the finish. (Source: Wikipedia.)
The image shows 4 × 4 street blocks and there are 14 possibilities. After some time of looking for, the
crew finds the stuffed animal, they are lucky!
Captain CiaoCiao ponders: What if there were 5 or 10 blocks—wouldn’t the number of paths then be
much too large to search?
The answer to the question is provided by mathematics. What is being searched for here is a mono‑
tonic path for a square with n × n cells. The number of possible paths provides the Catalan number, which
is calculated as follows:
Cn = (2n)! / (n+1)! n!
Task:
• Convert the formula by the method BigInteger catalan(BigInteger n). Use your own
internal method BigInteger factorial(BigInteger n) for the factorial calculation.
• Three factorials must be calculated in the formula: n!, (n+1)! and (2n)! It is (n+1)! nothing else
than n! × (n+1) is, so n! has to be calculated twice; also on the way to the calculation of (2n)! the
intermediate result (n+1)! arises. So many multiplications have to be done twice, so the products
should be cached: resort to the data type WeakHashMap for this.
• Compare the times when we call the catalan(…) method twice with the same parameters.
Use the following code as a template:
From the API documentation, we see that firstEntry() and lastEntry() return the smallest
and largest element, respectively. The return type is Map.Entry<K,V>.
Given a key key, the following methods return relative to that key:
• ceiling*(K key): Returns a result that is greater than or equal to this key.
• floor*(K key): Returns a result that is less than or equal to the given key.
• lower*(K key): Returns a result that is strictly less than the given key.
• higher*(K key): Returns a result that is strictly greater than the given key.
In the first case, fromKey is inclusive and toKey is exclusive; this corresponds to the usual convention
of Java. The second method allows more precise control over whether the start or end element belongs.
Task:
p.setLocation( 1, 2 );
System.out.println( map.get( p ) );
List<Set<String>> families =
Arrays.asList( gombonoGifts, banannaGifts, cilimbiGifts );
Since Bonny Brain is a perfection strategist, she wants to know if things are brought multiple times.
Task:
Example:
PROPERTIES
The class Properties is a special associative memory that associates only string with strings. The class
not only represents a data structure but can also read and write files called property files. These are text
files that are usually used for configuration. Key‑value pairs are separated in the file by =. It is also pos‑
sible to read and write the values in an XML format, but this is uncommon.
Task:
Write a class PropertiesConfiguration that decorates a Properties object. The most gen‑
eral method returns an Optional that is either filled or empty if the key does not exist:
The advantage with the Optional is that alternatives for default values can easily be determined: conf.
getProperty("rank").orElse("Captain").
Other methods of PropertiesConfiguration are to perform conversions:
If there was no associated value to the key, the container is empty. If the conversion to an error fails, that
also results in an empty container.
Example for the API:
Example:
An example of use:
• Stack.
• Queues.
With a stack, you can only insert elements at one end and must remove the elements again at this end. The
principle is also called LIFO: last in, first out. In contrast to this is the queue. With it, that is read out first,
which was added also as first. The principle is called FIFO: first in, first out.
Pure stacks and queues do not exist in Java, only interfaces implemented by lists.
1. Write a program that first tokenizes a string like "12 34 23 + *". Hint: For splitting, you can
use split(…) of String or a Scanner.
2. After splitting, the result is to be evaluated. Start with a fixed string for testing.
5 • Data Structures and Algorithms 109
3. Read in a string from the command line so that we have a real RPN calculator.
4. What errors and problems need to be handled and caught? How should we handle errors?
BITSET
The class BitSet is a space‑saving and performant alternative to boolean arrays. The data structure
is useful when you need a mapping of an integer to a truth value. The data structure can quickly answer
whether an index (a positive integer) is associated with true or false. If the number of bits becomes
too large, or if there are large gaps, https://github.com/brettwooldridge/SparseBitSet is a good alternative.
Forget No Ship ⭑
Once a year, there is an exercise where 13 ships have to be boarded. Each of the ships is uniquely identified
by a number from 10 to 22. Once the adventure is over, our trusty Bonny Brain receives a list of the ships
that their crew boarded. It could be something like this:
• {10, 20, 21, 15, 16, 17, 18, 19, 11, 12, 13, 14, 22 }
Sometimes she gets lists where numbers are missing or appear twice:
Such lists show Bonny Brain that ships were forgotten or repeatedly raided during the exercise. Since
manual checking is inconvenient, software should do the job.
110 Java Programming Exercises
Task:
• 🐩🐏🐴🦋🐙
• 🐴🐏🐧🐸🦋🐌
Captain CiaoCiao notes that both have poor memories, and only wants to have animals searched for that
are named by both.
Task:
Examples:
Since the result is urgent, implement the method so that the runtime is linear with the length of the strings,
in computer science jargon: O(N+M) if N and M are the lengths of the strings. All Unicode characters are
allowed.
data types from the package java.util.concurrent. The package contains different thread‑safe data
structures, which work correctly even with an arbitrary number of parallel accesses and have an excellent
performance.
Loading Ship ⭑⭑
Captain CiaoCiao and the crew are getting ready for the next big adventure on Gazorpazorp Island. Five
employees put crates and barrels on the loading ramp, and 10 employees stow the goods in the ship. There
can be no more than five objects on the loading dock at a time.
Task:
You can’t deduce the semantics from the method names; you have to learn the difference. For our
task, only one column, and these methods come into question.
112 Java Programming Exercises
Example:
Assuming that PriorityQueue<Message> tasks is a correctly initialized data structure, the
following program will produce the output shown:
SUGGESTED SOLUTIONS
true
false
The explanation for this lies in the implementation of the contains(Object) method. It internally falls
back to a method that returns the location of the element, but we can read from the current implementation
of the Java library quite well what must be true for a search:
Excerpt from OpenJDK’ implementation java.lang.ArrayList.
The indexOfRange(…) method searches for the position of the object o, and first distinguishes whether
to search for the null reference or a regular object. In our case, the object in the query is not null,
so the equals(…) method is used, that is, a loop compares each element in the list with our value via
the equals(…) method. Conversely, this means that the query will only work if we have also imple‑
mented a reasonable equals(…) method. And this is precisely the problem: the String class pro‑
vides an equals(…) method, not the one from java.lang.Object, but an overridden method. The
StringBuilder class, however, has no implementation of an equals(…) method, but inherits this
method from the superclass Object. In the superclass, however, only a reference comparison is imple‑
mented, which means that the content is not relevant at all. However, since our StringBuilder objects
in the list are always different, the reference comparison will never return true.
The proposed solution presents a different approach. It can be compared to a scale, where the quantity
of two items needs to be identical on both sides to achieve balance. Surprisingly, the specific weight of
each item is irrelevant as long as equilibrium is reached.
com/tutego/exercise/util/SameNumberOfCooksAndMusicians.java
The solution variant implemented here loops over the list and counts up a variable weight if there is a
cook, and counts down the variable if there is a musician in the list; of course, it could be the other way
around. If the number of cooks and musicians is the same, weight will end up being 0.
The keyword switch can be used in another variant, as an expression:
com/tutego/exercise/util/SameNumberOfCooksAndMusicians.java
In this variant, we need to introduce a default branch. This creates one more line of code, and the nota‑
tion is likely to be less attractive than the first notation or a variant with the conditional operator.
There is another way to solve the problem. It remains with the idea of creating a kind of scale, where
some enumeration elements are transferred to +1 and the others to -1:
com/tutego/exercise/util/SameNumberOfCooksAndMusicians.java
int result = 0;
for ( CrewMember member : crewMembers ) {
// CAPTAIN -+
// NAVIGATOR -+ |
// CARPENTER -+ | |
// COOK -+ | | |
// MUSICIAN -+ | | | |
// v v v v v
int zeroOrOneOrTwo = ((1 << member.profession.ordinal()) & 0b1_1_0_0_0) / 8;
int minusOneOrZeroOrPlusOne = (zeroOrOneOrTwo / 2) - (zeroOrOneOrTwo & 1);
result += minusOneOrZeroOrPlusOne;
}
return result == 0;
This approach might cause head shaking at first sight, but it is good to have seen and understood this way
once. Let’s deduce the solution.
116 Java Programming Exercises
Given is an enumeration with various elements, which all have one position, the so-called ordinal
number. CrewMember.Profession.CAPTAIN.ordinal() is 0, and CrewMember.Profession.
COOK.ordinal() is 3. If we write 1 << x, and x is between 0 and 5, we get 1 shifted left by x positions
and padded with zeros on the right. In other words, we get in binary notation the numbers
• 0b000001 (CAPTAIN).
• 0b000010 (NAVIGATOR).
• 0b000100 (CARPENTER).
• 0b001000 (COOK).
• 0b010000 (MUSICIAN).
• 0b100000 (DOCTOR).
We are interested in 0b001000 (COOK) and 0b010000 (MUSICIAN). To test whether the third or fourth
bit is set in a number, we combine this number with the bit pattern 0b11000. If one of the two bits is set,
the bit remains, all other bits are set to 0 by the AND operation with 0. In the end, because both bits can‑
not be set at the same time, we are left with 0 (no hit), 0b10000 (16), or 0b01000 (8). If we divide this
value by 8 (or shift it three positions to the right), we get 0, 1, or 2.
If we want to use the principle of scales again, the numbers 1 and 2 do not help. We must bring either
1 → -1 and 2 → +1 or 1 → +1 and 2 → -1, and the 0 must remain 0. Of course, we could insert a condi‑
tion statement, but we go to all the trouble to avoid an if statement. Let x be the number 0, 1, 2, then the
expression (x / 2) – (x & 1) transforms exactly to the desired target, -1 and +1. We can add the
expression and proceed in the same way as before.
Under normal circumstances, no one should program such solutions unless they are extremely per‑
formance critical and a profiler has shown that this variant is faster. Since we have here some mathemati‑
cal operations—where /8, /2 are also cheap —, this should not be faster in the end than a small if.
Condition statements are what people like to try to rewrite in micro-optimization, but if you don’t know
exactly what you’re doing here, it ends up being more expensive.
if ( lines.size() % 4 != 0 )
throw new IllegalArgumentException(
"Illegal size %d of list, must be divisible by 4".formatted(
lines.size() ) );
For the algorithm to work correctly, the list length must be a multiple of 4. Therefore, the first condition
statement checks if the length is divisible by 4; if not, there is an exception.
The following loop runs with an index blockStart in steps of four from back to front. The four
sums blockStart + 0, blockStart + 1, blockStart + 2 and blockStart + 3 represent the
5 • Data Structures and Algorithms 117
index to the four elements of a block. blockStart + 3 is to be kept, all other lines we delete via the
remove(..) method. You always have to be a little careful with this method because it is overloaded.
• The one variant remove(Object) deletes an equals(…)-equal element from the list.
• The second remove(int) method deletes an entry at the given position.
When deleting elements, we go from the higher index toward the lower index. For lists (especially
ArrayList), it is always reasonable to delete from the back, so that fewer elements have to be moved in
memory. If we start at blockStart + 0, the element below the index is deleted, and all other elements
move up. The solution would look like this:
lines.remove( blockStart );
lines.remove( blockStart );
lines.remove( blockStart );
The subList(…) method returns a sublist and changes are live; if the sublist is erased with clear(),
the elements in the underlying list are also removed. The solution approach can also be found in the fol‑
lowing task.
if ( numbers.size() < 2 )
return;
At first glance, the task is simple: we run the list and see if the next element is larger. If this is not the
case, we abort. Now, the peculiarity is the following: we do not remember the elements in a new list, but
we have to modify the list that was passed to us. That means, from a point where the elements become
smaller, we have to delete the passed list until the end.
However, such a method does not exist in the List interface. Therefore, we have to reprogram the
functionality manually. There are two possible solutions:
118 Java Programming Exercises
• There is a remove(int) method to which we can pass an index so that an element can be
deleted at that point. Sensibly, we start from right to left, calling the method until we have
shortened the list.
• The second possibility takes advantage of a peculiarity of data structures that there are live
views; the proposed solution also chooses this approach. The subList(…) method returns a
view of the list, but this one is live, and changes to this sublist are applied to the original, i.e.,
written through. If we clear this sublist with clear(), all elements in the original list disap‑
pear as well.
We want to represent the guests as objects with three properties. This is done by the record Guest,
which has an additional method hasDissimilarInterests(Guest); it compares itself with another
Guest and checks if there are no common interests at all.
In the allGuestsHaveSimilarInterests(…) method, a loop runs all guests. In doing so, we
get the guest and its right neighbor, where the last element in the list has no element behind it. With the
remainder operator, we come back out at the front of the list, which means the last element is compared to
the first element. This is correct because all the guests are in a circle, and they all have a neighbor.
In the loop, hasDissimilarInterests(…) determines if the two guests are without common
interests. In this case, we return the index from our method. If the loop passes through all guests, and they
all have one in common, the return is -1.
If we double the list from the task, we see that the second list occurs in it:
“Alexandre”, “Charles”, “Anne”, “Henry”, “Alexandre”, “Charles”, “Anne”, “Henry”.
For the first suggested solution, we copy all the names into a new list because we want to avoid destroy‑
ing the original list, and maybe the list is even immutable. With the method addAll(…), we append to
this copy the same elements from the source again. Thus, we have duplicated the first list. The method
Collections.indexOfSubList(List<?> source, List<?> target) implements the test, and
returns the position where the list target occurs in the list source. We do not get a truth value via the
method, but directly the position, and we only need to check if this position is greater than or equal to 0.
The method returns -1 if the second list is not present in the first list.
We must not work with containsAll(…) because the method does not check the order, but only
checks if all elements of a second collection are present in the first collection, completely independent of
the order, but the order is what matters.
The disadvantage of this solution is that we have to create a copy so that we can work with indexOf‑
SubList(…). With a little trick, we can do without a copy.
if ( list1.size() != list2.size() )
return false;
The method first checks whether the lists are the same size; this inquiry is also necessary for the first
solution. Since isSameCircle(…) can in principle work with all objects and not only with strings, the
method is declared as a static generic method. Only a valid implementation of equals(…) is required
for the comparison.
120 Java Programming Exercises
The virtual list is implemented by a subclass of AbstractList. This base class is often used for list
implementations to take over as much of the standard functionality as possible. We override two methods:
• The size() method returns twice as many elements as the original list.
• The method get(int) achieves by the remainder operator that if we go beyond the size with
the index, it starts again at the beginning of the data structure.
This solution does not create a new list in memory, but it is virtual and duplicated for outsiders. Of course,
the methods for modifying would not work at all, but size() and get(…) are enough to work with
indexOfSubList(…) again.
int localMaxOccurrences = 1;
int localStartIndex = 0;
To solve the problem, we need to consider two different sequences: a local longest sequence and a global
longest sequence. Therefore, we need several variables in which to keep track of states. Two variables
5 • Data Structures and Algorithms 121
store the local maximum number of equal elements and the start index of this sequence; two other vari‑
ables store the global maximum number of found elements and their position.
The program must answer whether an element occurs numerous times in a row. We initialize a vari‑
able recurringElement at the beginning with the first element and observe whether this element
repeats itself. The actual loop can start at index 1. The body of the loop reads the element and compares it
with recurringElement to see if there are sequences of recurringElement. The equivalence test
is done by the static equals(…) method of the Object class because this has the advantage over calling
equals(…) on the elements that null does not lead to a problem. If the element repeats, the localM‑
axOccurrences counter is incremented by 1, and a condition statement checks if the local number of
the same occurring elements exceeds the global maximum. If it does, then the if block updates the glo‑
balMaxOccurrences and globalStartIndex variables. We don’t need to remember the actual
element itself because that only becomes relevant at the end, and then we can query the element because
we know the position of the element in the data structure.
If a nonequal object appears in the list, the starting position is set to the index by the new sequence,
the number of repeated elements is set to 1 and recurringElement is reinitialized for the rest of the
sequence.
At the end of the loop, the parameterized constructor builds a WeatherOccurrence with the three
desired states.
if ( maybeIndex >=0 )
items.set( maybeIndex, items.get( maybeIndex ).incrementOccurrence() );
else
items.add( item );
}
return result.toString();
}
}
First, let’s take a look at the Item class. There are two parameterized constructors:
• The first constructor initializes all three pieces of information—the name, the price, and the
number of occurrences. In addition, the constructor checks the validity.
• The second constructor is a variant in which the number of occurrences is only one.
Since Item objects are immutable, the incrementOccurrence() method returns a new Item object
with an incremented count. The Item class overrides two methods from Object: the equals(…)
method and hashCode(). The compare method will become important later, as it allows us to search for
Item objects in the data structure. occurrence is not considered, we will see why shortly.
The receipt itself consists of a collection of Item objects. With the method addItem(Item) we
add a new Item to the receipt. We don’t want to simply append the item, but do a reduction if an item
with the same name and amount already exists, and then merge it. First, indexOf(…) searches for an
equals(…) like Item in the list, so occurrence had to be ignored. If indexOf(…) finds an item,
the result is >= 0, namely, the index of the found item. The following condition statement distinguishes:
5 • Data Structures and Algorithms 123
• If the element was found, at the position the element is replaced by a new element where
occurrence is increased by 1,
• If no equals(…)-equal Item was found in the list, it is appended at the end.
Finally, we come to the toString() method. It must go through all entries and output the product
name, the quantity, the price, and the quantity multiplied by the price. We can implement price out‑
puts in different ways; the solution chosen here uses the NumberFormat class to be able to sup‑
port the currency symbol for different languages eventually. The object built with NumberFormat.
getCurrencyInstance(Locale.GERMANY) then automatically places the Euro sign behind the
number. It is also the task of the toString() method to calculate the sum, which can then be output
at the end. When dividing by 100, it is important to make sure that it is not an integer division because
otherwise, we will miss the decimal places. The configured NumberFormat automatically sets two
decimal places and fills up with 0.
asList(…) implements the design pattern Adapter, which matches two incompatible APIs. In this case,
it adapts the array type with the square brackets for reading and writing and the length attribute to a
java.util.List. The operations on the list are live and are written through to the array. No list is cre‑
ated as a copy. Since the length of arrays cannot change after construction, elements cannot be deleted or
added. If you try, an UnsupportedOperationException is thrown. This can be seen quite well in
the original implementation:
OpenJDK’ implementation of java.util.Arrays
@Override
public int size() { return a.length; }
@Override
public E get( int index ) { return a[ index ]; }
…
}
The method asList(T... a) creates an instance of type ArrayList, where this does not
refer to a java.util.ArrayList, but to a nested class in Arrays. It is easy to see that the
read methods pass directly through to the array, but, for example, the add(…) method throws an
UnsupportedOperationException.
false
true
true
The parameter type is a vararg, which is an object array. In the first case, we pass an array of primitive
integers to asList(…), and primitive data types are not reference types addressed by a generic type vari‑
able. This means that the type T stands for the reference type int[]; consequently, the resulting list is of
type List<int[]>, and the contains(…) method returns false.
Varargs don’t actually exist within the JVM, they are just normal arrays. The key distinction is in the
calling, where a series of arguments can be directly passed and grouped into an internal array. This offers
two usage methods for vararg methods: passing numerous arguments, which are then assembled into an
internal array, or directly passing an array. This is precisely the variant we use here. The wrapper type
Integer is the type argument for the type variable T, creating a List<Integer>. The contains(…)
methods finds the 1, as it is converted to an Integer object through boxing, and the wrapper objects
implement equals(…).
In the third part, we use the variable argument list. First, the int elements are converted to Integer
objects via boxing, then placed in the anonymous internal array and passed.
The proposed solution gets the ListIterator from the list and runs over all elements as usual with
a combination of hasNext() and next(). After taking the element, matches(…) asks if the ele‑
ment matches any of the predefined strings. So for this check, we use a regular expression that contains
the different types of vegetables, even correctly with plural-s for selected types. If the string matches,
"cheese" is inserted.
You can consider an iterator to be a cursor that stands between the elements. After a next(), the cur‑
sor is behind the element. The add(…) method inserts a new element just before the cursor’s current posi‑
tion, or in other words, right after the element returned by next(). This means the cursor remains after
the newly inserted element, so calling next() again will not return the newly added item "cheese",
but the next item in the sequence.
The program looks innocent: the extended for loop iterates over the list, we remove an element in the list.
At first glance, it is strange why removing elements leads to a ConcurrentModificationException,
and what should be “concurrent” here? We do not work with threads!
“Concurrent” in our example is the iteration over the data structure and the “concurrent” deletion.
Java recognizes when resuming the iterator that there has been a change to the data structure. In other
words, the iterating and the removing are coupled by state. It works like this: an extended for loop does
nothing but internally uses an Iterator. If we rewrite the program a bit, as it is in bytecode, we get the
following:
System.out.println( names );
}
}
The implementation of List and the Iterator has a state in which they remember the number of
modifications. When the Iterator is created, it initializes once with the modification counter of the list,
and when the list makes modifications, it increments its modification counter. Later, when the Iterator
126 Java Programming Exercises
goes to the next element, it compares its stored modification counter with the modification counter of the
list, and if there is an inequality, there is the exception. In the code, it looks like this:
OpenJDK’s implementation of java.util.AbstractList
class AbstractList {
protected transient int modCount = 0;
public E next() {
checkForComodification();
…
}
The consequence is: we cannot make any change to the data structure inside the iteration loop. The expi‑
ration and deletion do not work this way, other solutions are needed for such tasks. A solution without a
loop could look like this:
names.removeIf( ""::equals );
removeIf(…) expects a predicate, and this very nice and compact method reference implements just
such a predicate, testing whether the incoming object is equals(…)-equal to the empty string.
The Iterator method nextIndex() returns the absolute position, we use it to compare whether
Bonny Brain is in the right or left half. Our own method searchRight(…) runs through the entire right
side using the Iterator and returns true if the searched item is found. If the method returns false,
we have not found the searched person in the right half, but we have to run back from there and go all the
way to the left. The search direction is indicated by the arrows in the output. If we reached the left side
and searchLeft(…) returns the result false, then the searched person did not exist in the list at all!
The second major else branch tests the other case, that Captain CiaoCiao is on the left side relative to
the center and first runs entirely to the left.
searchRight(Iterator<Ship>) and searchLeft(ListIterator<Ship>) are imple‑
mented like this:
com/tutego/exercise/util/FindCovidCough.java
The method to search to the right does not need a ListIterator because the normal Iterator
provides the two methods hasNext() and next() to traverse to the right and extract the next element.
The method searchLeft(…) requires a ListIterator because we have to walk to the left with the
methods hasPrevious() and previous(). A normal Iterator cannot run to the left or to the right
arbitrarily. Only with the ListIterator we have the possibility to run multiple times over the data
structure.
128 Java Programming Exercises
class MusicalChairs {
rotate( distance );
names.removeLast();
}
return names.getFirst();
}
The class MusicalChairs has an instance variable of type List, which contains the names. Although
a vararg array is passed in the constructor, we are more flexible with lists if we want to use the rotate(…)
method of the Collections class later. The constructor converts the array to a list and checks before‑
hand if the array contains any elements at all—otherwise, the constructor throws an exception. If the
constructor was called with null, there is the usual NullPointerException.
About the three methods:
• Our rotate(…) method makes use of the Collections rotate(…) method. This method
works in place, so it modifies the list. Our list is internal and not accessible from the outside.
5 • Data Structures and Algorithms 129
• rotateAndRemoveLast(…) first performs the rotation and then deletes the last list ele‑
ment. But there is a possible error to report: if the game is played with multiple rounds, calling
rotateAndRemoveLast(…) repeatedly may cause the list to become empty. This case is
checked by the first if statement and throws an exception if the list is empty. If there is more
than one element, we rotate the list and delete the last element from the list. An ArrayList
has no dedicated method to delete the last element. If instead of ArrayList we would take
a LinkedList, then there would be a method removeLast() from the interface Deque.
• The method play() is executing the game. Again, it must be true that the list must not be
empty. The while loop executes the body until the number of elements in the list becomes 1.
If there is more than one name in the list, the list is rotated and printed. After the loop passes,
the list contains exactly one element. We fall back to the first element. Again, a List does not
provide a special method to grab the first element. The situation is different for data structures
that implement the Queue interface: here there is the remove() method.
• The toString() method falls back on the static method String.join(…) because this is
the easiest way to create a comma-separated string with the elements.
The proposed solution proceeds in several phases. In the first phase, a new List is built and then shuffled
with the shuffle(..) method. Now we can iterate over these shuffled planets with the extended for
loop and ask a question for each of these planets that needs to be answered.
The next step is to select three random planets to the known planet. It is important to keep in mind
that the random selection does not include the queried planet again. Therefore, we build another list,
remove the answer of the question; it is very convenient that the enumeration elements implement the
130 Java Programming Exercises
equals(Object) method. Afterward we randomize this list, select three planets from this list, append
the answer to the list, and randomize this list again. This guarantees that the answer is not always in the
same position and that the three alternative answers are different.
What remains is the output on the screen and the query. If the planetary diameter is not correct, the
solution is displayed.
In the first step, we build a copy of one of the two sets. It does not matter which set we copy for its func‑
tionality; we take the smaller set, for a reason, and we will look at it shortly. The reason for the copy is that
the following method modifies the set, and we definitely don’t want to modify the incoming sets; possibly
the sets passed into the method are also immutable, meaning exceptions would occur.
After building the copy, we call the Set method boolean retainAll(Collection<?>). It
modifies the object on which the method is called, leaving only those elements in its own set that are also
present in the passed collection. This is effectively forming an intersection. Two things are interesting
about the signature:
1. The parameter is not a set, but an arbitrary Collection. That is, we could also pass a list. If
there are also elements in the list more than once, it wouldn’t matter.
2. Moreover, in retainAll(Collection<?>), the <?> is interesting that the type of the col‑
lection passed is irrelevant. Internally, the objects are compared using equals(…).
5 • Data Structures and Algorithms 131
The default implementation walks the own set with an Iterator and checks with contains(…)
whether the element occurs in the passed collection. If so, the element is removed via the Iterator. If
the passed Collection is a list, then the contains(…) method is on average more expensive than if
the query is on a TreeSet or HashSet.
Knowing this, we come back to the fact that we copied the smaller of the two sets. This has two
consequences:
1. First, copying small data structures is faster and more memory efficient than copying large
collections.
2. Second, the OpenJDK implementation of retainAll(…) shows us that the iterator runs over
its own set, meaning that if its own set is smaller, fewer elements are visited. One can counter
that it is always necessary to test against a larger (or equally large) set, but if fewer queries are
made overall, it is faster on average.
After forming the intersection, we set the size of the intersection with the number of hobbies the two
people have, in correlation. If the two people have a different number of hobbies, the percentage match is
not equal; it would be only if both partners specify an equal number of hobbies. The program output in
our example is:
The implication would be that all objects would then be the same, regardless of what their name state.
The assigned names are not included in the compare method at all.
When inserting elements into a Set, an element is only included if there is not already an equivalent
element in the set. Usually, the data structures access the equals(…) method. TreeSet, which is a
sorted set, is an exception because the implementation does not need the equals(…) method and can use
compareTo(…) to read whether two objects are equivalent. Whether therefore a separate equals(…)
method is implemented or throws an exception is not relevant because equals(…) is not called in the
scenario.
The main(…) method outputs three things: twice the results of the add(…) method, and then the
contents of the set via the toString() representation. The add(…) method of the set returns a bool‑
ean value whether an element has been added to the set. In the first case, a string is added, so the
return is true. On the second call to the add(…) method, the TreeSet recognizes that, according to
Comparable, the second string is equivalent to an already existing element of the set. Equal elements
are not overwritten and replaced, but the element is discarded. Since nothing is added, the add(…) method
returns false. In the toString() representation of the set, only the first inserted element appears.
return result;
}
return result;
}
Before we implement the wordList(…) method with dictionary access, we want to implement another
method: substrings(String). The method returns a collection of all possible substrings. In essence,
the method consists of two nested loops. The outer loop specifies the starting position, the inner loop gen‑
erates all lengths starting from at least three characters, up to the maximum string length. The minimum
size is determined by a constant, so it can be easily changed.
5 • Data Structures and Algorithms 133
The results are built up in an internal list. The size of the list can be calculated. The strange type
conversion has the reason that we blow up the value range when multiplying two large int values. The
subtraction with 3L forces a conversion to long, and later the explicit type conversion changes the long
back to an int.
Via the String method substring(…), a partial string is formed and added to the result set. As
data structure, we choose an ArrayList because it is very space-saving and will be run sequentially
from front to back later. In addition, we can determine the number of elements to be expected in advance,
so that at runtime the internal array does not have to be enlarged.
The implementation of wordList(…) is short with the prework. Again, an ArrayList is built as
a container and return, and in the loop, a word is placed in this container exactly when the word occurs
in the passed dictionary.
private E lookahead() {
while ( iterator.hasNext() ) {
E next = iterator.next();
if ( ! hasSeenSet.contains( next ) )
return next;
}
return null;
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public E next() {
134 Java Programming Exercises
E result = next;
hasSeenSet.add( result );
next = lookahead();
return result;
}
}
The constructor of the class takes the original Iterator and stores the reference in an internal instance
variable. In addition, there are two other instance variables: one for the set of elements already seen, and
the other referencing the next element. next will be null if the original iterator cannot supply any
more elements. The constructor has a second task, to reference the first element. The focus here is on the
lookahead() method.
The method lookahead() reaches the original Iterator and polls until there is an element that
was not yet in the set of already seen elements. If the Iterator cannot return any more elements, the
new UniqueIterator cannot return any elements either, and the method returns null. If the internal
Iterator finds an element that does not yet occur in the set, lookahead() returns this element.
Let us summarize: when the constructor is called, the first element is queried immediately via the
internal iterator and stored in the variable next. If next is equal to null, then there was no element
in the underlying Iterator.
For an Iterator, two methods have to be overridden:
1. The implementation of hasNext() is accordingly simple: if next is not equal to null, then
there is an element. The update is done by next().
2. With the method next(), we first note the assignment of next in an intermediate variable
result. If the method next() is called from outside, the result must be included in the set
of elements already seen so that it is considered known for the next time. The instance variable
next is updated with the lookahead() method for the next call to the Iterator methods.
There may be a next element—in which case next will be nonzero—or there may be no ele‑
ment and next will be null. After updating the variable next, result will contain the
previous value returned by the next() method.
So much for the algorithm. Iterators, like many other data types, are generic types. We also use this
possibility. The class UniqueIterator has a type parameter E, which is inherited from the interface
Iterator. This can be seen in the next() method, which returns something of type E. This type of vari‑
able becomes interesting in the constructor, which assumes an Iterator<? extends E>, thus not only
expecting an Iterator<E>, but allowing more possibilities. <? extends E> expresses that the origi‑
nal iterator may contain subtypes of E. In other words: for example, if we declare a UniqueIterator
with a type argument Object, then the internal underlying Iterator can return String, for example,
because String extends Object.
if ( array.length == 0 )
return Collections.emptyMap();
if ( array.length == 1 )
5 • Data Structures and Algorithms 135
return result;
}
Multidimensional arrays in Java are nothing more than arrays that reference other arrays. In a two-dimen‑
sional array, we have one main array, descriptive for the column, which references many small arrays for
the rows.
Two peculiarities show up in the program code:
1. The optimization for empty arrays and arrays with only one key-value pair.
2. The check for null.
Arrays are objects in Java, so references come into play, which can be null. The first query on the length
of the main array will throw a NullPointerException if null was passed to the method. If the row
arrays do not exist and are null, access via array[index] will also throw a NullPointerException.
Later calls to Objects.requireNonNull(…) will test at the elementary level to make sure they are
not null; otherwise, the method will throw an exception.
Associative stores have a much larger memory footprint than, say, an array. For two special cases, we
can reduce the memory requirement significantly, namely, when a Map contains no elements and when a
Map contains only one key-value pair. The Collections class provides two special methods for build‑
ing empty associative stores and those with only one pair: the emptyMap() and singletonMap(…)
methods.
Only when there is more than one element, a HashMap is built; the capacity is pre-initialized with
the number of rows of the array, but at least with 16. But what is the capacity? The capacity is a kind of
buffer. A HashMap has a DEFAULT _LOAD_FACTOR that defaults to 75%; if new elements are added and
the associative memory has reached 75% capacity, a so-called rehashing is performed. The HashMap
is enlarged, all elements are reclassified. Of course, one wants to reduce this cost. If we give the initial
capacity for 16 elements, the HashMap can directly hold 12 elements without rehashing. Knowing this,
we could also work with array.length / 0.75 + 1 and thus calculate the optimal size. But suppose
we have a giant array, then multiplying by 1.3 leads to an overflow, the number becomes negative, and
then there is an exception in the HashMap constructor. These are already very extreme special cases, but
if we want to be outstanding software developers, we have to pay attention to such things. Starting from
Java 19, we can use the HashMap.newHashMap(int) method to create a new, empty HashMap with
a specified initial size. This map uses the default load factor of 0.75 and its initial capacity is generally
sufficient to accommodate the expected number of mappings without resizing.
We can use a HashMap because the task says that equals(…) and hashCode() are implemented.
We would have a problem if the two methods were not implemented correctly. Then the probability would
be high that all entries of the array would be included in the associative memory anyway. If equals(…)
and hashCode() were not overridden from the superclass Object, each nonidentical object would also
not be equivalent to another object, and the hash code would most likely always be different so that the
objects would have no connection to each other.
So, if the array contains more than one element, a HashMap is built, the array is expired, and the
key-value pairs are added to the associative store.
There is a subtle difference in the return: for no or one element, the return is an immutable associative
store. For more than two entries, we get back a mutable data structure.
136 Java Programming Exercises
Each insert operation writes a pair into both associative memories. We would rather not accept null val‑
ues, but they do not occur in our program anyway. If a query returns no value, the response is not null
as in the Map method get(…), but Optional.empty(). Sorting the keys is not required, so we don’t
need a TreeMap, a HashMap is fine.
Let’s get to the main program. The first step is to fill the BidiMap:
com/tutego/exercise/util/MorseDemo.java
class Morse {
private final BidiMap<Character, String> charToMorse = new BidiMap<>();
Morse() {
String encoded = "a.- b-... c-.- d-.. e. f..-. g--. h.... i.. j.--- k-. " +
"l.-.. m-- n-. o--- p.--. q--.- r.-. s... t- u..- v...- " +
"w.-- x-..- y-.-- z--.. 1.---- 2..--- 3...-- 4....- " +
"5..... 6-.... 7--... 8---.. 9----. 0-----";
for ( String token : encoded.split( " " ) )
charToMorse.put( token.charAt( 0 ), token.substring( 1 ) );
}
However, this approach requires a lot of code. The proposed solution uses another way, a coding that a
string consists of space-separated pairs, where in the pair the first symbol is for the key and the sequence
after it are the Morse characters.
Let us come to encode(…) and decode(…):
com/tutego/exercise/util/MorseDemo.java
The encode(String) method loops the string and converts it to Morse code. The result is built dynam‑
ically, which is actually a typical task of StringBuilder, but here a StringJoiner is used. This
class is useful because, on the one hand, it is a dynamic data structure for strings, and on the other hand
because it produces a result in which the individual substrings can be separated by a user-defined separa‑
tor, in our case a space.
A simple for loop gets to each character. If the character is a space, we put a space string in the
StringJoiner, resulting in two spaces in the output. Why? Because " " + "" + " " just gives " ". If
no space is seen, we convert the character to a lowercase letter and then query the BidiMap. The get‑
FromKey(…) method returns Optional.empty() if there is no corresponding Morse code for the
character. In that case, there is nothing to do. Otherwise, we pass the Morse code to the StringJoiner.
The method decode(String) goes the opposite way. We get a long string of Morse code sequences
that must be converted back to the original text. The result of the method is a string that is built dynami‑
cally using StringBuilder. We initialize it with a capacity and estimate how large the result will be;
we estimate that it will be a quarter of the original size of the input string.
The Morse words are separated by two spaces, so the first thing we want to do is query the words.
split(TWO_SPACES) will give us all the words. The split(…) method directly returns an array, which
is fine for small results; for indeterminately large results it makes sense to work via an iterator, for exam‑
ple, via
Alternatively, tokens can be run directly using the hasNext() and next() methods, something like
this:
The letters and digits converted to Morse code are separated by spaces. Again we resort to split(…),
though a Scanner or StringTokenizer would also help. With the substring extracted, getFrom‑
Value(…) queries the BidiMap. The sequence of symbols might not exist and getFromValue(…) will
then return Optional.empty(). With Optional we can express very well this cascade:
Finally, we convert the StringBuilder into a String and return the result.
wordOccurrenceList.sort( wordOccurrenceComparator );
return result;
}
5 • Data Structures and Algorithms 139
The proposed solution consists of three steps: first, we want to count the frequency of all words in the text.
In the second step, we sort by frequency. In the third step, we take the first five elements of the sorted data
structure and prepare them for return.
Step 1: To make the solution more modular, wordOccurrences(String... words) helps us
determine the occurences of strings. To count occurences, we use a Map that associates a String with
an Integer, the occurency. A loop iterates over all words and adds them to the Map. To count up, we can
use the convenient merge(…) method:
With it, we can either set an initial value (in our case, 1) for new keys, or call a BiFunction for existing
keys that combines the old value with the merge value (also 1) and then writes it back. The combination
is addition in our case.
Let’s move on to the importantGossip(…) method.
Step 2: After calling wordOccurrences(…), the wordOccurrences variable stores each word
and its occurence as a Map<String, Integer>. The Map is not sorted, and even if we were to use
TreeMap, we could only sort by keys, not by values. We need to copy the data into a different data struc‑
ture that allows sorting by occurences. Here, two options come into consideration: a List that can be
sorted later with sort(Comparator), or a TreeSet(Comparator) that always keeps the elements
sorted. The elements are tuples of the string and occurence. The Map method entrySet() is useful,
which returns a Set of the key-value pairs of type Map.Entry. We can copy these Map.Entry objects
into a new data structure and then sort them by occurences, which we get with getValue() from the
Map.Entry.
To sort, we want to use a List that is more lightweight than a TreeSet. Therefore, a new change‑
able ArrayList is filled with the Map.Entry objects from wordOccurrences(…).
The sorting is determined as usual by a Comparator; the variable wordOccurrenceCompara‑
tor names the Comparator. This must take the occurence as the first criterion from the Map.Entry
object, and if the occurence is equal, the lexicographic order of the words must be added as a comparison.
The Comparator can be well built using the static and default methods. We have two options to start:
Since Java generics cannot infer types in this case, it must be Entry.<String,Integer>comparin
gByValue().
Since words with a higher occurence must come first and less frequent words last, we need to reverse
the default order, which is done by reversed(). If an occurence occurs twice, we use thenCompar‑
ing(…) to push the second Comparator, which uses the order of the words as the second criterion. This
allows the list to be sorted.
Step 3: In the final step, the program must extract the first five elements from the sorted list. There are
various ways to achieve this, and two approaches to fill the List<String> result with the results are:
1. An enhanced for loop that iterates through the sorted List<Entry<String, Integer>>,
extracts the key (the word) from the Map.Entry object, and adds it to the target container
result. The loop is terminated if result is already five elements long. If there are fewer
than five words in the word list, the loop is terminated earlier, and not by the conditional
statement.
140 Java Programming Exercises
if ( hexRgb.length() == 4 )
hexRgb = "#" + hexRgb.charAt( 1 ) + hexRgb.charAt( 1 )
+ hexRgb.charAt( 2 ) + hexRgb.charAt( 2 )
+ hexRgb.charAt( 3 ) + hexRgb.charAt( 3 );
return Integer.decode( hexRgb );
}
In the suggested solution, the Color class is included as a nested static class in ColorNames. This
dependency makes sense because Color objects are only relevant in the context of ColorNames. The
Color object has the desired instance variables for the name and RGB value and provides a constructor
that takes the name and RGB value. The instance variables are private, and so is the constructor—the
outer class can still use the private constructor, a feature of Java visibility. The constructor takes the RGB
value as a string, which means it must be decoded. For this, the Color class declares its own method. If
the constructor was not private, we could have used a record instead of a class.
int decodeHexRgb(String) first checks the validity of the string. If it does not start with a #,
the specification is false. Furthermore, if the string is not either four or seven characters long, that is an
error, and an IllegalArgumentException follows. While the RGB values in the file are correct,
this public method is for everyone to use, and incorrect strings should be noticed. If the string contains
only four symbols, then it is the shorthand notation, and the red, green, and blue values are doubled. For
the doubling, we resort to a simple concatenation that is easy to read. There are other possibilities, for
example, with an argument index in the formatting string; one solution would be
"#%1$s%1$s%2$s%2$s%3$s%3$s".formatted(
hexRgb.charAt( 1 ), hexRgb.charAt( 2 ), hexRgb.charAt( 3 ) );
"#%1$s%<s%2$s%<s%3$s%<s".formatted( ... );
It should be obvious that simple concatenation is the best readable. Integer.decode(…) then returns
the corresponding RGB value as an integer.
Also, ColorNames has a constructor, and this one takes the file name. Files.readAllLines(…)
reads all lines of the file, and the extended for loop traverses them line by line. Each line is tokenized as
a string with the split(…) method, and we access the second and third elements in the array—that is,
index 1 and 2—and fill the constructor with them. We put the resulting Color object into the Map, where
the RGB value as an integer is the key for the Color object associated with it.
The remaining method decode(String) queries the Map, and this may return null if there is no
association between the RGB value and color. Since we want to avoid null as a return value, Optional.
ofNullable(…) makes unknown RGB values become Optional.empty().
namesByLength.forEach(
(len, names) -> System.out.println( len + " " + names )
);
142 Java Programming Exercises
The type SortedMap<Integer, List<String>> represents the association of an integer with a list.
Before the strings can be added to the list, the list must be created. A program must proceed as follows:
1. It must check if there is already a list for the length, if so, the string can be appended.
2. If there is no entry in the Map for the length yet, a list must be associated with the length and
the first word with the length must be added to this list.
if ( ! namesByLength.containsKey( line.length() ) )
namesByLength.put( line.length(), new ArrayList<>() );
It checks using containsKey(…) whether a list already exists for the string length, and if not, it creates
a new ArrayList for that string length. The subsequent statement always succeeds in adding it with
put(…), as there is assured to be a list for this string length.
Since Java 8 we don’t have to write this out manually but can use the default method computeI‑
fAbsent(…). The description is a bit complicated, easier is to understand the method at the source code:
OpenJDK’s implementation of java.util.Map
return v;
}
First, the get(…) method is called, and there are two outputs:
1. If the value is not null, it is returned directly. For our case, this means that a list for the string
length has already been built.
2. get(…) returns null for the case that there was no association. (We ignore the case where
there might be null associated with a key). If there is no associated value, the passed function
is called with the key. The function returns a value, and under the key the value is stored, at
least if it is not null. In our case, the function creates a new ArrayList, and the name of the
5 • Data Structures and Algorithms 143
key is unnecessary for this. Therefore, the program hides this identifier with two underscores
__. Question for readers: Instead of __ -> new ArrayList<>(), what is the argument
against the constructor reference ArrayList::new, which would also work?.4
computeIfAbsent(…) returns the list at the end in any case, and we can cascade add(…).
After the program has read the file line by line and filled the data structure, the Map method
forEach(BiConsumer) traverses all entries. Our BiConsumer gets the key and value and outputs
the pairs to the screen.
The last part of the program is in a loop that terminates only if the input is 0 or negative. The pro‑
gram asks for an integer for the word length. Then get(…) asks for the list, but the result could be null
if there is no list for the input. Optional.ofNullable(…) can wrap a possible null well into an
Optional because if there is no associated value to the key, Optional.isEmpty() is true. ifPre‑
sentOrElse(…) is like a control statement: if the Optional contains a value, our Consumer prints
the list of names; if there is no associated value, a notice follows on the screen that no word of the desired
length exists. The trick with the assignment int finalLen = len is necessary because lambda expres‑
sions can only access final variables, but the counter len changes. The copy into an intermediate variable
solves the problem.
The task can be solved with a wide variety of data structures and approaches. The version chosen here
does the following: it builds a Map<String, List<String>> in which the position of a letter is
associated with a list of words. Let us take house and mouse as an example. Both words have an o at
index 1. This association between index and letter is the key to associative memory. The program shall
transform the input house, mouse, horn, and cannon into the following map:
To build the Map, an outer loop traverses all the words and then an inner loop over each letter of the
selected word. The pair from the index, a minus sign, and the position are put into the associative mem‑
ory. Since the associative memory does not directly connect this key with the word, but the strings
come into a list, the list must be rebuilt whenever the first word is to be put into the list. For such tasks,
144 Java Programming Exercises
List<String> missingLettersWords =
Arrays.asList( "_ouse", "ho__", "ca__on", "gun", "__e__", "_____" );
missingLettersWords.forEach( letterFinder );
The Consumer is a kind of a subroutine that takes a word with underscores, queries the Map data
structure, and prints the matching words on the screen. The matching words are stored in a set called
matches. The algorithm works by first searching for all possible words that have the same character at
the same position as the given word, adding them to the set, and then forming intersections with the added
characters at the following positions. Initially, there may be numerous candidates with an h at the first
position, for example, but the set becomes smaller and smaller as additional characters are added, such as
an o at the second position.
A large main loop can be identified in the program. It runs the initial word from front to back, and all
characters at the position 0, at the position 1, and so on are retrieved. Unknown characters are not helpful
in recognition (but we can accept all words), so the program returns to the loop. If the character is not an
underscore, we generate a key from the index, minus sign, and the character. With this key, we query the
Map, and in the best case, we get a list of candidates. However, there may be no list for this key, that is,
there is a character in the source word, but no match; there is no solution, the set of matches becomes
null, and we can abort the loop.
5 • Data Structures and Algorithms 145
Once we have found a list of matching words for the character, the program first creates a copy of the
list and in the second step removes all words that do not have the same length as the source word. Now we
have to create an intersection between the previous words and the new words under the index because
the result must appear in both sets. If no words have been found before, matches is equal to null, and
we build a new set with the current candidates. Only on the first hit, no intersection is built; if there were
already results and matches was not null, retainAll(…) deletes from matches all words that do
not also occur in wordCandidates.
At the end of the loop, we have iterated over all known characters and kept reducing the set of
matches. A console output shows us the contents of the set or a message if the set is empty.
• Strong references are the usual references we deal with as Java programmers in everyday life.
The garbage collector would never clear these references.
• A WeakReference is a reference that is released when a garbage collector phase is running.
It depends on the implementation of the JVM exactly when this is.
• With a SoftReference the JVM tries to keep the object alive until it is close to an
OutOfMemoryError.
• With a PhantomReference we only get to know that the garbage collector has removed the
object.
The WeakHashMap uses a WeakReference internally, so we have nothing to do with handling these
references directly and cleaning up the bins. It is a usable data structure that keeps references as long as
there is enough free memory, and then releases the objects when the garbage collector needs to make
space. For our computation, this is a good choice because the WeakHashMap is filled when it is needed
for the factorial computation—but the factorials do not need to be in memory for an unnecessarily long
time and are cleared away again.
The WeakHashMap is a special Map, which means we can use all known methods.
K, V
«interface»
Map
K, V
WeakHashMapMap
com/tutego/exercise/util/CachedCatalan.java
if ( maybeCachedValue != null )
return maybeCachedValue;
// n < 2 ? 1 : n * factorial( n - 1 )
BigInteger result = isLessThan( n, TWO )
? ONE
: n.multiply( factorial( n.subtract( ONE ) ) );
factorialCache.put( n, result );
return result;
}
factorial(…) is the method that needs to access the cache. So, we first create an instance variable
factorialCache for the internal cache that can hold BigInteger. The association is a mapping from
n to n!; both types are BigInteger.
There are two main steps in using a cache:
1. When asked for a value, we first ask the cache if the value is contained. If it is in the cache, we
are quickly done.
2. If the value is not in the cache, it is computed and cached. This takes time.
The method factorial(…) is implemented in the same way. The cache is queried, and maybe there is
a result, perhaps not. We already know the response behavior of get(…) method of Map, which returns
null if there is no associated value. This is the indicator for us whether we have a value in the cache or
not. If the result is not equal to null, we have the BigInteger as a computed factorial in the cache and
can return it directly. But if get(…) returns null, we have to compute the factorial and then put the result
in the factorialCache and return it. For comparing whether one BigInteger is larger or smaller
than another, there is no special method in BigInteger because BigInteger has a natural ordering,
so it implements Comparable. However, the readability with compareTo(…) is not optimal; therefore,
a separate method isLessThan(…) was introduced. The BigInteger constants ONE and TWO were
imported statically, which shortens the code a bit.
An alternative implementation could use computeIfAbsent(…), but the code presented here is
well understood and comprehensible.
The catalan(…) method performs the documented calculations and accesses factorial(…)
three times. Many entries are already in the cache, so factorial(…) can serve many responses from
the cache.
System.out.println( dates.firstEntry() );
System.out.println( dates.lastEntry() );
festiveSeason.clear();
System.out.println( dates );
The proposed solution uses the described methods firstEntry(), lastEntry(), higherEntry(K)
and subMap(K, boolean, K, boolean).
Different methods from the NavigableMap return so-called views. A view is not a copy of the data,
but operations on this view always go to the underlying data structure. Thus, we can realize the deletion
of the subarea via the clear() method on the view. Views are memory efficient and performant, but can
lead to errors because you may accidentally modify the underlying data structure, but thought you were
working on a copy. And it can lead to a problem if the underlying data structure is immutable, i.e., cannot
be modified at all, but elements are inserted into the view, for example.
{java.awt.Point[x=2,y=1]=java.awt.Point[x=1,y=2]}
null
java.awt.Point[x=1,y=2]
The inserted key objects must be immutable, otherwise the dynamically computed hash code will change,
and the objects will not be found.
148 Java Programming Exercises
System.out.println( giftsToCounter );
We leave the counting and printing of the corresponding gifts to a method printMultipleGifts(…).
This method receives a list of quantities, representing the gifts each family brings. Counting the same
things is a common task, but it cannot be done with a built-in data structure in Java. Therefore, we con‑
struct a small local class Bag that extends HashMap. This class Bag associates a string, the present, with
the integer for the frequency. We give this small class a method so that we can externally increment the
frequency for a key; internally, the associated value is incremented by 1. Here we resort to the merge(…)
method provided by Map. If there is no value associated to a key yet, 1 is set, and if there was at least one
value, 1 is added to the old value and the record is updated.
In our method, we then build an instance of Bag, iterate over all the sets of families with the gifts,
then run all the gifts themselves and call the add(…) method for each gift.
If we want to know which gift occurred more than once, we can use the forEach(…) method of
Map to iterate over all key-value pairs and ask if the counter surpassed 1, and in that case, output the gift.
Our PropertiesConfiguration class has a constructor that accepts and stores the actual
Properties. Our methods are more flexible than the methods provided by Properties, but our meth‑
ods are internally based on the methods of the Properties class. That is, all the methods we provide rely
in some way on the methods of the Properties class. In the center is the getProperty(String)
method and once to set properties, we also use setProperty(…).
The actual algorithm can be implemented in different ways. One approach would be to first collect all
numbers on one stack and all operators on a second stack. Finally, the stack with the operators is pro‑
cessed together with the stack of values. We use a different approach with only one stack.
Java provides the data structure java.util.Stack, but this data structure belongs to the discarded
types of Java 1.0, which should not be used anymore. Therefore, we use a LIFO queue, where add(…)
appends something behind (Last In) and remove(…) takes something away from the queue (First Out).
If the token is the string representation of an integer, then we convert the string to an integer and put
it on the stack. Furthermore, we use a regular expression to check if the token is a binary operator. With
the order of the operators in the regular expression [+*/‑], we have to take care that the character ‑ is
not between the symbols; otherwise, the minus stands for a range specification like a‑z, which we don’t
want, of course.
If the token is an operator, we have to get two operands from the stack. It is important to take care to
fetch the second operand from the stack first and then the first operand. Addition and multiplication are
commutative, but subtraction and division are not. The switch expression with the modern arrow nota‑
tion is performing the correct operation based on the token and puts the result back on the stack. Since the
switch expression must be exhaustive, unfortunately, a default branch remains. This is unnecessary
from the program logic because we checked the symbols before, but the compiler forces us to do so. An
alternative notation would be a switch statement, then no default would be necessary. However, this
solution also has an advantage: changes to the regex are noticeable if case blocks are not also adjusted.
Since a binary operation always consists of three symbols, only one number remains after the resolu‑
tion. If we have processed all tokens from the input and the number of numbers and operators was bal‑
anced, there is only one number left at the end, the result. Readers may consider what errors could occur
due to incorrect input values.
Forget No Ship
com/tutego/exercise/util/CompletedCompetition.java
The data type BitSet is a kind of special data structure that can be used to associate a small integer value
with a truth value. This is useful for the solution because we can map each ID of a ship to a truth value
(bit set or not set) that tells us whether the ship was seen or not.
The first step is to build the BitSet itself. Since it is a good practice to specify the number of ele‑
ments suspected when initializing, we do that. It is not too bad if this quantity is not precise, all dynamic
data structures can adjust their internal capacity afterward.
The following extended for loop iterates over all IDs, tests the value ranges, and checks if the ID has
already been seen. If it has, this is an error that is reported. If the ID has not been seen yet, a bit is set at
the corresponding position in the BitSet.
Finally, a regular for loop runs over all IDs, and if the bit was not set, we know which ship ID was
not seen.
The bits are shifted by the starting position 10. Of course, this is a tiny memory optimization—we
could have left the first 10 bits unused.
return result.toString();
}
A task like this can be easily solved as follows: we take the first character of the first string and check if
this character appears in the second string. If so, we note the character. Then we take the next character
from the first string and test again if the character appears in the second string. However, the runtime
would be quadratic because we would have to iterate over the second string from the beginning to the end
repeatedly. If we want to avoid this, we need to approach the task differently.
The solution: we keep track of every occurred character in the first string. We could use a set, i.e.,
a Set<Character>, but an alternative solution is presented here. The BitSet is a special associative
storage that associates an integer with a boolean. The integers should not be too large, as otherwise the
BitSet will require more memory, but since our Unicode characters cannot be arbitrarily large, the
5 • Data Structures and Algorithms 153
memory requirement will also be manageable. We can easily calculate it. Unicode 15 has around 150,000
characters, so we need that many bits—150,000 bits / 8 = 18,750 bytes, which is less than 20 KiB, about
as much as a tiny image.
The statement string1.codePoints().forEach(…) extracts each individual character of the
first string. Each character is represented by a code point. The code point becomes an index, and at that
position, we set a bit in the BitSet for marking. codePoints() returns an IntStream, a special data
type that we will encounter more intensively in the next chapter.
For the second string, we proceed similarly. Here, too, we retrieve an IntStream of characters,
but we do not set a bit. Instead, we ask if there is a bit set in the BitSet at the position of character.
If so, the same character appears in the first and second strings. We want to remember the character in
a StringBuilder. The StringBuilder size is estimated as the arithmetic mean of the lengths of
string1 and string2, and we must do the addition in the value range of the data type long to avoid
risking overflow when adding two int numbers.
After the iteration, we convert the StringBuilder into a String and return it. The runtime is
linear and dependent on the size of the two strings, as we only have to iterate over the first string once and
then the second string once.
Loading Ship
The proposed solution consists of three parts: the declaration of the two records Loader and Unloader
and the starting of the threads.
The data exchange is done by the java.util.concurrent.BlockingQueue implementation
ArrayBlockingQueue. For our application, blocking is desired, so we use put(…) and take().
com/tutego/exercise/thread/LoadingShips.java
The Loader is a Runnable and has the desired constructor which takes a BlockingQueue, the data
structure with which later the Loader and the Unloader exchange data. The run()‑ method contains
a loop, which is always terminated, if an interrupt is sent from outside. This is not given in our case, but
it corresponds to best practice.
In the body of the loop, a random product is selected and a product name with a random identifier is
generated. This product is put on the ramp. The put(…) method blocks, because the ramp might already
be full. If the thread comes back from put(…), a product could be successfully put on the ramp. Finally,
the thread delays execution a few milliseconds and continues repeating the loop.
154 Java Programming Exercises
com/tutego/exercise/thread/LoadingShips.java
The record Unloader is similar in structure. The difference is only in the body of the while loop,
where a product must be taken from the ramp. We use the take() method. It is possible that the take()
method blocks because there is no product on the ramp. If the take() method returns, a product could be
taken off the ramp and a screen output is made and the program flow is slightly delayed.
com/tutego/exercise/thread/LoadingShips.java
In the last part, the ArrayBlockingQueue is built, passed to the two instances of Loader and
Unloader, and then the threads are started.
The keywordComparator is at the heart of the solution. It takes the two messages from the mes‑
sage and checks for the presence of the term of affection. To make the code a bit clearer, the result of
contains(…) is stored in two variables. Now there are four cases: both messages contain the term of
affection, both messages do not contain it, or one of the two messages contains the searched word. If both
contain the term or not, the Comparator returns 0, otherwise only one of the two messages contains the
string. If the term of affection is contained in the first message, this message is smaller according to this
Comparator, and the Comparator answers with a negative value. Otherwise, the term of affection
must be present in the second message, and the Comparator responds with a positive value.
5 • Data Structures and Algorithms 155
public SecureRandomBigIntegerIterator() {
Runnable bigIntegerPutter = () ‑> {
try {
while ( true ) {
BigInteger bigInteger = internalNext();
System.out.printf( "> About to put number %s... into the queue%n",
bigInteger.toString().subSequence( 0, 20 ) );
channel.put( bigInteger );
System.out.println( "> Number was taken" );
}
}
catch ( InterruptedException e ) { throw new IllegalStateException(e); }
};
ForkJoinPool.commonPool().submit( bigIntegerPutter );
}
At the heart of the solution is the SynchronousQueue class. It differs significantly from a normal
Queue and is essentially only used to pass an element from one thread to another. This is precisely the
context in which we use SynchronousQueue.
156 Java Programming Exercises
NOTES
1 The Magnetic declination, aka Compass Course (CC), is the angle between a ship’s path and compass north.
2 Linux users usually have dc installed and can play a bit with the RPN, a brief introduction is provided by
https://en.wikipedia.org/wiki/Dc_(computer_program).
3 Interestingly, the List implementation Vector has a useful method setSize(int) that ArrayList does
not have.
4 The constructor reference is for ArrayList(int), i.e., the parameterized constructor, so an ArrayList is
created with the capacity from the word length—things are not related at all.
Java Stream-API
6
The Stream API enables the step-by-step processing of data. After a source emits data, different steps
follow that filter and transform data and reduce it to a result.
Although the term Streams can be ambiguous and may be mistaken for input/output streams, it is
an essential feature of Java 8 that leverages other innovations in the Java SE library, including predefined
functional interfaces and Optional. By combining Streams with lambda expressions and method ref‑
erences, developers can write concise code and configure processing steps declaratively in a novel way.
The first task in this assignment block makes use of the heroes we already met in the chapter about
the class library. All the major terminal and intermediate operations are used for this collection of heroes.
Different tasks follow, the solution of which shows the elegance of the Stream API.
Prerequisites
• java.util.stream.Stream
• java.util.stream.IntStream
• java.util.stream.Collectors
• java.util.IntSummaryStatistics
• java.util.DoubleSummaryStatistics
• java.util.regex.Pattern
Stream construction:
• For the following task items, always build a new Stream with the heroes, and then apply the
terminal and intermediate operations according to the following pattern:
Heroes.ALL.stream().intermediate1(…).intermediate2(…).terminal()
Terminal operations:
• Given is an array of strings with names. Which name was mentioned how often? The names
are not case-sensitive.
6 • Java Stream-API 159
• Many people just call Captain CiaoCiao simply CiaoCiao, this should be equivalent to
Captain CiaoCiao.
Example:
Frame Pictures ⭑
Captain CiaoCiao has been chosen as the best captain, so the joy is great. He would like to have his picture
framed.
Given is a multiline string, such as
______
_.-’:::::::`.
\::::::::::::`.-._
\:::’’ `::::`-.`.
\ `:::::`.\
\ `-::::`:
\______ `:::`.
.|_.-’__`._ `:::\
,’`|:::| )/`. \:::
/. -.`--’ : /.\ ::|
`-,-’ _,’/| \|\\ |:|
,’`::. |/>`;’\ |:|
(_\ \:.:.:`((_));`. ;:|
\.:\ ::_:_:_`-’,’ `-:|
`:\\| SSt:
)`__...---’
+------------------------------+
| |
| ______ |
| _.-’:::::::`. |
| \::::::::::::`.-._ |
| \:::’’ `::::`-.`. |
| \ `:::::`.\ |
| \ `-::::`: |
| \______ `:::`. |
| .|_.-’__`._ `:::\ |
| ,’`|:::| )/`. \::: |
| /. -.`--’ : /.\ ::| |
| `-,-’ _,’/| \|\\ |:| |
| ,’`::. |/>`;’\ |:| |
| (_\ \:.:.:`((_));`. ;:| |
| \.:\ ::_:_:_`-’,’ `-:| |
| `:\\| SSt: |
| )`__...---’ |
| |
+------------------------------+
160 Java Programming Exercises
Task:
• Write a frame(String) method that frames a multiline string. Use the String methods
lines() and repeat(…).
• The horizontal lines consist of -.
• The vertical lines consist of |.
• In the corners, there are plus signs +.
• The spacing to the right and left of the frame is two spaces.
• The inner space at the top and bottom is a blank line.
• Line breaks are \n, but they should be relatively easy to change in the program.
He sees the 1 and says to himself, “Oh, 1 times the 1!” He writes that down:
1 1
Now he sees two ones and can read it out like this:
2 1
1 2 1 1
1 1 1 2 2 1
3 1 2 2 1 1
Captain CiaoCiao finds that the numbers get big quickly, though. He is curious to see if after a few passes
only 1, 2, and 3 occur as digits.
6 • Java Stream-API 161
Task:
The task can be solved with a clever regular expression with a back-reference. However, this solution
variant is sophisticated, and those who want to take this route can find more details at https://www.
regular-expressions.info/backref.html.
What is asked here is the look-and-say sequence, which https://oeis.org/A005150 explains in more detail
with many references.
Balancar
Erbium
Benecia
Yttrium
Luria
Thulium
Kelva
Neodymium
Mudd
Europium
Tamaal
Erbium
Varala
Gadolinium
Luria
Thulium
162 Java Programming Exercises
One line contains the island, the next line contains the rare earth metals. However, different crew mem‑
bers may enter the same pairs in the text file. In the example, it is the pair Luria and Thulium.
Task:
• Write a program that deletes all duplicate line pairs from the text.
• The program must be flexible enough that the input can come from a String, File,
InputStream or Path.
• The lines are always separated only with a \n. Moreover, the last line ends with a \n.
For the solution, the types Pattern, Scanner, and MatchResult as well as the Scanner method
findAll(..) and further Stream methods are helpful.
Task:
The distance in kilometers is calculated using the Haversine formula like this:
String[] cars = {
"Gurkha RPV", "Mercedes-Benz G 63 AMG", "BMW 750", "Toyota Land Cruiser",
"Mercedes-Benz G 63 AMG", "Volkswagen T5", "BMW 750", "Gurkha RPV", "Dartz
Prombron",
"Marauder", "Gurkha RPV" };
Task:
• Write a program that processes an array of model names and produces a Map<String,
Long> at the end that associates the model names with the number of occurrences. This part
of the task can be solved well with the Stream API.
• There should be no models named only once; only models named twice or more should appear
in the data structure. For this part of the task, we can better do without the Stream API and use
another variant.
Modify the query so that all models are in a map, but the names are associated with false if there are
fewer than two mentions. An output might look like this:
PRIMITIVE STREAMS
In addition to streams for objects, the Java standard library provides three special streams for primitive
data types: IntStream, LongStream, and DoubleStream. Many methods are similar, important
differences are ranges and special reductions, for example, to sum or average.
Task:
• Write a method containsNan(double[]) that returns true if the array contains a NaN,
otherwise false.
• A single expression should suffice in the body of the method.
Example:
Generate Decades ⭑
A decade always represents a period of ten years, regardless of its start and end dates. Decades are
typically grouped by their common tens’ digit. The decade from 1990 to 1999 is known as the 0-to-9
decade, starting on January 1, 1990, and ending on December 31, 1999. Another interpretation is the
1-to-0 decade, where the counting of decades begins with a 1 in the one place. In this case, the 1990s
would start on January 1, 1991, and end on December 31, 2000.
Task:
• Write a method int[] decades(int start, int end) that returns all decades from a
start year to an end year as an array.
• The 0-to-9 decade is to be used.
Examples:
Example:
Draw Pyramids ⭑
Task:
• Given the following template for the teddy with a wildcard character #.
String teddy = """
_ _ \s
(c).-.(c) \s
/ o_o \\ \s
__\\( Y )/__ \s
(_.-/’-’\\-._)\s
|| # || \s
_.’ `-’ ‘._ \s
(.-./`-’\\.-.)\s
`-’ `-’ \s""";
• Write a program, which converts a string into a sequence of consecutive teddies.
Example:
• For the input “MME” the following output should appear on the screen:
_ _ _ _ _ _
(c).-.(c) (c).-.(c) (c).-.(c)
/ o_o \ / o_o \ / o_o \
__\( Y )/__ __\( Y )/__ __\( Y )/__
(_.-/’-’\-._) (_.-/’-’\-._) (_.-/’-’\-._)
|| M || || M || || E ||
_.’ `-’ ‘._ _.’ `-’ ‘._ _.’ `-’ ‘._
(.-./`-’\.-.) (.-./`-’\.-.) (.-./`-’\.-.)
`-’ `-’ `-’ `-’ `-’ `-’
166 Java Programming Exercises
• The input is a string. Using clever stream concatenation, generate a new string containing each
letter of the source string followed by the frequency of that letter in the given string.
• The pairs of letters and frequencies should be separated by a slash in the result string.
• Performance does not play a central role.
Examples:
• "eclectic" → "e2/c3/l1/e2/c3/t1/i1/c3"
• "cccc" → c4/c4/c4
• "" → ""
From 1 to 0, from 10 to 9 ⭑⭑
Bonny Brain wants to buy a new boat and sends Elaine to the marina to evaluate boats. Elaine writes her
ratings from 1 to 10 in a row on a piece of paper, something like this:
102341024
The Bonny Brain gets the sequence of numbers, but is not happy with the order and the numbers. First, the
numbers should be separated by a comma, and second, they should start at 0, not 1.
Task:
• Given an int array with the identifiers of the octopuses. Write a program that determines the
number from the array that occurs most often.
6 • Java Stream-API 167
Example
• In the array
int[] values = { 1, 1, 2, 3, 4, 2, 3, 2, 2, 1, 7, 3, 2, 2, 1 };
2 is the winning octopus, since it was named most frequently.
int[] numbers1 = { 1, 2, 3 };
int[] numbers2 = { 1, 1, 2 };
int[] numbers3 = { 4, 3, 1, 2 };
int[] result1 = join( numbers1, numbers2, numbers3 );
int[] result2 = join( numbers1, numbers2, numbers3, 5 );
System.out.println( Arrays.toString( result1 ) ); // [1, 2, 3, 1, 1, 2, 4, 3,
1, 2]
System.out.println( Arrays.toString( result2 ) ); // [1, 2, 3, 1, 1]
▨ ▧
Then she puts two rings in the players’ hands and lets them throw. If the ring goes over an object, that
counts as a win. How many ways are there to win, and what are the possibilities? Taking the two objects
▨ ▧, it could be that a player “hits” ▨ or ▧, or both—▨ and ▧—no-hit is no win.
Task:
• Given a string of arbitrary characters from the Basic Multilingual Plane (BMP), i.e., U+0000 to
U+D7FF and U+E000 to U+FFFF.
• Create a list of all the ways a player can win.
168 Java Programming Exercises
Example:
STATISTICS
The streams IntStream, LongStream, and DoubleStream offers terminating methods
such as average(), count(), max(), min(), and sum(). But if you need not just one, but sev‑
eral statistical pieces of information, you can gather various data in an IntSummaryStatistics,
LongSummaryStatistics, or DoubleSummaryStatistics.
Task:
• Create a Stream of Result objects. Pre-assign some Result objects with selected values
for testing.
• Output a small statistic of times in the end.
Example:
From the following stream …
count: 6
min: 122
max: 434
average: 254
6 • Java Stream-API 169
Calculate Median ⭑⭑
The *SummaryStatistics types return the arithmetic mean with getAverage(). The arithmetic mean is
calculated by dividing the sum of the given values by the number of values. There are many other mean
values, such as the geometric mean or the harmonic mean.
Means are often used in statistics, but they have the problem of being more prone to outliers. Statistics
frequently work with the median. The median is the central value, that is, the value that is “in the middle”
of the sorted list. Numbers that are too small or too large are at the edge and are outliers and are not
included in the median.
If the number of values is odd, then there is a natural middle.
• Example 1: In the list 9, 11, 11, 11, 12, the median is 11. If the number of values is even, the
median can be defined from the arithmetic mean of the two middle numbers.
• Example 2: In the list 10, 10, 12, 12, the median is the arithmetic mean of the values 10 and
12, i.e., 11.
Task:
• We are looking for a program that can process and display temperatures. More precisely:
• Generate a list of random numbers that in the best case follow the temperature curve of the
year, say in the form of a sine curve from 0 to π.
• Generate random temperature values for several years, and store the years with the values
in associative memory. Use the Year data type as a key for a Map sorted by years. Bonus:
the number of days corresponds to the number of days in the year, so 365 or 366.
• Write an ASCII (American Standard Code for Information Interchange) table with the
temperatures of all years to the console.
• Output the highest and lowest annual temperature of a year.
• Output the highest, lowest, and average temperature for a month of a year.
• Generate a file that aggregates and visualizes the 12 average temperatures of a month
from one year. Take the following HTML document as a basis and fill the data array
accordingly.
<!DOCTYPE html><html>
<head><meta charset="UTF-8"></head>
<body>
<canvas></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.3.2/dist/chart.min.
js"></script>
170 Java Programming Exercises
<script>
const cfg = {
type: "bar",
data: {
labels:"Jan. Feb. Mar. Apr. May June July Aug. Sept. Oct. Nov.
Dec.".split(" "),
datasets: [{
label: "Average temperature",
data: [11, 17, 21, 25, 27, 29, 29, 27, 25.6, 21.6, 17.5, 12.5],
}]
}
};
window.onload = () => new Chart(document.querySelector("canvas").
getContext("2d"), cfg);
</script>
</body></html>
SUGGESTED SOLUTIONS
com/tutego/exercise/stream/LambdaHeroes.java
Consumer<Hero> csvPrinter =
hero -> System.out.printf( "%s,%s,%s%n",
hero.name(), hero.sex(), hero.
yearFirstAppearance() );
heroes.stream().forEach( csvPrinter );
To run over all elements of a stream, the method forEach(…) can be used:
This method expects a consumer of type Consumer. The method forEach(Consumer) calls the
method apply(…) on the passed Consumer for each element, and in this way transmits the element in
the stream to the Consumer. For the Consumer we declare a mapping that implements console output,
pulling the three components from the Hero. For just traversing all elements, we don’t need to fetch a
Stream, an Iterable also provides forEach(…).
Predicate<Hero> isAppearanceAfter1900 =
hero -> hero.yearFirstAppearance() >= 1900;
System.out.println( heroes.stream().allMatch( isAppearanceAfter1900 ) );
6 • Java Stream-API 171
A Stream provides three methods that find out whether all or certain elements in the stream have a prop‑
erty or not: allMatch(…), anyMatch(…) and noneMatch(…). All methods are passed a Predicate
to test the property. If we want to know whether all heroes were introduced after 1900, we use the all‑
Match(…) method. The allMatch(…) method walks over all elements of the stream and calls the
Predicate method test(…). If this test always returns true, then all elements in the stream meet the
correct criteria, and the overall response is true. If one of the tests returns false, then the final result
is already false.
The interface Predicate does have some default and static methods, including and(Predicate),
or(Predicate), and negate(). If two criteria are supposed to be true at the same time, two predicates
can be concatenated with the and(…) method and thus bound together to form a larger predicate. If we
want to test whether some hero is female and was introduced after 1950, we can first build two single
predicates and then link them. This approach is reasonable because, first, the predicates become smaller
and easier to test, and second, they are easy to reuse.
Comparator<Hero> firstAppearanceComparator =
Comparator.comparingInt( h -> h.yearFirstAppearance() );
// Comparator.comparingInt( Hero::yearFirstAppearance );
System.out.println( heroes.stream().min( firstAppearanceComparator ) );
A Stream provides min(…) and max(…) methods and can determine the largest and smallest element
based on an ordering criterion:
The return of both methods is an Optional because it may be that the Stream is empty. The min(…)
and max(…) methods expect a Comparator. If the hero/heroine is asked who was introduced first, the
criteria are yearFirstAppearance. Comparator.comparingInt(…) helps to quickly build a
Comparator, and the smaller the year, the smaller the objects. The min(…) method returns the answer
according to the earliest hero.
For the question of which hero appeared first around 1960, we rely on the reduce(…) method, which
favors exactly those heroes that are closer to the year 1960. reduce(…) is declared as follows:
In order to perform the operation, a BinaryOperator must be provided that takes two input elements
and returns a single output element. In this particular implementation, the distance between the first and
second heroes from the year 1960 is calculated, and the hero who is closer to the year 1960 is returned.
In the event that both heroes are equidistant from 1960, the algorithm can arbitrarily select one of the two
heroes.
In principle, another solution with a Comparator and the min(…) method of Stream is also pos‑
sible, like this:
com/tutego/exercise/stream/LambdaHeroes.java
StringBuilder collectedYears =
heroes.stream().collect(
StringBuilder::new,
( sb, hero ) -> sb.append( sb.isEmpty() ? "" : "," )
.append( hero.yearFirstAppearance() ),
( sb1, sb2 ) -> sb1.append( sb2.isEmpty() ? "" : "," + sb2 ) );
System.out.println( collectedYears );
If we want to end up with a StringBuilder with all values, we will not resort to the reduce(…)
method, but to the collect(…) method. While reduce(…) always reduces two values to one, col‑
lect(…) looks at all elements and transfers them into another representation. The method is overloaded,
but the variant of interest to us is the following:
R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner).
The first argument is a producer of the result. Since in our case the result is a StringBuilder, the
constructor reference StringBuilder::new produces this Supplier. The second parameter is
a BiConsumer and puts the year of the hero into the StringBuilder. Special handling adds the
comma between elements if necessary; the separator comes between elements exactly when the
StringBuilder contains elements and is not empty. The last BiConsumer of collect(…) com‑
bines several StringBuilders, which may have been created by concurrent processing, into one
StringBuilder. Although this is not necessary in our case, we want to implement this functionality
as well.
6 • Java Stream-API 173
Instead of passing the three arguments to the collect(…) method, it is also possible to build a
Collector object that combines the Supplier and the two BiConsumer. This increases reusability
and may look like this:
com/tutego/exercise/stream/LambdaHeroes.java
A Collector is passed. The class Collectors declares numerous static methods for predefined
Collector implementations. These include, for example, toList(), toSet(). A Collector is
handy that can be used to group stream elements: Collectors.groupingBy(…):
The generic type information is complex, but the method is simple: the job of the Function is to extract
the keys for the resulting Map. All elements from the stream with the same key are associated as a list
with the key in the Map.
Hence, if a Map of genders is desired, the function is hero -> hero.sex, and a Map<Sex,
List<Hero>> is created, such that lists of heroes that are either male or female have lists attached under
each of the two genders.
Predicate<Hero> isAppearanceAfter1970 =
hero -> hero.yearFirstAppearance() >= 1970;
Map<Boolean, List<Hero>> beforeAndAfter1970Partition =
heroes.stream()
.collect( Collectors.partitioningBy( isAppearanceAfter1970 ) );
System.out.println( beforeAndAfter1970Partition );
The result of groupingBy(…) is always a Map with any number of keys. If the resulting set knows only
two different parts, the method partitioningBy(…) can be used alternatively:
174 Java Programming Exercises
We pass a Predicate for a test, and those elements that pass this test go into the Map as keys under
Boolean.TRUE, the others under Boolean.FALSE.
This answers the question of which heroes were introduced before and after 1970.
System.out.println( heroes.stream()
.filter( hero -> hero.sex() == Sex.FEMALE )
.count() );
All elements that satisfy the predicate are preserved in the stream. If the question is about female heroes,
we write a predicate that extracts the gender of the hero and tests for Sex.FEMALE. A new Stream is
created, but after filtering, the stream may contain fewer elements. The count() method returns the
number of elements.
heroes.stream()
.sorted( Comparator.comparingInt( hero -> hero.yearFirstAppearance() )
)
.forEach( System.out::println );
Some methods of the Stream class are stateful. The operations cannot wait as long as possible with the
evaluation (they are not lazy), but all elements of a stream must be read in so that operations like sorting
or removing duplicate elements can be realized. If the hero stream is to be sorted by the time the heroes
were introduced, we will first form a Comparator again with Comparator.comparingInt(…) and
pass it to the sorted(…) method:
With forEach(…), we consume the sorted stream and output all heroes on the screen.
Besides filter(…), the map(…) method might be the second most important of the stream interface:
The map(…) method applies the Function to each element from the stream, and a new stream is cre‑
ated, possibly of a new type. Thus, we can also use the map(…) method to extract all hero names. After
the desired filter operation, the name is extracted and then bound together via a special Collector to
form a large String where the names are comma-separated.
In the task, we want to remove the entries in round brackets. Again, we do this via the map(…) method
and via a special function that replaces the heroes. The function gets a hero with a plain name and returns
a new hero with everything in round brackets removed. Instead of the type Function<Hero, Hero>
you can also use UnaryOperator<Hero>.
Processing chains of this kind could in principle modify objects and return the modified object, but it
is cleaner to create new objects with the desired changes. Since Hero objects are immutable, we also need
to build new Hero objects. We take the name, and use replaceAll(…) to replace everything in round
brackets—and any sequence of spaces before it—with an empty string, thereby deleting that portion. This
new name, along with the unchanged gender and year, is then passed to the constructor.. The newly cre‑
ated Stream<Hero> is transformed into a list by the Stream method toList().
In addition to the map(…) method, there are methods that return special primitive streams:
These three special streams provide the toArray() method, which results in a primitive array at the
end, rather than an array of objects. This is a good way to collect all the years in an array. Duplicate ele‑
ments are removed by distinct().
176 Java Programming Exercises
Heroes.UNIVERSES.stream()
.flatMap( Heroes.Universe::heroes )
.map( hero -> hero.name() )
.forEach( System.out::println );
The passed function of the method map(…) leads to a direct mapping. This does not create more elements,
the elements are only exchanged. The situation is different with the flatMap(…) method:
The intermediate operations of the stream objects return new stream objects, on which we must cascade
to call the other methods. If the intention is that both streams start over via peek(…) and forEach(…),
the stream must also be rebuilt. Otherwise, it is not possible that forEach(…) to commence from the
beginning of the stream numbers that were previously initiated by peek(…).
String[] names = {
"Anne", "Captain CiaoCiao", "Balico", "Charles", "Anne", "CiaoCiao",
"CiaoCiao", "Drake", "Anne", "Balico", "CiaoCiao" };
Map<String, Long> nameOccurrences =
Arrays.stream( names )
6 • Java Stream-API 177
The starting point is the array names with the names. Arrays.stream(…) gives us a Stream of
strings, an alternative is Stream.of(…). Since our captain can appear in different notations, we normal‑
ize the notations, and with the map(…) method, Captain CiaoCiao always appears in the Stream
instead of CiaoCiao. Other strings are not transformed.
The actual aggregation and counting is done by a special Collector:
counting() returns a Collector<T, ?, Long>, that is, a collector that takes elements of type
T and reduces them to a Long. From the code, we can see that this is also just a shortcut, and we might
as well have written:
Frame Pictures
com/tutego/exercise/stream/FramePicture.java
return
string.lines()
.map( s -> "| " + s + " ".repeat( max - s.length() ) + " |" )
.collect( Collectors.joining(NEW_LINE,
topBottomBorder + emptyRow,
NEW_LINE + emptyRow + topBottomBorder) );
}
To draw the frame around the ASCII art, different sub-problems have to be solved. It starts with the
question, how long is the longest line because that determines the width of the frame. The actual answer
is provided by a single stream expression. If we map each string to the string length and determine the
maximum from the stream of integers, we get our answer.
We do two things with this length: it helps generate the top and bottom horizontal frames, and it also
pads shorter lines with spaces so that all lines are the same length later. The frame starts with a plus sign
and is followed by minus signs, two more for each page than the longest string is long. There will be two
more per side because that is the desired inner distance from the right and left margin. To generate the
minus signs, we use repeat(int) from String. Aside from generating the upper and lower borders, it
is also possible to generate a String for a free line that has strokes on the left and right edges with spaces
in the middle. This free line can then be placed between the top and bottom lines.
After preparing the variables, the actual placing of the image in the frame is just a Stream expres‑
sion. Again, we fetch a Stream of lines with the lines() method and use the map(…) method to trans‑
form each String from the ASCII image:
• Before the string from the image, the frame symbol and the spacing are set.
• Spaces are placed after the string so that the string always has the same width.
• Again some spacing and the frame symbol follow on the right side.
Finally, we collect all the lines and use a Collector for this, which we supply with three pieces of
information:
Let’s take the string 111221. Probably at first, you think of a counting loop, which sets the index always
one position further and looks if the symbol has changed. But we can use regular expressions here. This
sounds strange at first sight because what should be recognized here? The change from 1 to 2 to 1? No! A
regular expression can help us to catch the symbol that repeats. We would write normal repetitions with
the following regular expression:
.*
However, we would then have matched a sequence of arbitrary characters. But we need to express that the
same character repeats multiple times. The character itself can be arbitrary. To achieve this, we can resort
to a special feature of the regex engine implementation: the back-reference.
(.)\\1*
\1 is the back-reference that refers to the first group, that is, to (.). The dot matches one symbol in any
case, and the back-reference matches any other number of identical symbols. We have to use * here and
not +, because the symbol may occur only once, that is, the back-reference is not necessary.
The practical thing about sequences is that we only have to consider sequences of the same symbols.
In this symbol, sequences are the two pieces of information we need: how many times did which symbol
occur? If we find all places with the regular expression, we can replace the found places with the length
of the string and the symbol. This is precisely what the proposed solution does.
In the first step, the intermediate variable sameSymbolsPattern stores the Pattern. This is
always a good idea, if a program requires the same pattern several times. In the second step, we declare
a variable lengthAndSymbol to map the MatchResult to a String. The string that contains the
length of the symbol sequence is the first part and the repeating symbol is the second character of the
string. Interestingly, only the lengths 1, 2, 3 occur, so it always remains with two characters that are con‑
catenated. In principle, both variables are not necessary, but they make the following stream a bit shorter
and more readable.
The static method Stream.iterator(…) needs a start value as the first argument. This is "1". It is
followed by an UnaryOperator, a Function with the same type for input as for output. The operator
is called with the last value of iterator(…), so at startup, it is "1", and can work its way up from there.
In total, we limit the stream to 20 elements and output all elements to the screen in a terminal operation.
At each step, the string grows by about 30%, so it’s good to limit the number of iterations.
• The input can not only originate from a String, but can also be given via a File, an
InputStream or a Path.
• The input is not single line, but always consists of two lines.
180 Java Programming Exercises
However, both requirements do not change the approach of building a Stream<String> and having
duplicate entries removed via distinct(). The central question is only: How do we get the different
sources into one Stream, and how are two lines realized as one string in the stream?
To convert a string, with whatever separators, into a Stream<String>, the very flexible class
Scanner can be used. Scanner objects can be initialized with different input sources, and these
include the String, File, InputStream, and Path types required in the task.
com/tutego/exercise/stream/RemoveAllEqualPairs.java
String lines =
"Balancar\nErbium\n" +
"Benecia\nYttrium\n" +
"Luria\nThulium\n" + // <-
"Kelva\nNeodym\n" +
"Mudd\nEuropium\n" +
"Tamaal\nErbium\n" +
"Varala\nGadolinium\n" +
"Luria\nThulium\n"; // <-
// (?m)(^.*$\n?){2}
Pattern pattern = Pattern.compile( "(^.*$\n)" + // A line
"{2}", // two lines
Pattern.MULTILINE );
String s = new Scanner( lines )
.findAll( pattern )
.map( MatchResult::group )
.distinct()
.collect( Collectors.joining() );
System.out.println( s );
These next*() methods do not help because they do not result in a Stream. The Scanner class has
three methods that return a Stream:
The findAll(…) method is helpful in the task because it returns results from a Match. We just need to
use the regular expression to determine what exactly we want to catch, and that is exactly two lines. The
regular expression "(^.*$\n){2}" consists of four central components:
so that the boundary matchers ^ and $ stand for a local line and not for the entire input, a flag Pattern.
MULTILINE must be set. This variant is chosen by the proposed solution, but the flag can also be inserted
directly into the regular expression, then one would write: "(?m)(^.*$\n?){2}".
If this regex expression comes into the findAll(…) method, a Stream<MatchResult> is cre‑
ated. From the MatchResult objects in the stream, only the complete match returned by group() is
relevant. Each group is a two‑line string. distinct() removes all duplicate two‑part strings, and finally
a collector concatenates all these strings in the stream back into one big String.
We can easily have duplicate elements removed with the distinct() method. Then collect(…) can
use a Collectors.toMap(…) to transform the elements into a Map. Reminder:
The first Function determines the keys, which in our case are the Point.Double objects in the
Stream. Function.identity() is t ‑> t, so the elements in the stream are also immediately
the keys. The second function calculates the distance to the captain, where our Function<Point.
Double, Integer> internally falls back to distance(…). Thus, toMap(…) establishes the associa‑
tion between the point and the distance.
If the distinct() method is missing, there are keys twice, and an exception
IllegalStateException: Duplicate key follows.
But it also works without distinct(). Variant 2:
com/tutego/exercise/stream/DistanceToNextStation.java
The third argument is a BinaryOperator and the parameter name already reveals what it is about:
mergeFunction is only called if keys occur multiple times and reduce the values to one result. Since in
our case, the key‑value pairs are always identical, we can simply drop a pair of coordinates and distance.
This is handled by (d,__) -> d; the BinaryOperator<Integer> is passed two distances; the sec‑
ond value is ignored, and only the first distance is ever returned, since they are always the same.
The solution makes use of the groupingBy(…) method, which provides a function as a classifier, and a
Collector for the associated elements. As a reminder:
The second proposed solution also uses groupingBy(Function, Collector), but here the program
does not use a predefined Collector, but writes its own for the return Map<String, Boolean>.
Collector objects can be built using the following static factory method:
For the desired return type Map<String, Boolean> we are looking for a Collector that returns
Boolean, so the parameterization must look like this: Collector<Object, *, Boolean>. The
first type can remain Object because we do not access the strings. The type * does not occur in the
stream and is an internal container, we use a long array to store the count; so the correct declaration is:
Collector<Object, long[], Boolean>.
In the of(…) method, we pass the four necessary arguments; Characteristics is a vararg and
irrelevant in our case.
1. The Supplier returns a long array with one entry only. The Collector remembers
the number. The Collector has a state by which it can later decide whether String has
occurred more than twice.
2. The BiConsumer gets the long array and the String, but only increments and does not
access the String. Therefore, the type argument could also be Object and did not have to
be String.
3. The BinaryOperator merges two long arrays. The operation is performed only on parallel
streams.
4. The Function at the Collector maps the result to a boolean. Whenever the counter in
the array exceeds 1, the result is true and thus forms the value associated with the string.
The terminal method anyMatch(*Predicate), available in both regular and primitive streams, effort‑
lessly accomplishes this task. The method reference Double::isNaN is an abbreviation for value ‑>
Double.isNaN(value).
Generate Decades
com/tutego/exercise/stream/DecadesArray.java
The primitive streams IntStream and LongStream include two static range*(…) methods for creat‑
ing a stream from integers:
IntStream
LongStream
The start and end value can be determined, also whether the end value belongs to the stream or not, but
the step size is always 1. Since in the task, the end value belongs to the result, rangeClosed(…) is a
good choice.
To solve the problem, we need to increase the step size from 1 to 10. This can be done by
1. Dividing the start and end value for rangeClosed(…) by 10, which results in a stream in
steps of one.
2. Multiplying the elements by 10 in the next step via map(…).
Since the target is an array and not an IntStream, toArray() returns the desired int[].
In the first step, the method checks the validity of the parameters as usual. The size must not be negative,
otherwise, an exception follows. The assignment of value does not have to be checked because the vari‑
able can be assigned with any value.
IntStream.range(…) generates the IntStream with size many elements. The fact that the
stream generates the numbers from 0 to size is irrelevant in our case, we transfer all values to a fixed
value. The identifier _ _ expresses that the lambda parameter is unused. This creates a stream with only
constant values. toArray(…) converts the stream into an array.
_ _ \s
(c).‑.(c) \s
/ o_o \\ \s
__\\( Y )/__ \s
(_.‑/’‑’\\‑._)\s
|| %c || \s
_.’ `‑’ ‘._ \s
(.‑./`‑’\\.‑.)\s
`‑’ `‑’ \s""";
String teddies =
teddy.lines()
.map( line ‑> text.codePoints()
.mapToObj( line::formatted )
.collect( Collectors.joining() ) )
.collect( Collectors.joining( "\n" ) );
System.out.println( teddies );
When solving the task, we have to keep the big picture in mind that we don’t want the teddies to be one
beneath the other, but side by side. To achieve this, we have to place the lines of the template several times
behind each other, which necessitates an outer loop that runs over all the lines. We also need an inner loop
that places the teddies next to each other.
The streams implement the loops. teddy.lines() builds a Stream<String> over all lines of the
teddy bear. For each line, there is another stream over the number of characters in the text. The call
to codePoints() returns an IntStream for the string. The primitive int elements of the stream are
converted to a Stream<Object> with mapToObj(…), where the method reference is just a shortcut for
(int c) ‑> line.formatted(c), so the int from the IntStream will go into the String format
template exactly where %c is.
This gives us as many teddy bear lines as there are characters in text. The first collect(…)
method concatenates each partial line into one big line string. Now the lines just need to be joined with a
newline character to form the main string, this is done by the second call to collect(…).
Draw Pyramids
com/tutego/exercise/stream/StreamPyramid.java
The generated output has the peculiarity that there are always two characters /\ next to each other. In the
first line, it is one pair, in the second line, it is two pairs, and so on. So, we have to create an IntStream
that goes from 1 to the desired height. The spaces that have to be set in front are also dependent on this
counter. For a generated stream from 1 to 5, the number of spaces is just 5 ‑ i. The program is extracted
into a method so that the height can be parameterized.
expanded to an int, and in the second method they are equal to int. The difference primarily affects
compound code points, for our case, we’ll stick to the simple chars() method.
com/tutego/exercise/stream/LetterOccurrences.java
The IntStream provides us with a stream containing each character. We now need to associate these
characters with their respective frequencies in the input string. To count frequencies, we can repeatedly
build an IntStream and use the filter method and count() to find out the number of characters in the
stream. If we concatenate the character and this counter, in the next step, we have a Stream<String> in
which each character has been mapped to this pair. Finally, these pairs must be put together with a slash.
This can be done by a reduction with Collectors.joining(…).
From 1 to 0, from 10 to 9
com/tutego/exercise/stream/DecrementNumbers.java
With a smartly chosen Stream, the task can be solved in a single expression. To get from start to finish,
let’s first look at the steps involved:
The first step is to recognize the numbers. Finding partial strings is the task of regular expressions.
Regular expressions can be processed with the class Pattern, but this class does not help us with data
streams. The second class that allows us to find strings that match regular expressions is Scanner. We
can apply a Scanner directly to a Reader. The Scanner returns a Stream<MatchResult> of all
matches with the findAll(…) method. The regular expression must recognize all possible occurring
numbers, i.e., 10|9|8|…|1. The or‑connection can be abbreviated as 10|[1‑9], but not as [1‑9]|10.
The matches are returned as MatchResult, and we have to fall back from that to the main group
with group(); that gives us a String. For summing, we use mapToInt(…) to convert the String to
an integer first. This is done in Integer.parseInt(…). It is perfect to use a method reference because
Integer.parseInt(…) matches the signature of Function. Parse errors cannot occur because the
regular expression only matches numbers.
6 • Java Stream-API 187
The integer itself must be decremented by 1. We could write this as a lambda expression, but there is
a suitable method that can also be accessed via a method reference: Math.decrementExact(…); it does
throw an ArithmeticException if we exceed the value range, but that does not occur in our case.
After decrementing the number, everything must be converted to a large string. This is done in two
parts: in the first step, each integer is converted into a string by itself, then these strings are assembled
into one long string using a special collector.
int[] values = { 1, 1, 2, 3, 4, 2, 3, 2, 2, 1, 7, 3, 2, 2, 1 };
OptionalInt winner =
IntStream.of( values ) // IntStream
.boxed() //
Stream<Integer>
.collect( groupingBy( identity(), counting() ) ) //
Map<Integer,Long>
.entrySet() //
Set<Entry<Integer,Long>>
.stream() //
Stream<Entry<Integer,Long>>
.max( Comparator.comparingLong(Map.Entry::getValue) ) //
Optional<Entry<Integer,Long>>
.map( Map.Entry::getKey ) //
Optional<Integer>
.map( OptionalInt::of ).orElse( OptionalInt.empty() );// OptionalInt
System.out.println( winner );
• Since we want to use the Stream API for the task, it starts by creating a stream. First, an
IntStream is generated from the array of primitive integers.
• Since the IntStream has no useful methods by which we can accomplish our task, a
Stream<Integer> is generated from the IntStream, that is, the wrapper objects are gen‑
erated from the primitive values.
• Regular streams allow grouping with the collect(…) method, and a special Collector that
a Map is created at the end. The key is supposed to be the value in the stream, and the associated
value is the frequency of occurrence. Both methods are imported statically, entirely it is called
Function.identity() and Collectors.counting(). The result is a Map<integer,
long>. With this, the main work is done.
• Now it is necessary to select the largest value from the associated value reflecting the frequen‑
cies. Keys and values are together in the Map and cannot be sorted by value. However, sets can
already be sorted, so a Set<Map.Entry<Integer, Long>> is built.
• The entrySet() method returns a Set, but we cannot easily use it to determine the smallest
or largest value. However, a Stream has this ability, so a Stream<Map.Entry<Integer,
Long>> is built.
• Sorting the Stream is not necessary, only the largest associated value of the Map.Entry objects
is needed. This can be obtained by the max(…) method with a special Comparator. The
Comparator can be built using a key extractor, which extracts the value (not the key) from the
Map.Entry object. The result is an object of type Optional<Map.Entry<Integer, Long>>.
188 Java Programming Exercises
public static int[] join( int[] numbers1, int[] numbers2, int[] numbers3,
long maxSize ) {
if ( maxSize > numbers1.length + numbers2.length + numbers3.length )
throw new IllegalArgumentException(
"The maximum number of elements exceeds the number of total elements" );
To make our method join(…) a bit more flexible, there are two implementations. join(int[] num‑
bers1, int[] numbers2, int[] numbers3, int maxSize) has an additional parameter
maxSize which limits the number of elements of the resulting array. Let’s get started.
First, we run two tests.
1. The first test checks whether the desired maximum number of elements maxSize is achievable by
three arrays at all. If there are fewer elements, there shall be an IllegalArgumentException
with an error message.
2. As it stands now, arrays cannot get larger than Integer.MAX _ VALUE ‑ 8, so there is a constant
in the proposed solution.1 Therefore, the second case distinction checks that the passed maxSize
is not above the maximum size of Java arrays. Even if later numbers1.length + numbers2.
length > MAX _ ARRAY _ LENGTH but maxSize is below MAX _ ARRAY _ LENGTH,
limit(maxSize) of the Stream object keeps the later array within bounds.
6 • Java Stream-API 189
There is no special method in Java to assemble arrays, but primitive streams come to the rescue. The
IntStream concat(IntStream a, IntStream b) method concatenates two instances of
IntStream into a new IntStream. Since our sources are int[] arrays, IntStream.of(…) returns
the required IntStream instances. Since there is no parameterized concat(…) method with three
arguments or a data structure, concat(…) must be nested, either by concat(A, concat(B, C) or
concat(concat(A, B), C). This is followed by pruning with limit(…) and in the end, toArray()
creates the desired array.
The simpler method join(…) with three parameters calculates the total length and delegates
to the method with four parameters. The array lengths are of type int, and we expand this to long
so that the sum is long, we have no overflow, and then we can see that the sum is not greater than
MAX_ARRAY_LENGTH.
The resulting strings go into a set, and duplicate resulting strings fly out. Since the caller expects
a set and should not pass an empty container into the method, there is a second public method
removeLetter(String). That method builds a HashSet, passes it along with the word to the private
method removeLetter(String, Set<String>), and returns the new set at the end.
DoubleSummaryStatistics statistics =
stream.mapToDouble( Result::time ).summaryStatistics();
System.out.printf( "count: %d%n", statistics.getCount() );
190 Java Programming Exercises
The first variant is to create a DoubleStream with the times from the Stream<Result> and then call
summaryStatistics() on the DoubleStream.
The second possibility is to use directly a corresponding Collector:
com/tutego/exercise/stream/PaddleCompetition.java
DoubleSummaryStatistics statistics =
stream.collect( Collectors.summarizingDouble( Result::time ) );
Calculate Median
com/tutego/exercise/stream/DoubleStreamMedian.java
As usual, we check the input, and if the array consists of no elements, there is an exception. Moreover,
there is automatically an exception if values are equal to null.
When calculating the median, we have to navigate to the middle. Then we have to consider one ele‑
ment or two elements in the middle. If a DoubleStream is built and then sorted, skip(…) allows skip‑
ping a certain number of elements to get to the middle. limit(…) in the next step reduces the number
of remaining elements in the stream to either one element (array had an odd number of elements) or two
elements (array had an even number of elements). Finally, the chain averages the value, with nothing much
to calculate for one value, but with two elements, the average() method gives us the arithmetic aver‑
age. Since the method wants a floating‑point number as the result, getAsDouble() returns that number,
and that is valid since the stream has exactly one element. An alternative API design could have returned
OptionDouble, thus accounting for the special case where the double array contains no elements.
The most exciting part is the calculation of the shift and the limit. For this, the program introduces
two variables skip and limit, which are derived from the length of the input. Two examples:
The variables are initialized as follows:
Both variables depend on the length of the array. The center is the array length divided by two, which fits
quite well for arrays with an odd number of elements, but a pure division by two leads to a problem for
arrays with an even number of elements. Because in that case, we have to consider the two elements to the
left of the center and the right of the center. (These elements even have a name and are called upper and
lower median.) Therefore, if the length is decreased by one before dividing by two, we end up with an even
number one element before the center. To clarify it:
TABLE 6.2 Examples for the calculation of skip, the values in the middle of the list are shown in bold
LIST (VALUES.LENGTH ‑ 1) / 2 (VALUES.LENGTH) / 2
9, 11, 11, 11, 12 (5 – 1) / 2 = 2 5 / 2=2
10, 10, 12, 12 (4 – 1) / 2 = 1 4 / 2=2
The limit must be 2 for an even number of elements in an array and 1 for an odd number of ele‑
ments. values.length % 2 returns 0 for an even number and 1 for an odd number. Consequently,
the expression 2 ‑ values.length % 2 returns 2 ‑ 0 = 2 for an even number and 2 ‑ 1 for an
odd number, i.e., 1.
The special data type Year is useful because it returns the number of days in the year via the length()
method because not every year is 365 days long. With this number of days in the year, we can form an
IntStream and then generate a random temperature for each day in the year. The average temperature
distribution shows up in a curve: at the beginning and end of the year the temperature is low and in the
middle of the year it is high. This is something we can express well using a sine function. Therefore, we
transfer the integer with a function into a sine value. The beginning of the year at the first day corresponds
to the sine of 0 and the end of the year at the last day results in the sine of π (Pi) thus again 0. In between
is the sine hill. The sine values between 0 and 1 are small, the maximum of sin(π) is 1, so the values are
multiplied by 20 in the next step and then taken plus 10 in the next step. The values are not random now,
so the last mapping brings in some randomness so that the sine values fluctuate up and down a bit.
createRandomTemperatureMap() uses the randomTemperaturesForYear(…) method
to build a Stream<Year> with five elements, starting at the current year and then moving forward one
year at a time. The result is a SortedMap<Year, int[]> of the temperature values associated with
each year. However, the method toMap(Function keyMapper, Function valueMapper) with
two arguments should not be used because there is no prediction of the Map—internally, the OpenJDK
uses a HashMap. In our case, a Map sorted by years is useful. Therefore, toMap(Function keyMap‑
per, Function valueMapper, BinaryOperator mergeFunction, Supplier map‑
Factory) is used, so that explicitly the data come into a sorted TreeMap.
This can go into the output:
com/tutego/exercise/stream/TemperatureYearChart.java
printTemperatureTable(…) takes care of printing the table for all the years passed in. The pass is
an associative store that associates the years with the temperatures. The forEach(…) method goes over
the data structure sorted by years and creates a string temperatureCells. To build the string, the first
step is to turn the int array into an IntStream. Each temperature value is mapped to a string, with
values less than 10 getting a preceding space so that the output is always two digits wide. The collector
combines all the individual strings, and the result is temperatureCells. This string with the tempera‑
ture values is printed, with the preceding years, on the screen.
Let’s move on to the statistics:
com/tutego/exercise/stream/TemperatureYearChart.java
IntSummaryStatistics yearStatistics =
Arrays.stream( yearToTemperatures.get( Year.now() ) ).summaryStatistics();
System.out.printf( "max: %d, min: %d%n",
yearStatistics.getMax(), yearStatistics.getMin() );
6 • Java Stream-API 193
We have already used a few times the Arrays.stream(…) method, which is an alternative to
IntStream.of(…) and creates an IntStream from an int array. The three primitive streams have
a special method summaryStatistics() compared to the regular Stream objects, which provide a
statistics object with information about the minimum, maximum, and average value. We can easily grab
this information and output it.
A separate method getStatistics(…) retrieves these IntSummaryStatistics for one month
of a year from the Map:
com/tutego/exercise/stream/TemperatureYearChart.java
In the passed int array temperatures, all temperature values of the year are stored. If we want to
calculate the statistics of a concrete month, we have to build a sub‑array at the appropriate place. This can
be solved well with Arrays.stream(…) because the start and end index in this array can be given. The
question that arises with a year of approximately 365 days is: when does a month like March or December
actually begin? The answer is provided by the YearMonth object. For the start value, we request with
atDay(1) a new YearMonth object for the beginning of the month and get with getDayOfYear()
the day in the year. We do the same for the last day of the month, using the atEndOfMonth() method
to set the YearMonth object to the end of the month. We pass the start and end values to Arrays.
stream(…), where the month starts at 1, and we need to move the start value one position to the left.
Furthermore, we do not move the end one position to the left because the value is exclusive and not
inclusive.
The method can be called as follows:
com/tutego/exercise/stream/TemperatureYearChart.java
IntSummaryStatistics monthStatistics =
getStatistics( YearMonth.of( 2020, SEPTEMBER ),
yearToTemperatures.get( Year.now() ) );
System.out.printf( "max: %d, min: %d, average: %.2f%n", monthStatistics.
getMax(),
monthStatistics.getMin(), monthStatistics.getAverage() );
data: {
labels:"Jan. Feb. Mar. Apr. May June July Aug. Sept. Oct. Nov.
Dec.".split(" "),
datasets: [{
label: "Average temperature",
data: [%s],
}]
}
};
window.onload=()=>new Chart(document.querySelector("canvas").
getContext("2d"),cfg);
</script>
</body></html>""";
String formattedTemperatures =
IntStream.rangeClosed( JANUARY.getValue(), DECEMBER.getValue() )
.mapToObj( Month::of )
.map( month ‑> year.atMonth( month ) )
.map( yearMonth‑>getStatistics(yearMonth,yearToTemperatures.
get(year)) )
.map( IntSummaryStatistics::getAverage )
.map( avgTemperature ‑> String.format( ENGLISH, "%.1f",
avgTemperature ) )
.collect( Collectors.joining( "," ) );
String html = String.format( template, formattedTemperatures );
Files.writeString( path, html );
}
The declaration of the HTML document takes up the largest area in the method. In one place it says
data: [%s], and this %s is a typical placeholder in the format string, so we can use the String.for‑
mat(…) method later to insert the dynamically calculated values. The method proceeds as follows:
Finally, the string formattedTemperatures contains the result, which now has to be inserted into
the HTML document. After creating the complete HTML document, write(…) creates the file.
writeTemperatureHtmlFile(…) can be used as follows:
6 • Java Stream-API 195
com/tutego/exercise/stream/TemperatureYearChart.java
try {
Path tempFile = Files.createTempFile( "temperatures", ".html" );
writeTemperatureHtmlFile( Year.now(), yearToTemperatures, tempFile );
Desktop.getDesktop().browse( tempFile.toUri() );
}
catch ( IOException e ) { e.printStackTrace(); }
NOTE
1 The OpenJDK declares a constant in the internal package jdk.internal.util: public class ArraysSupport
{ public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE ‑ 8; … }. Since
the module jdk.internal.util is taboo for us, there is a copy of the constant in in the proposed solution.
Files, Directories,
and File Access 7
Despite everything moving to the cloud and databases becoming all the rage, the good old file system is
still relevant for storing and organizing important documents. Even tech‑savvy folks like Captain Bonny
Brain and Captain CiaoCiao still rely heavily on local storage. There are certain things that just shouldn’t
see the light of day, you know?
Prerequisites
• Know the File class, Path interface, and Files class in the basics.
• Be able to create temporary files.
• Have the capability to extract metafiles from both files and directories.
• Be able to list and filter directory contents.
• Read and write complete files.
• Know RandomAccessFile.
• java.io.File
• java.nio.file.Path
• java.nio.file.Paths
• java.nio.file.Files
• java.nio.file.DirectoryStream
• java.nio.file.FileVisitor
• java.io.RandomAccessFile
• java.awt.Desktop
• Create a temporary file ending with the file suffix .html using an appropriate files method.
• Write HTML code in the new temporary file, such as the following:
<!DOCTYPE html><html><body>
'The things we steal tell us who we are.'
‑ Thomas of Tew
</body></html>
• Find a method from the java.awt.Desktop class that opens the default browser and dis‑
plays the HTML file as a result.
• Write a method mergeFiles(Path main, Path... temp) that opens the master file,
adds all temporary contents, and then writes back the master file.
• Write a Java method cloneFile(Path path) that creates copies of files, generating the file
names systematically. Suppose <name> symbolizes the file name, then the first copy will be
Copy of <name> and thereafter the file names should be Copy (<number>) of <name>.
• If you call the methods on directories or there are other errors, the method may throw an
IOException.
Example:
• Suppose a file is called Top Secret UFO Files.txt. Then the new file names should look like this:
• Copy of Top Secret UFO Files.txt.
• Copy (2) of from Top Secret UFO Files.txt.
• Copy (3) of from Top Secret UFO Files.txt.
198 Java Programming Exercises
• Using Files and the newDirectoryStream(…) method, write a program that lists the
directory contents for the current directory.
• Call the dir program under DOS. Rebuild the output of the directory listing completely. The
header and footer are not necessary.
• Given is any directory. Search in this directory (not recursively!) for all images that are of type
GIF and have a minimum width of 1024 pixels.
Access the following code to read the widths and perform GIF checking:
The method reads the first bytes and checks if the first six bytes match either the string GIF87a
or GIF89a. In principle, this test can also be implemented with ! new String(bytes, 0,
6).matches("GIF87a|GIF89a"), but that would cause some temporary objects in memory.
After the check, the program reads 2 bytes for the width and converts the bytes to a 16‑bit integer.
Exercise:
• Using a FileVisitor, run recursively from a chosen starting directory through all subdirec‑
tories, looking for empty text files.
• Text files are files that have the file extension .txt (case‑insensitive).
• If found, show the absolute path of the file on the console.
• newDirectoryStream(Path dir)
• newDirectoryStream(Path dir, String glob)
• newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path>
filter)
The result is always a DirectoryStream<Path>. The first method does not filter the results, the sec‑
ond method allows a glob string such as *.txt, and the third method allows any filter.
java.nio.file.DirectoryStream.Filter<T> is an interface that filters must implement.
The method is boolean accept(T entry) and is like a predicate.
The Java library declares the interface, but no implementation.
Exercise:
Ideally, the API allows all filters to be concatenated, something like this:
DirectoryStream.Filter<Path> filter =
regularFile.and( readable )
.and( largerThan( 100_000 ) )
.and( magicNumber( 0x89, 'P', 'N', 'G' ) )
.and( globMatches( "*.png" ) )
.and( regexContains( "[‑]" ) );
try ( var entries = Files.newDirectoryStream(dir,filter) ) {
entries.forEach( System.out::println );
}
SUGGESTED SOLUTIONS
try {
String html = """
<!DOCTYPE html><html><body>\
›The things we steal tell us who we are.‹\
‑ Thomas von Tew</body></html>""";
Path tmpPath = Files.createTempFile( "wisdom", ".html" );
Files.writeString( tmpPath, html );
Desktop.getDesktop().open( tmpPath.toFile() );
}
catch ( IOException e ) {
System.err.println( "Couldn't write HTML file in temp folder or open file"
);
e.printStackTrace();
}
In the proposed solution, we are dealing with three central statements. At the beginning, there is the cre‑
ation of the file in the temporary directory. In the method createTempFile(…) we can specify a part
of the name as well as a suffix, and we choose the extension .html, so that later the operating system
can select the appropriate viewer via this file extension. It returns createTempFile(…) the generated
Path, which we use to write the string into this file.
For writing a string, there is the method writeString(Path path, CharSequence csq,
OpenOption… options). The class String implements CharSequence. The OpenOption
parameter is not necessary in our particular case, as it would only be required if we needed to append to
existing files.
open(…) is one of the few methods that require a File object. From the Path we generate a
File object and use it to open the browser, which should be associated with rendering HTML files.
Alternatively, for web pages, we can use browse(URI) and get the URI from the path via toUri().
7 • Files, Directories, and File Access 201
public static void mergeFiles( Path main, Path... temp ) throws IOException
{
Iterable<Path> paths =
Stream.concat( Stream.of( main ), Stream.of( temp ) )::iterator;
Collection<String> words = new LinkedHashSet<>();
for ( Path path : paths )
try ( Stream<String> lines = Files.lines( path ) ) {
lines.forEach( words::add );
}
Files.write( main, words );
}
For our task, the LinkedHashSet data structure is ideally suited because as a set, it contains elements
only once, and it considers the order of the inserted elements. We only have to take care that the rows of
the first file come first into the data structure, and then the rows of the remaining files.
For reading the lines and inserting them into the data structure, the first file should be treated in the
same way as the rest of the files. But the unification is only possible by a workaround because the first
data type in the parameter list is a single Path variable, and then follows a vararg, i.e., a Path array.
The proposed solution first puts the first element into a Stream and combines that with a second stream
of the elements in the vararg array; the result is a Stream<Path>. We only have to run this stream. In
theory, forEach(…) can be used here, but there is a problem: input/output operations throw checked
exceptions, and these do not get along with the lambda expressions. The Stream is therefore converted
to an Iterable so that we can use the extended for loop. The ::iterator method reference returns
an expression of type Iterable; a neat trick, since Stream itself does not implement Iterable.
The extended for loop runs over the files, reads in all the lines, puts them into the data structure, and
finally writes all the lines back to the first file.
The algorithm from the proposed solution proceeds by generating possible filenames in order and testing
until a free filename is found. In round brackets, there is a counter starting at 2. There is no Copy (1)
of <Name>.
The method cloneFile(Path path) starts with a query if a directory was passed as path
accidentally, and raises an exception in that case; we cannot clone directories. If it is a file, we extract the
directory of the file and the filename.
The first sample for a possible new filename starts with Copy of and does not yet contain a counter.
We can test this filename for existence with Files.exists(…). If the file exists, we have to continue
with a counter. Therefore, we set this existence test as a condition in a for loop and use a counter variable
i, which we initialize with 2 at the beginning to be able to represent the counter in round brackets. In the
body of the loop, the variable copyPath is reassigned, always with the loop counter in round brackets.
We run through the loop until we find a copyPath that does not exist. Then the loop terminates, and
Files.copy(…) creates a copy of the file with the path specification of copyPath.
To make it easier to change the strings for different languages, they are extracted as constants.
To solve the task, different APIs come together. We need the class Files, the type Path, date/time cal‑
culations, and format strings.
Since the file operations can throw potential exceptions, but we cannot handle them, our method will
pass possible exceptions to the caller. Nevertheless, a try‑with‑resources comes into play—the resource
is the DirectoryStream. If you program quick and dirty, you will often see the DirectoryStream
as an Iterable, located to the right of the colon of the extended for loop. But the DirectoryStream
is a resource that needs to be closed. So, we find the extended for loop to loop through all entries in the
directory in the next step.
The variable path now contains a path, which can stand for a file or a directory. In any case, we want
to get the time of the last access. Although Files.getLastModifiedTime(…) returns the neces‑
sary FileTime object, the toString() method does not return anything appealing. Therefore, a little
detour is necessary to get a nice output: first, the FileTime is converted to an Instant, which is then
7 • Files, Directories, and File Access 203
converted to a LocalDateTime, and this allows us to use a DateTimeFormatter with the pattern
"dd.MM.yyyy hh:mm", for whose format we have introduced a separate constant.
We have the date and time, now the other segments follow. Depending on whether the path is a direc‑
tory or a file, we have to set <DIR> or, in the case of a file, ask for the file length; String.format(…)
brings the number of bytes to an appropriate length.
In the last step, we ask for the name of the file or directory and put everything together in one line.
This line starts with the formatted date and time, then the indication if it is a directory, otherwise the file
length, and finally the file name or directory name.
For us, DirectoryStream.Filter<? super Path> filter is relevant because it can be used to
implement a criterion for limiting the results. For context, let’s look at the UML diagram:
java.lang java.io
T
«interface» «interface»
Iterable Closeable
java.nio.file
T
«interface»
DirectoryStream
iterator(): Iterator<T>
«interface»
Filter
The Filter is a nested type of DirectoryStream. If we need to pass a filter, we need to imple‑
ment the Filter interface and the accept(…) method. The implementation can be done by a class, by
a lambda expression, or by a method reference. And “luckily” boolean isGifAndWidthGreate
rThan1024(Path entry) matches boolean accept(Path entry), which suggests a method
reference.
com/tutego/exercise/io/FindBigGifImages.java
try {
try ( DirectoryStream<Path> files =
Files.newDirectoryStream( directory,
FindBigGifImages::isGifAndWidthGreaterT
han1024 ) ) {
files.forEach( System.out::println );
}
}
catch ( IOException e ) {
e.printStackTrace();
}
The solution implements the findEmptyTextFiles(…) method with two parameters: the first one is
used for the base directory, while the second one is a consumer that gets called whenever a path is found.
The static method Files.walkFileTree(…) recursively walks the file system from a directory to
a desired depth. In our case, we do not limit the depth and do not provide any other options. The method
must be passed as an implementation of FileVisitor. This is not a functional interface, but an interface
with four methods. We could implement the interface ourselves, but the Java library provides a simple
implementation with SimpleFileVisitor.
7 • Files, Directories, and File Access 205
java.nio.file
T
«enumeration» «interface»
FileVisitResult FileVisitor
T
SimpleFileVisitor
FIGURE 7.2 UML diagram of FileVisitor, the subclass SimpleFileVisitor and the enumeration
FileVisitResult for the returns.
From this class, we make a subclass and override the visitFile(…) method relevant for us, which
is called whenever walkFileTree(…) finds a file. Filter does not allow walkFileTree(…), which is a
pity because newDirectoryStream(…) allows filter. Consequently, we have to implement the criteria
that the filename ends with .txt and is 0 bytes in size ourselves. Thankfully, visitFile(…) gives us
If both criteria are correct, we call the callback function and pass the path to the file. Then we want to
continue the search in the directory, and for this the method returns FileVisitResult.CONTINUE.
class FileFilters {
public interface AbstractFilter extends DirectoryStream.Filter<Path> {
default AbstractFilter and( AbstractFilter other ) {
return path ‑> accept( path ) && other.accept( path );
}
default AbstractFilter negate() {
return path ‑> ! accept( path );
}
static AbstractFilter not( AbstractFilter target ) {
return target.negate();
}
}
/**
206 Java Programming Exercises
suffixOffset, suffix, 0,
suffixLen );
} );
}
/**
* Tests if the content of a {@code Path} starts with a specified sequence
of bytes.
*/
public static AbstractFilter magicNumber( int... bytes ) {
ByteBuffer byteBuffer = ByteBuffer.allocate( bytes.length );
for ( int b : bytes ) byteBuffer.put( (byte) b );
return magicNumber( byteBuffer.array() );
}
/**
* Tests if the content of a {@code Path} starts with a specified sequence
of bytes.
*/
public static AbstractFilter magicNumber( byte... bytes ) {
return path ‑> {
try ( InputStream in = Files.newInputStream( path ) ) {
byte[] buffer = new byte[ bytes.length ];
in.read( buffer );
// If file is smaller than bytes.length, the result is false
return Arrays.equals( bytes, buffer );
}
};
}
/**
* Tests if a {@code Path} regexContains a specified regex.
*/
public static AbstractFilter regexContains( String regex ) {
return path ‑> Pattern.compile( regex ).matcher( path.toString()
).find();
}
/**
* Tests if a filename of a {@code Path} matches a given glob string.
*/
public static AbstractFilter globMatches( String glob ) {
return path ‑> path.getFileSystem().getPathMatcher( "glob:" + glob )
.matches( path.getFileName() );
}
The interface has no additional default or static methods, and furthermore the type argument
is always Path in our case, so a new interface AbstractFilter is to be created as a subtype of
Filter with two additional default methods and one static method. The and(…) method associates
two AbstractFilters with a logical AND, the negate() method negates the result of its own
accept(…) method, and the static not(…) method returns a new AbstractFilter object, which
also negates the result.
208 Java Programming Exercises
The FileFilters class declares various constants and methods. Whenever something has to be
parameterized, a method is used; if no parameterization is necessary, a constant is sufficient. All follow‑
ing constants are of the data type AbstractFilter and the methods return AbstractFilter. The
constants are initialized with a corresponding method from the Files class via the method reference.
Let’s focus on the more exciting methods.
• hasSuffix(…) joins all possible file extensions to a Stream and then queries whether
the path has one of the passed file extensions. This is done by first converting the path to a
string and then using regionMatches(…) to check the suffix in a case‑insensitive manner.
Checking via regionMatches(…) is a bit more cluttered in code than working with toLow‑
erCase(…) and endsWith(…), but regionMatches(…) does not build temporary objects
and is a little more performant.
• magicNumber(byte...) takes a variable number of bytes, throwing a
NullPointerException if the parameter variable is null. Otherwise,
1. An input stream is opened.
2. Exactly as many bytes are read in as the parameter array is large.
3. The two arrays are compared with Arrays.equals(..). If the file is smaller than the
passed number of bytes, then the equals(…) method will return false in any case,
because the first thing the method checks is the same number of elements.
• magicNumber(int...) is the overloaded variant of magicNumber(byte...) because
bytes in the parameter list are inflexible due to the value range –128 to +127; developers would
have to write, for example:
magicNumber( (byte) 0x89, (byte) 'P', (byte) 'N', (byte) 'G' )
The int… data type is more convenient for callers; so it could be:
magicNumber( 0x89, 'P', 'N', 'G' )
Therefore, magicNumber(int..) converts the int... to a byte[] and delegates to
the main method.
• regexContains(…) takes a regular expression, compiles it, then applies it to the path, and if
the find() method returns true, matches the filename to the regular expression.
• globMatches(…) matches the filename against a glob string; these are simple expressions,
like *.txt or ??‑??‑1976. getPathMatcher(…) returns a PathMatcher with the pre‑
fix glob: or regex:. The implementation of regexContains(…) does not fall back to a
PathMatcher with regular expression because the PathMatcher tests for a full match, but
in the solution a partial search is desired.
System.out.println( matcher.group( 1 ) );
}
}
The solution has two steps: First, a block is read at the end of the file, and then the last line is extracted
from this block.
To determine the correct block size, we multiply the maximum line length known from the task (100)
by the maximum number of bytes expected per UTF‑8 character; in UTF‑8 encoding, a maximum of four
characters can encode a symbol. The task becomes more difficult if the maximum line length is unknown,
but the product MAX_LINE_LENGTH * MAX_NUMBER_OF_BYTES_PER_UTF_8_CHAR tells us that the
last block of this size in the file also contains the last line.
In the next step, we set the file pointer to the beginning of the last block. We read in a byte array and
convert it to a string after UTF‑8 encoding. If the last line does not have the maximum length, then the
read block contains the remains of the previous line or lines with end‑of‑line characters as well as the last
line. For extracting the last line, we can use lastIndexOf(…), but the regular expression ([^\r\\n]*)$
gives us the last line much nicer. The regular expression is composed as follows:
1. The dollar sign at the end signals the end of the input.
2. The group in round brackets represents what we want to extract.
3. In the character class [^\r\n] the little hat stands for the negation that we want all charac‑
ters that are not end‑of‑line characters. [^\r\n]* with asterisk gives us a sequence of such
non‑line‑end characters.
• java.io.OutputStream
• java.io.InputStream
• java.io.Reader
• java.io.Writer
• java.nio.files.Files
• java.util.Scanner
• java.lang.Readable
• java.lang.Appendable
• java.io.IOException
• java.io.DataInputStream
• java.io.DataOutputStream
• java.io.FilterInputStream
• java.io.FilterOutputStream
• java.io.FilterReader
• java.io.FilterWriter
• java.io.DataInput
• java.io.DataOutput
• java.util.zip.GZIPOutputStream
• java.io.ByteArrayOutputStream
• java.io.InputStreamReader
• java.io.OutputStreamWriter
• java.io.PrintWriter
• java.lang.AutoCloseable
• java.io.BufferedOutputStream
• java.util.zip.CheckedOutputStream
• java.io.Serializable
• Write a method long distance(Path file1, Path file2) that returns the number of
different characters. In computer science, this is called Hamming distance.
• It is assumed that the two files are the same length.
The distance is three as the three symbols do not align, referring to the Hamming distance.
<!DOCTYPE html>
<html><body>
<svg width="256" height="256">
<rect x="10" y="10" width="1" height="1" style="fill:rgb(0,29,0);" />
</svg>
</body></html>
In a book about computer‑generated art, Captain CiaoCiao finds an illustration on the first few pages. The
pattern is generated by a Python program:
for x in range(256):
for y in range(256):
drawingTool.point((x, y), (0, x^y, 0))
del drawingTool
image.save("xorpic.png", "PNG")
The Python function point(…) gets the x‑y coordinate and RGB color information, where the three
arguments 0, x^y, and 0 represent the red, green, blue components, respectively.
Exercise:
• Since Captain CiaoCiao dislikes snakes, the Python program must be converted to a Java
program.
• Instead of a PNG file, end up with an HTML file with an SVG block where each pixel is a 1 × 1
SVG rectangle.
Bonus: at the end, open the HTML file with the browser—the desktop class will help you here.
The encoding of the numbers in dashes is as follows, where the underscore _ symbolizes the spacing by
a space:
Exercise:
Example:
• Open a text file, read each character, convert it to lowercase, and write it to a new file. Write
a method that does this and call it convertFileToLowercase(Path inPath, Path
outPath).
P3
3 2
255
255 0 0
0 255 0
0 0 255
255 255 0
255 255 255
0 0 0
214 Java Programming Exercises
There are various tokens separated by white space. We define the following rules:
Exercise:
For conversion to grayscale value, the following interface and constant can be used:
Java provides a mapping from (int, int) to an int with the IntBinaryOperator, but there is no
functional interface that has three parameters.
Although the average method for converting a color image to grayscale is efficient, it may not accu‑
rately reflect how humans perceive color. To create a more realistic grayscale image, it is necessary to take
into account that people perceive colors differently. One popular method for doing this is the luminosity
method, which takes into account the relative contributions of each color channel to perceived brightness.
Specifically, the luminosity method assigns weights to each color channel (red, green, and blue) based on
their perceived contribution to luminance, with red given a weight of 0.21, green given a weight of 0.72,
and blue given a weight of 0.07. By combining these weighted color channel values, the luminosity method
produces a grayscale image that more closely matches how people perceive the original color image.
The interface IntUnaryOperator can be used very well for mapping a grayscale value (int) to
an ASCII character (char, expanded to int). A default converter may look like this:
The given string1 is 64 characters long. Basically, this means black becomes @, and white becomes a space.
8 • Input/Output Streams 215
Example:
• Write a program that is passed a file name on the command line and then splits that file into
numerous smaller parts.
Example:
If the file Hanjaab.bin is 2440 KiB in size, then the Java program will turn it into the files
Hanjaab.bin.1 and Hanjaab.bin.2 with sizes 1440 KiB and 1000 KiB.
NESTING STREAMS
Streams can be nested like Russian dolls; one stream is the actual resource in the core, and other streams
are wrapped around it like a hull. Operations that go through the wrappers eventually go into the core.
• Create a compressed file with numbers from 0 to < N written to a GZIPOutputStream using
writeLong(…).
• Compare the file sizes for different N.
• At which N is compression worthwhile?
SERIALIZATION
Java uses serialization to allow object states to be written to a data stream, and then later to recreate the
object from a data stream; this process is called deserialization.
To convert Java objects to a binary stream and vice versa, the classes ObjectOutputStream and
ObjectInputStream are used; all object types to be serialized must be Serializable. We will use
the types in the next exercises and see practical examples of serialization.
Both classes are typical decorators: when serializing, the ObjectOutputStream determines
the data and writes the serialized byte sequences to the OutputStream specified in the construc‑
tor—when reading, it is the other way around, here the ObjectInputStream reads from a passed
InputStream.
To convert binary data into a string and vice versa, the Base64.Encoder and Base64.Decoder and
especially the wrap(…) method can help.
8 • Input/Output Streams 217
class Inputs {
public static class Input {
String input;
}
public List<Input> inputs = new ArrayList<>();
}
But now every user input should be stored in the file system so that at startup the application displays the
input made.
Exercise:
for an additional instance variable. Restart the program: what happens or doesn’t happen?
218 Java Programming Exercises
SUGGESTED SOLUTIONS
public static long distance( Path file1, Path file2 ) throws IOException {
if ( filesize1 != filesize2 )
throw new IllegalStateException(
"File size is not equal, but %d for %s and %d for %s".formatted(
filesize1, file1, filesize2, file2 ) );
long result = 0;
return result;
}
One important requirement is equal file size. Therefore, the program first retrieves the file sizes, compares
them, and if they do not match, an IllegalStateException follows. The error message is very pre‑
cise and conveys which file has which size, so outsiders can easily understand the error.
Assuming successful completion of the previous step, we will proceed to construct two resources for
the corresponding files. We call the Files method newBufferedReader(…) for a Reader. There
are two reasons for this method: first, we want to process strings and not binary streams, hence the
Reader and not an InputStream. Second, buffering is important for performance reasons, and new‑
BufferedReader(…) returns a Reader with an internal buffer. Individual characters are read from
the internal buffer, and there is no file system access for each character, which would be slow.
Since we already know the number of characters, a loop runs and asks for one symbol from each of
the two streams. If the symbols do not match, we increment a counter, which we return at the end.
The try‑with‑resources closes the two streams again, even if there should be an error in processing.
The method does not handle exceptions, but passes them on to the caller. Errors can occur when request‑
ing the file size, opening the file, and reading the character.
8 • Input/Output Streams 219
printer.println( "</svg></body></html>" );
}
Desktop.getDesktop().open( new File( filename ) );
}
catch ( IOException e ) {
e.printStackTrace();
}
}
The Java and Python languages are entirely unique, and the libraries vary as well. Therefore, there is little
in common in the code, almost everything is different.
There are several ways to write files in Java. The common classes are: FileOutputStream,
FileWriter, PrintWriter, and Formatter. Types based on OutputStream are omitted
because we would rather not write bytes, but Unicode characters. Since format strings are quite useful,
FileWriter is dropped, and Formatter is left out because it can only write formatted strings, but not
just strings without format strings.
Since something can always go wrong with input/output, the Java methods throw exceptions that we
have to handle. This is what the first try block takes care of. It catches every IOException.
Files are resources that need to be closed; therefore, the creation of the resource is also put into a
try‑with‑resources block. This particular block does not have a catch branch because it is supposed
to try‑with‑resources only to close the resource again at the end—any error handling is handled by the
outer try‑catch block.
First, we build a BufferedWriter, then we decorate it with a PrintWriter so that we also have
a method for writing formatted strings.
The next step is to write the prologue of the HTML file. In the two nested loops, printf(…) writes
the SVG rectangle to the data stream. The three values in Python are the color values for RGB, where the
red and blue parts are 0, so they remain unused. The program writes only the green part, as XOR of the
coordinates x and y. The value ranges of x and y are between 0 and 255, and this also happens to be the
maximum value for the 8‑bit RGB color values.
After passing through the two loops, the try‑with‑resources block closes the open stream. The
fact that the two try blocks are so strangely nested at first sight is because after the end of a success‑
ful write, the file is to be opened with a browser. However, we have to consider two peculiarities: The
220 Java Programming Exercises
try‑with‑resources must first write and close the file before we are allowed to reopen it for viewing. And,
we are only allowed to open the file if it was written without errors. If there was an error while writing,
then there must be no opening of the file. This logic converts these two nested write blocks.
To solve the task, we need to loop a String character by character and map the character to the symbol
sequence. There are different approaches. For example, we could compare the digit to a switch‑case
and then write the corresponding string to the writer. Another solution offers a map, which we can build
up beforehand with a composite of characters with the target code. The proposed solution shown here uses
an array, where the entries correspond exactly to the corresponding target codes of this position.
A switch‑case can make a case distinction directly on the char, but for an index on the array
we need the numeric value; here Character.getNumericValue(…) helps. The big advantage of this
method is that it works for all digits in all languages. A valid result is in the value range between 0 and 9;
with this number, you can access the array and then write the value into the writer. If we have not yet
reached the last digit in the input string, two spaces are written as separators.
The listing contains comments for the array, which show well that the dashes and spaces are in prin‑
ciple nothing else than a binary representation of the number. An anomaly is the number 7, which is not
represented as the predictable bit pattern 0111, but with 1010, i.e., symbolically _ | _ |; 1010, however,
would be the bit pattern for the number 10. If | _ _ were to represent 7, too much white space would be
involved, which could irritate readers—again, the underscore is symbolic of space.
If we interpret the number as a bit pattern, then a slightly different solution can be programmed that
does not require an array:
com/tutego/exercise/io/Zielcode.java
As usual, we iterate through the string, initially checking for the presence of a 7 at each position. If
found, we replace the digit with the string "10". If the digit is not a seven, we extract a string of length
1 containing only the current character using substring(…). The result in both cases is a string. This
string is passed into the BigInteger constructor for initialization. BigInteger has a handy method
testBit(…) which answers with true or false whether a bit is set at a position or not. To complete
the task, we simply need to retrieve the values of bits 3, 2, 1, and 0, and based on these values, set either a
space or a vertical bar. Although this differs from the task requirements, we will output the results directly
onto the screen.
The proposed solution first declares a private static variable EOF, which we will use later because we run
through the file character by character and ‑1 signals that there are no more characters in the stream.
The actual method convertFileToLowercase(…) is overloaded once with the parameter type
String and once with the parameter type Path. The variant with the filenames creates Path objects
and delegates to the actual conversion, to the second method.
Given a Path for the input file and a Path for the output file, we can use the Files methods to
request a Reader and Writer. Both objects have the nice property that they buffer automatically, so
character‑by‑character processing is much faster than if Reader and Writer were not buffered. When
reading, BufferedReader first creates an 8 KiB buffer, which is then filled to the maximum. Reading
of single characters takes place from this buffer first. When writing, the same applies: first all data are
222 Java Programming Exercises
collected in an internal buffer and when the buffer is full, the BufferedWriter writes the data of the
buffer into the output stream below.
The for loop declares a variable c for the character to be read. In the condition expression of the
for loop, the program first reads a character and assigns the result to the variable c; in the next step, it
compares with EOF. The loop runs as long as characters can be read. In the body of the loop, the character
is converted to an uppercase letter and written to the Writer.
class PPM {
private PPM() { }
Appendable output )
throws IOException {
// Matrix
for ( int y = 0; y < height; y++ ) {
for ( int x = 0; x < width; x++ ) {
int r = nextIntOrThrow( scanner,
"End of file or wrong format for red value" );
int g = nextIntOrThrow( scanner,
"End of file or wrong format for green value" );
int b = nextIntOrThrow( scanner,
"End of file or wrong format for blue value" );
int gray = rgbToGray.toGray( r, g, b );
output.append( (char) grayToAscii.applyAsInt( gray ) );
}
output.append( '\n' );
}
}
Since the class has only static methods, a constructor is not necessary, and it is set privately. The class
does not store any object states.
For retrieving consecutive tokens, the class Scanner is useful. Two kinds of errors can occur: data
can be missing in the stream, or the data type is wrong. Two helper methods nextStringOrThrow(…)
and nextIntOrThrow(…) simplify the reading of strings and integers, respectively, and raise an excep‑
tion if there is no token in the stream. The method for reading integers also checks whether the number is
incorrectly negative, and also throws an IllegalStateException in that case.
Accessible from the outside are the two overloaded methods renderP3PpmImage(…). Let’s start
with the entire method first, which has four parameters:
224 Java Programming Exercises
1. A Readable input.
2. A RgbToGray mapping for converting RGB values to grayscale values.
3. An IntUnaryOperator mapping for converting grayscale values to ASCII values.
4. An Appendable output destination for writing the resulting ASCII art.
It is possible for the method to throw an IOException due to the inherent risks of input/output opera‑
tions, which may result in errors during reading and writing.
The Scanner is connected to the Readable, which is the source from which it can read data. We
fetch a token and expect a special header, P3. This is the only use of the nextStringOrThrow(…)
method.
After reading the header, the height and width must follow. They must not be negative; however, the
assignment 0 will not lead to an error, we want to allow that. Afterward, the largest possible color value is
read in, which according to our definition must always be 255. In principle, the standard allows arbitrary
values, but we simplify this.
Once we have the height and width, we can write two nested loops, each reading the three color
tones. In principle, one loop would also suffice, but the program may want to refer to the x/y coordinates
of the points later. After reading the RGB values, the converter function is called, and the grayscale
tone is created, which then becomes the ASCII character via the next mapping. The result from the
IntUnaryOperator is an int, which we convert to a char and write to the output stream. At the end
of the line, we write a new line.
The second method, renderP3PpmImage(…) accesses the default implementations of the two
mappings. Users of the library can choose to use the default converters or pass in their own images.
private static void splitFile( Path source, int size ) throws IOException {
Objects.requireNonNull( source );
Objects.checkIndex( size, Integer.MAX_VALUE );
if ( args.length == 0 ) {
System.err.println( "You need to specify a file name to split the file." );
return;
}
8 • Input/Output Streams 225
try {
String filename = args[ 0 ];
splitFile( Paths.get( filename ), 1_474_560 );
}
catch ( IOException e ) {
System.err.println( e.getMessage() );
}
}
If we later work via the read(…) method, it will return ‑1 if no new bytes can be read. For this, we
introduce a constant EOF.
Our splitFile(…) method takes a path to the file and the size. The path could be null and the
index negative. Although an exception would be thrown later because of this, we want to check the cor‑
rectness beforehand. Here we turn to two static methods of the Objects class.
If there are input/output exceptions in the following, splitFile(…) does not catch them—what
should also the handling look like?—but passes them upward. There may be exceptions when opening the
file, reading the contents, and writing.
In the first step, we open the file for reading. Since the file is to be processed byte by byte,
we obtain an InputStream. This is a resource that the program has to close at the end in any
case, so try‑with‑resources is used. Splitting can be done in two different ways: one possibility
would be to open an OutputStream, read bytes from the InputStream, and write them to the
OutputStream; this would be very memory efficient. The other option is chosen here and saves
some program code, but bears the risk of an OutOfMemoryError because the solution reads in an
entire byte array in one go, and this array is as large as the passed size. However, with the intended
size of a floppy disk, this is not to be expected, and reading into the buffer and writing directly gives
a good performance.
The size of the array is the size byte. We use the array repeatedly in the loop. In the loop, we declare
two variables, once a counter for a generated file extension and a variable remaining for the number of
actually read bytes from the input stream. The actual reading is implemented in the condition part of the
for loop, and after the reading, we get the variable remaining updated, which is either ‑1 or contains
the number of bytes read.
In the body of the loop, the byte array is written to the file. Files.newOutputStream(…) returns
an output stream. The first argument to the method is a generated Path object that takes the file name and
appends a counter after the dot. Using the the OutputStream’s method write(byte[], int, int),
the populated portion of the array is written to the file. If the loop is run multiple times, the byte array is
guaranteed to be filled by the second to last run. In the last pass, probably not size many bytes are read,
so fewer bytes of the array must be written, it is always remaining <= size.
The main(…) method checks if an argument was passed on the command line, and if so, file‑
Split(…) is called with the argument. We write exceptions to the error output channel.
method from the InputStream, OutputStream, Reader or Writer from the ground up. To under‑
stand this in more detail, let’s take a look at the implementation of FilterOutputStream:
OpenJDK’s implementation of FilterOutputStream
package java.io;
@Override
public void write(int b) throws IOException {
out.write(b);
}
@Override
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte b[], int off, int len) throws IOException {
if ((off | len | (b.length ‑ (len + off)) | (off + len)) < 0)
throw new IndexOutOfBoundsException();
The code highlights that only the write(int) method forward data to the underlying stream, whereas
the other two write(…) methods are simply wrappers that invoke the write(int) method. Custom
filters are only obligated to override the write(int) method. However, for optimal performance, it is
advisable to implement the other methods as well, since writing entire byte arrays in a single operation is
faster than performing individual write operations for each element in the array.
DataInputStream and DataOutputStream are special filter classes, as the UML diagram
shows in more detail at method level for DataInputStream; in addition, they implement DataInput
and DataOutput.
8 • Input/Output Streams 227
java.io
InputStream
+InputStream()
+available(): int
+close(): void
+mark(readlimit: int): void
+markSupported(): boolean
+nullInputStream(): InputStream
+read(): int
OutputStream +read(b : byte[]): int
+read(b : byte[], off: int, len: int): int
+readAllBytes(): byte[]
+readNBytes(b: byte[], off: int, len: int): int
+readNBytes(len: int): byte[]
+reset(): void
+skip(n: long): long
+skipNBytes(n: long): void
+transferTo(out: OutputStream): long
«interface»
DataInput
DataInputStream
+DataInputStream(in: InputStream)
+read(b: byte[]): int
+read(b: byte[], off: int, len: int): int
+readBoolean(): boolean
+readByte(): byte
+readChar(): char
+readDouble(): double
+readFloat(): float
DataOutputStream +readFully(b: byte[]): void
+readFully(b: byte[], off: int, len: int): void
+readInt(): int
+readLong(): long
+readShort(): short
+readUnsignedByte(): int
+readUnsignedShort(): int
+readUTF(): String
+readUTF(in: DataInput): String
+skipBytes(n: int): int
The abstract superclasses InputStream and OutputStream work only with byte and
byte arrays, just as the superclasses Reader and Writer handle only the data types char, char
arrays, String or CharBuffer. The special feature of the classes DataInputStream and
DataOutputStream is that they also provide methods for other primitive data types. Thus, integers
or floating‑point numbers can also be read and written. This is a typical example of a decorator that pro‑
vides a more powerful API and goes for the simple methods in the background. The implementation of
readInt() is a good example of how this works:
OpenJDK’s implementation of readInt() from DataInputStream
final int n = 4;
Files.delete( tempFile );
For example, we want to avoid creating a file in the current directory, but in the temp directory of the
operating system. This should be deleted periodically, and therefore we try in Java to delete the tempo‑
rary file again at the end of the program. After initializing the constant n, which we can easily change
later, for example, we build up to three streams, which are nested. Since all these streams are of type
8 • Input/Output Streams 229
AutoCloseable, we use a try with resources. The nested streams are like nested rings: in the inner‑
most ring is the output stream, which writes to files. Around it is a stream that compresses: everything
written to the GZIPOutputStream is compressed and then written to the file stream. The last ring is
the decorator, with a more powerful API. Therefore, the data type in the try‑with‑resources is no longer
OutputStream, but DataOutputStream because it has the desired writeLong(long) method.
DataOutputStream wraps around the compressing data stream. If we write something into the
DataOutputStream, the data are passed to the GZIPOutputStream. The GZIPOutputStream
passes the data to the output stream so that it is written.
In the loop, we write n numbers. In the end, we calculate how big the file would be if the data were
not compressed. We don’t need to create an actual file to do this; we can easily calculate the file size. To
get the compressed size, we resort to the Files.size(…) method; another solution would have been to
count the number of bytes flowing through the streams right away—but we didn’t do that here.
The uncompressed size of the file would be 8,000,000 bytes, the compressed size of 2,129,303 bytes.
Since the data are written as long, many bits are 0. Generated are the bit patterns (the last number stands
for 999999, spaces separate the byte blocks):
The compressed file is about a quarter of the original size. Compression of this particular sequence is
worthwhile for four or more long elements.
The mapping succeeds, or there is an exception, which is caught and terminates the processing as
IllegalStateException, a runtime exception. If the ByteArrayOutputStream contains the
data, toString(…) returns the result. The resulting strings consist of pure ASCII characters, so US _
ASCII encoding can be used.
The reverse step turns a string into an object.
com/tutego/exercise/io/ObjectBase64.java
We need to generate an InputStream from the String so that the ObjectInputStream class
can be used. This is a bit of a problem because Java does not provide a natural way to use the String
as a source for an InputStream. Open‑source libraries such as Google Guava or Apache Commons
have solutions here, for example, in the form of the Apache class CharSequenceInputStream.
Java inherently offers only the other direction, for example with an InputStreamReader, which
adapts an InputStream into a Reader, not lets a Reader be represented as an InputStream.
Therefore, a StringReader, which is usually used when a string must appear as a Reader, does not
help us either.
The chosen solution converts the string into a byte array. This is not satisfactory, since the input
is run twice, once by the conversion and another time by reading from the stream. On the other hand,
this should have little practical relevance, and the bit of extra memory is not a burden for our use case.
After converting to a byte[], ByteArrayInputStream creates the desired InputStream.
The input stream consisting of ASCII characters becomes a byte stream via the decoder of Base64.
The InflaterInputStream unpacks the data, and finally the ObjectInputStream reconstructs
the object via readObject(). The serialized stream contains an identifier in which the data type is
to be reconstructed. This data type could in principle not exist on this virtual machine, which is why a
ClassNotFoundException is thrown in this case. Just like a possible IOException we catch these
checked exceptions and create an unchecked exception.
The mechanism for serialization traverses an object graph recursively, and all elements must be serializ‑
able. In our example:
1. Serialize Inputs. Is the class Serializable? Yes, then serialize the ArrayList inputs.
2. Serialize ArrayList. Is the class Serializable? Yes, then serialize internally the array of
inputs entries.
3. Serialize Input. Is the class Serializable? Yes, then serialize the instance variable
String input.
4. Serialize String. Is the class Serializable? Yes, then serialize the strings.
Primitive data types are serialized automatically, regardless of their visibility. However, static variables,
including those marked as transient, are not serialized. Certain fundamental Java types, such as
String, are inherently Serializable. Additionally, arrays and enumerations can also be serialized.
There is an exception that reports an incompatible serialVersionUID. The background is the follow‑
ing: each class has an identifier, the serial version UID. This UID (Unique Identifier) is either statically
fixed in the class, or it is dynamically calculated. Since an own UID is not available in the class from
the task, the serializer calculates the UID similar to a hash code, only not from the allocations, but from
the types. This happens on read and write; if an object is serialized, this UID is also written to the data
stream. When reading, the deserializer checks whether the UID in the data stream matches the UID of the
class. If there were structural changes, for example, the change of a data type, the dynamic UID changes.
This is precisely what the exception indicates. The two values represent the UID from the data stream and
the calculated UID of the changed class.
If structural changes are not supposed to lead to an exception, a UID must be set manually in the
code. This brings us to the proposed solution.
com/tutego/exercise/io/InputHistory.java
Inputs and also the nested class Input both contain the private static serialVersionUID. The
actual value assigned to this field is not significant. The serialver tool included with the JDK generates
232 Java Programming Exercises
the same UID that is written to the data stream even if serialVersionUID is missing. If there is
a serialVersionUID and in the data stream the UID matches that of the class, deserialization is
more relaxed: unknown attributes in the data stream are ignored, and attributes where the data type has
changed are also skipped.
CharSequence is an interface, and interfaces do not usually extend Serializable. Since type
checking occurs at runtime and String implements the Serializable interface, there is no error.
Nevertheless, String and CharSequence result in different UIDs.
The main program is embedded in the class InputHistory. The constructor of the class reads a file
and deserializes the input. Another method, addAndSave(…), updates Inputs and serializes the result
to a file. The main(…) method ties everything together.
com/tutego/exercise/io/InputHistory.java
InputHistory() {
try ( InputStream is = Files.newInputStream( FILENAME );
ObjectInputStream ois = new ObjectInputStream( is ) ) {
inputs = (Inputs) ois.readObject();
inputs.inputs.forEach( input ‑> System.out.println( input.input ) );
}
catch ( IOException | ClassNotFoundException e ) {
inputs = new Inputs();
e.printStackTrace();
}
}
The class has two instance variables: the file name and a reference inputs to the inputs. The construc‑
tor opens an InputStream for the file and initializes the ObjectInputStream with it. readOb‑
ject(…) starts deserialization, and if there is an exception, it is caught and a new Inputs object is built.
If Inputs could be reconstructed, all strings are output via the forEach(…) method of the list.
The addAndSave(String) method creates a new Inputs object, sets the passed string into
this object, and appends the new Inputs object to the inputs list. Then the list is serialized via the
ObjectOutputStream. Errors should not occur unless there are file system problems.
The main(…) method creates an InputHistory object, which activates the constructor that dese‑
rializes the file. At the very first start, this file does not exist, an exception is thrown, but the program
is not aborted. In the following, the file is created and grows by console input and saving. At the next
program start, the deserialization should work and the last entered strings should appear on the screen.
NOTE
1 The string is a simplification of https://www.pouet.net/topic.php?which=8056&page=1.
Network
Programming 9
Access to the network is as common nowadays, just like access to the local file system. Since Java 1.0 Java
offers a network API for developing client–server applications. The Java library can establish encrypted
connections and also bring support for the Hypertext Transfer Protocol (HTTP). The exercises in this
chapter are about getting resources from a web server and developing a small client–server application
with its protocol.
Prerequisites
• java.net.URL
• java.net.Socket
• java.net.ServerSocket
• javax.net.SocketFactory
• javax.net.ServerSocketFactory
• java.net.DatagramSocket
• For a given URL, write a program that downloads the resource and stores it on the local file
system.
• The file name should be based on the URL.
FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Dea
ths,Recovered,Active,Combined_Key,Incidence_Rate,Case‑Fatality_Ratio
,,,Afghanistan,2020‑10‑22
04:24:27,33.93911,67.709953,40510,1501,33824,5185,Afghanistan,
104.06300129769207,3.7052579609972844
,,,Albania,2020‑10‑22 04:24:27,41.1533,20.1683,17948,462,10341,
7145,Albania,623.6708596844812,2.574102964118565
,,,Algeria,2020‑10‑22 04:24:27,28.0339,1.6596,55081,1880,38482,
14719,Algeria,125.60932701190256,3.4131551714747372
Task:
Example:
If the CORONA numbers go down, CSSE may stop publishing new documents. The old documents should
stay.
HTTP CLIENT
Although URLConnection can be used to make an HTTP request, changing the HTTP method (GET,
POST, PUT …) and setting headers, this is not comfortable. Therefore, the HTTP client was added in
Java 11. With the new API, HTTP resources can be obtained more elegantly over the network. Moreover,
the HTTP client supports HTTP/1.1 and HTTP/2 as well as synchronous and asynchronous programming
models. In the next chapter on file formats, we will also return to the HTTP client in the exercises, since
JSON or XML is often exchanged.
Task:
Example:
• The class with the two methods can be used like this:
System.out.println( Arrays.toString( hackerNewsTopStories() ) );
String newsInJson = news( 24857356 );
System.out.println( newsInJson );
9 • Network Programming 237
Objects of Socket and ServerSocket are created via the constructor or even better via the facto‑
ries javax.net.SocketFactory and javax.net.ServerSocketFactory; for UDP there is no
factory.
Example:
• After starting the server and client, an interaction may look like this:
sir
You, sir, are an oxygen thief!
an
You, sir, are an oxygen thief!
Stop trying to be a smart ass, you’re just an ass.
• Write a program that tries to register a ServerSocket and DatagramSocket on all TCP/
UDP ports from 0 to 49151; if it succeeds, the port is free. Otherwise, it is busy.
• Display the occupied ports on the console, and in addition, for the known ports, a description
of the usual service that occupies that port.
238 Java Programming Exercises
Example:
A network interface connects computers via a computer network. In the following, we always assume a
TCP/UDP interface. The network interface does not have to be physical, but can also be implemented in
software. Like the loopback interface with the IP 127.0.0.1 (IPv4) or ::1 (IPv6). Typical network inter‑
faces continue to exist for the LAN or WLAN. Operating system tools can display all network interfaces,
such as ipconfig /all under Windows or ip a under Linux. In Java, the network interfaces can be
retrieved via java.net.NetworkInterface. Each network interface has its IP address.
To register a server socket there are two possibilities: either the service only accepts requests from a
special local InetAddress or the service accepts requests from all local addresses. So, in principle,
it is possible to bind the same port several times on one network card, because on one network card any
number of network interfaces can be configured because they have distinguishable IP addresses.
The solution of the task can perform a simple test and register the socket on all network interfaces; if
this fails, a service was already active on one of the network interfaces. This is enough for us as a criterion
that on some network interface the port is busy.
SUGGESTED SOLUTIONS
1. The URL object returns an InputStream for the bytes of the resource with openStream().
Since you should close what you open, we put the opening of the stream in a try‑with‑resource
block.
2. The name of the target file is derived from the URL. We have to be a bit careful with the file‑
name because not every character in the URL is always a valid character for a filename. The
Wikipedia page https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations
summarizes the special characters for some file systems. Furthermore, not all characters may
appear in a URL, but even the simple path separator / will cause problems. Therefore, we
replace all problematic characters. The regular expression [^a‑zA‑Z0‑9 _ .‑] chosen in the
solution replaces all characters that are not letters, digits, _ , . or ‑ with an underscore. This
gives a safe filename for each file system. However, the URLs are not unique because, for
example, the URLs https://www.penisland.net/?; and https://www.penisland.
net/?, would become http___www.penisland.net___.
3. The second step elegantly uses the copy(…) method of the Files class. There are two of these:
one for reading from a file and writing to an OutputStream, and one for reading all data from
an InputStream and writing to a file—this is the version we use. All bytes are read from
the InputStream and written to the new destination file. The third parameter at copy(…) is
a vararg and stands for attributes: StandardCopyOption.REPLACE _ EXISTING states,
that existing files will be overwritten, otherwise there will be an exception at existing files.
The first step is to build the URL. The URL contains the date, which we receive as a parameter. However,
it is not possible to call toString() method directly on the LocalDate object because that returns a
string according to the ISO 8601 notation; the smart Center for Systems Science and Engineering uses a
different order when specifying the date segments. The program, therefore, builds up its own date format‑
ting with the DateTimeFormatter by placing the month first, followed by the day, and then the year.
From this dynamically generated string, a URL object is built and the central method openStream()
is called, which returns an InputStream. Since we want to read character by character, we convert this
binary InputStream into a Reader. For line‑by‑line reading, the BufferedReader is well suited,
in particular, the lines() method is handy, which returns a Stream<String>. Using a filter, we leave
the lines in the stream that contain the search string, and finally concatenate all the lines into one big
string, which we return.
Two possible exceptions are possible: the URL may be formed incorrectly, in which case there is a
MalformedURLException, or there are connection errors or errors during the read. We catch these excep‑
tions, print messages on the screen, and return an empty string since nothing was found even in the error case.
try {
HttpResponse<InputStream> response =
client.send( request, HttpResponse.BodyHandlers.ofInputStream() );
var scanner = new Scanner( response.body() ).useDelimiter( "[,\\[\\]]" );
return scanner.tokens().mapToLong( Long::parseLong ).toArray();
}
catch ( IOException | InterruptedException e ) {
e.printStackTrace();
return new long[ 0 ];
}
}
com/tutego/exercise/net/HackerNews.java
try {
return client.send( request, HttpResponse.BodyHandlers.ofString() )
.body();
}
catch ( IOException | InterruptedException e ) {
e.printStackTrace();
9 • Network Programming 241
return "";
}
}
For our example, we create a HttpClient as a class variable. Since our two methods are static, it is
convenient to preconfigure the object. If object methods should access the HttpClient, and this still
from several threads, then each thread should use its own HttpClient object.
The send(…) method has two parameters:
The first thing to do is to build and configure an HttpRequest for the individual request. This can be done
with HttpRequest.newBuilder() and HttpRequest.newBuilder(URI); the second version saves
the later call to uri(…). The timeout is optional and is set to 5 seconds by the program. The final build()
method returns the HttpRequest object, which is executed by the HttpClient. This is the principle of
the API: different HttpRequest calls run over the HttpClient once it is built and configured.
The second parameter of the send(…) method determines how the result should be fetched. For
the following Scanner an InputStream is handy, which HttpResponse.BodyHandlers.ofIn‑
putStream() requests. From the HttpResponse<InputStream> response we read the content
with body(), where the body is not the data for now, but just an InputStream of that data. This
InputStream configures the Scanner. Encoding is not necessary in our case, because the JSON
document consists of pure ASCII characters. However, the Scanner is configured with a delimiter that
sees [ and , and ] as delimiters. The Scanner provides the useful method tokens() which gives us
a stream of all tokens, in our case the numbers. mapToLong(…) converts any textual representation of a
number into a LongStream, and toArray() returns all elements of the stream as an array. If there are
any errors, they are caught, reported, and an empty array is returned.
The second method, news(long id), builds the HttpRequest with a concrete ID for a message.
The implementation is a bit simpler because we don’t need to parse the output, and HttpResponse.
BodyHandlers.ofString() determines that a string is returned as the result.
JSON documents from web services will generally be converted to Java objects. We’ll look at how to
do this in the next chapter.
while ( ! Thread.currentThread().isInterrupted() ) {
Socket socket = serverSocket.accept();
242 Java Programming Exercises
The main(…) method prepares a server socket on the predefined port 10,000, enters an infinite loop that
can in principle be terminated with an interrupt, and waits for an incoming connection. The thread blocks
at accept(), and the block does not unblock until there is an incoming client. In that case, a lightweight
virtual thread will handle our handleCollection(…) method.
If accept() returns, the return is the client socket, and that becomes the argument of handle‑
Connection(…). We move the call to handleConnection(…) to a Runnable and that is passed to
the virtual thread, and so executed in the background. While the thread handles the client–server commu‑
nication in the background, the infinite loop goes back to accept() to quickly serve the next interested
party. At this point, we are not allowed to close the socket, so it must not say:
The processing is asynchronous! The close() from the try‑with‑resources would otherwise be right
after sending via execute(…) and consequently the Socket would be closed quickly while handle‑
Connection(…) has just started communicating.
Let’s have a look at handleConnection(…): a Socket is AutoCloseable, and the
try‑with‑resources closes the Socket at the end. We have the unusual case here that we don’t actually
need to declare a new resource variable, but the abbreviated notation is only allowed since Java 9.
In communication, input and output streams are necessary. These are also resources to be closed
via try‑with resources. A string is read from the input stream, searched for the insulting word, and
the result is written back. The protocol requires strings to go over the wire, so the InputStream and
OutputStream are upgraded to character‑oriented types. The Scanner can be built in the construc‑
tor with an InputStream and an encoding and can then read a line with nextLine(). We write the
output to a PrintWriter; again, the constructor can accept the OutputStream and the encoding.
The second argument true is important because it controls the flushing of the buffer on an end‑of‑line
9 • Network Programming 243
character. println (…) writes the result to the PrintWriter, the line feed signals the flushing of the
buffer. The catch block ends the try‑with‑resources, and all resources are closed and the socket is
returned to the operating system as a native resource.
The utility method searchInsult(…) checks if the search word is contained in the given strings,
and concatenates all results with a newline.
The client has a similar logic, but of course, it doesn’t have to listen for incoming connections, it
establishes them.
com/tutego/exercise/net/SlangingMatchClient.java
The main(…) method contains an infinite loop, asks the user for a string, and passes it to
remoteSearchInsult(String). The new method is responsible for communication with the server.
In the first step, the socket factory returns a socket object for localhost and the desired port.
Sockets are native resources that must be returned to the operating system at the end; closing them is
handled as usual by a try‑with‑resources block, which also closes the input/output streams.
Writing the string is again done by PrintWriter. The client uses a BufferedReader for reading,
since it has the advantage of providing a stream of lines with the lines() method. The lines read in
are joined with a Collector and printed. You cannot connect a Reader directly to an InputStream,
so the InputStreamReader decorator is needed to enable the Reader API on an InputStream.
com/tutego/exercise/net/PortScanner.java
enum Protocol {
TCP {
@Override AutoCloseable openSocket( int port ) throws IOException {
return ServerSocketFactory.getDefault().createServerSocket( port );
}
},
UDP {
@Override AutoCloseable openSocket( int port ) throws IOException {
return new DatagramSocket( port );
}
};
The Protocol enumeration type declares an abstract method openSocket(int) for opening the
connection, since for TCP and UDP the code is different; the two enumeration elements implement
the abstract method accordingly. In the case of TCP, the application builds a ServerSocket via the
ServerSocketFactory or just a DatagramSocket via the constructor of DatagramSocket.
Although ServerSocket and DatagramSocket are different types, both implement the
AutoCloseable interface, and openSocket(int) also returns this type because only this type is
relevant for isAvailable(int).
It is the task of isAvailable(int) to find out if the port is already in use. To do this, it calls
openSocket(…), and if there was no exception, the port was free, and the connection can be closed right
away; this is what try‑with‑resources on AutoCloseable takes care of.
serviceName(int) accesses a previously built Map. Inside the class, there is a constant
COMPRESSED _ SERVICE _ NAMES, which could easily come from a file. The string contains the port
number and, separated by spaces, a brief description, which in turn is terminated with a newline. Text
9 • Network Programming 245
blocks save us the line break at the end of the line. A Stream expression prepares the Map by break‑
ing the String into lines, this is where the lines() method is useful. The Stream then consists
of lines, which are passed into the constructor of the Scanner object, so that afterward nextInt()
returns the key for the Map and nextLine() the short description associated with the port number.
serviceName(int) can access this associative store SERVICE_NAMES and returns an empty string if
there is no description associated with the port number.
The main(…) method makes use of the methods of PortScanner:
com/tutego/exercise/net/PortScanner.java
First, we declare constants for the boundaries of the port ranges that our port scanner should run. They
represent the boundaries of the port ranges that our port scanner should run. In the code example, we
manage to use only two constants for the upper and lower limits because after MAX _ SYSTEM _ PORT
we continue with MIN _ REGISTERED _ PORT. We use a for loop to use all registered ports from 0
to 49151. Protocol.values() returns an array with the two enumeration elements TCP and UDP,
and if the isAvailable(…) method declared on the enumeration shows a blocked port, this prints the
console output.
Process XML,
JSON, and Other
Data Formats
10
Two important data formats for exchanging documents are XML and JSON (JavaScript Object Notation).
XML is historically the older data type, we often find JSON nowadays in communication between a server
and a JavaScript application. JSON documents are also popular for configuration files.
While Java SE provides different classes for reading and writing XML documents, JSON support is
only available in Java Enterprise Edition or through complementary open‑source libraries. Many of the
tasks in this chapter, therefore, resort to external libraries.
Description languages form a significant category of document formats. They define the structure of
the data. Among the most important formats are HTML, XML, JSON, and PDF.
Java does not provide support for other data formats, except for property files and the ability to
process ZIP archives. This is especially true for CSV (Comma‑separated values) files, PDFs, or Office
documents. Fortunately, dozens of open‑source libraries fill this gap, so you don’t have to program this
functionality yourself.
Prerequisites
• javax.xml.stream.XMLOutputFactory
• javax.xml.stream.XMLStreamWriter
• javax.xml.stream.XMLStreamException
• javax.xml.stream.XMLInputFactory
• javax.xml.stream.XMLStreamReader
• jakarta.xml.bind.JAXB
His recipes are in RecipeML format, an XML format that is loosely specified: http://www.formatdata.
com/recipeml/. There is a large database at https://dsquirrel.tripod.com/recipeml/indexrecipes2.html. An
example from “Key Gourmet”:
<ing>
<amt>
<qty>3</qty>
<unit>cups</unit>
</amt>
<item>Strawberries</item>
</ing>
<ing>
<amt>
<qty>3</qty>
<unit>cups</unit>
</amt>
<item>Sugar</item>
</ing>
</ingredients>
<directions>
<step>Put the strawberries in a pan.</step>
<step>Add 1 cup of sugar.</step>
<step>Bring to a boil and boil for 4 minutes.</step>
<step>Add the second cup of sugar and boil again for 4 minutes.</step>
<step>Then add the third cup of sugar and boil for 3 minutes.</step>
<step>Remove from stove, cool, stir occasionally.</step>
<step>Pour in jars and seal.</step>
</directions>
</recipe>
</recipeml>
Task:
• Implement an XHTML checker that reports whether each img tag has an alt attribute set.
• Take as XHTML file, e.g., http://tutego.de/download/index.xhtml.
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind‑api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
10 • Process XML, JSON, and Other Data Formats 249
<artifactId>jaxb‑runtime</artifactId>
<version>4.0.5</version>
<scope>runtime</scope>
</dependency>
Task:
The provider does not provide a schema, so it is generated from the XML using https://www.freeformat‑
ter.com/xsd‑generator.html.
Task:
• Load the XML schema definition at http://tutego.de/download/jokes.xsd, and place the file in
the Maven directory /src/main/resources.
• Add the following element to the POM file:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2‑maven‑plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>com.tutego.exercise.xml.joke</packageName>
<sources>
<source>src/main/resources/jokes.xsd</source>
</sources>
<generateEpisode>false</generateEpisode>
<outputDirectory>${basedir}/src/main/java</outputDirectory>
<clearOutputDir>false</clearOutputDir>
<noGeneratedHeaderComments>true</noGeneratedHeaderComments>
<locale>en</locale>
</configuration>
</plugin>
</plugins>
</build>
The plugin section includes org.codehaus.mojo:jaxb2‑maven‑plugin and
configures it; all options are explained at https://www.mojohaus.org/jaxb2‑maven‑plugin/
Documentation/v3.1.0/index.html.
10 • Process XML, JSON, and Other Data Formats 251
• From the command line, launch mvn generate‑sources. This will generate two classes
in the com.tutego.exercise.xml.joke package:
• Data.
• ObjectFactory.
• Use JAXB to get a joke from the URL https://sv443.net/jokeapi/v2/joke/Any?format=xml and
convert it to an object.
JSON
Java SE does not provide built‑in support for JSON, but there are two standards from the Jakarta EE
project that provide this support: Jakarta JSON Processing (JSON‑P) (https://jakarta.ee/specifications/
jsonp/) and Jakarta JSON Binding (JSON‑B) (https://jakarta.ee/specifications/jsonb/). JSON‑B allows for
the mapping of Java objects to JSON and vice versa, while JSON‑P provides APIs for processing JSON
data. Another popular implementation is Jackson (https://github.com/FasterXML/jackson).
To use JSON‑B, we need to add both the API and an implementation to our project’s POM. The refer‑
ence implementation, Yasson, is a good choice.
<dependency>
<groupId>jakarta.json.bind</groupId>
<artifactId>jakarta.json.bind‑api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>3.0.3</version>
<scope>runtime</scope>
</dependency>
{
"by":"luu",
"descendants":257,
"id":24857356,
"kids":[
24858151,
24857761,
24858192,
24858887
],
"score":353,
"time":1603370419,
252 Java Programming Exercises
• Write a new method Map<Object, Object> news(long id) that, using JSON‑B, obtains
the JSON document at "https://hacker‑news.firebaseio.com/v0/item/" + id + ".
json" and converts it to a Map and returns it.
Example:
HTML
HTML is an important markup language. The Java standard library does not provide support for HTML
documents, except for what the javax.swing.JEditorPane can do, which is to render HTML 3.2
and a subset of CSS 1.0.
For Java programs to be able to write and read HTML documents correctly and validly, and to be able
to read nodes, we have to turn to (open‑source) libraries.
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
Task:
OFFICE DOCUMENTS
Microsoft Office continues to be at the top when it comes to word processing and spreadsheets. For many
years, the binary file format has been well known, and there are Java libraries for reading and writing.
Meanwhile, processing Microsoft Office documents has become much easier since the documents are, at
their core, XML documents that are combined into a ZIP archive. Java support is excellent.
1. Add the following for Maven in the POM to include Apache POI and the necessary dependen‑
cies for DOCX:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
254 Java Programming Exercises
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.1</version>
</dependency>
2. Study the source code of SimpleImages.java.
3. Java allows you to capture screenshots, like this:
private static byte[] getScreenCapture() throws AWTException,
IOException {
BufferedImage screenCapture = new Robot().createScreenCapture( SCREEN_
SIZE );
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write( screenCapture, "jpeg", os );
return os.toByteArray();
}
4. Write a Java program that takes a screenshot every 5 seconds for 20 seconds and attaches the
image to the Word document.
ARCHIVES
Files with metadata are collected in archives. A well‑known and popular archive format is ZIP, which not
only combines the data but also compresses it. Many archive formats can also store the files encrypted and
store checksums so that errors in the transfer can be detected later.
Java offers two possibilities for compression: since Java 7, there is a ZIP file system provider, and
already since Java 1.0, there are the classes ZipFile and ZipEntry.
Task:
SUGGESTED SOLUTIONS
record Recipe(
String head$title,
List<String> head$categories,
String head$yield,
List<Ingredient> ingredients,
List<String> directions) {
record Ingredient(
String ing$amt$qty,
String ing$amt$unit,
String ing$item
) {}
}
256 Java Programming Exercises
A recipe is represented by the two types Recipe and Ingredient. Ingredient is a nested type,
which expresses well the relationship between the two types Recipe and Ingredient. In principle,
one could declare a separate record (or class) for each subelement, but this would be too much for the
proposed solution. Therefore, the variable names with the dollar express the type hierarchy.
Before we start with our program, one observation: many elements are written, which entails many
statements of the following type:
writer.writeStartElement( … );
…
writer.writeEndElement();
For this kind of problem, the execute‑around pattern is useful. A thought experiment:
We can pass the tag, a block representing the body, and at the end, we want to write the end tag. Since
the Java library does not provide such a feature, the proposed solution introduces a separate helper class
HierarchicalXmlWriter, a facade around the XMLStreamWriter:
com/tutego/exercise/xml/RecipeMLwriterDemo.java
interface XMLStreamWriterBlock {
void write() throws XMLStreamException;
}
The constructor takes the sink where the XML document will be written. The passed OutputStream
is stored in an instance variable so that it can be closed later as a resource. Furthermore, the
XMLStreamWriter is requested and saved via XMLOutputFactory so that the XMLStreamWriter
can also be closed in close(). Finally, the constructor writes the XML prolog.
The XMLStreamWriterBlock is AutoCloseable, so that use as a resource in
try‑with‑resources is possible. The close() method sets the end tag from the XML document, closes
the XMLStreamWriter and the OutputStream. Important: The XMLStreamWriter does not
independently pass the close() to the underlying resource, as is usually the case with input/output
decorators. The OutputStream should also be closed if an exception occurs when calling the two
XMLStreamWriter methods.
The first helper method element(String tag, XMLStreamWriterBlock block) sets the
start tag, executes the block, and writes the end tag. The second helper method string(String tag,
String string) writes the start tag, the text inside, and the end tag.
The main class RecipeMLwriterDemo can access HierarchicalXmlWriter and now build
the XML blocks as desired:
com/tutego/exercise/xml/RecipeMLwriterDemo.java
The Path object passed to the createXMLStreamReader(…) method is the basis for an
InputStream, which we pass to createXMLStreamReader(…) to get an XMLStreamReader with
this input stream. Unfortunately, to date (as of Java 21), an XMLStreamReader is not AutoCloseable,
so it cannot be closed in try‑with‑resources. However, this is not dramatic when reading; we close the
InputStream of the file very well via a try‑with‑resources.
Passing the data through an XMLStreamReader always looks the same: hasNext() tells whether
there are still tokens in the data stream, and if so, fetches the next token with next(). This is similar to
Scanner and Iterator. The call to next() changes the state of the XMLStreamReader element,
and getEventType() returns an integer to identify the incoming data. This can be e.g., the start of the
document, a processing instruction, a comment, text, or even a start element. Instead of integers, we use
constants, interface XMLStreamReader extends XMLStreamConstants. When an element
starts, it could be an img element. So getLocalName() asks the parser for the element name and com‑
pares it to img—case‑insensitive. If this is true, we have found an img tag. Now the question is whether
10 • Process XML, JSON, and Other Data Formats 259
the alt attribute is also set. This is answered by our method containsAltAttribute(…). If the img
tag has no alt attribute, there is a message on the standard error channel and via getLocation() the
exact location can also be identified and specified in the error message.
containsAltAttribute(…) gets the XMLStreamReader as parameter and runs all attributes
from 0 to getAttributeCount(). If an attribute alt exists, regardless of the assignment, the method
returns true, otherwise false.
@XmlRootElement
class Ingredients {
public Ing[] ing;
}
class Ing {
public Amt amt;
public String item;
}
class Amt {
public int qty;
public String unit;
}
com/tutego/exercise/xml/JaxbRecipeML.java
1. You write classes with a parameterless constructor and use either setters/getters or public
instance variables for the data.
2. Builds an object graph and writes it with JAXB.marshal(ingredients, System.out)
to an output stream, for example, to the console.
260 Java Programming Exercises
For compatibility reasons, the proposed solution sets the @XmlRootElement annotation to the root
element Ingredients. This is no longer necessary for current JAXB implementations but is used for
compatibility reasons so that the solution also works under Java 8, which contains a slightly older JAXB
version (JAXB RI 2.2.8), currently 2.4.0.
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"category", "type", "flags", "setup", "delivery", "id", "error"
})
@XmlRootElement(name = "data")
public class Data {
@XmlElement(required = true)
protected String category;
@XmlElement(required = true)
protected String type;
…
}
To the client:
com/tutego/exercise/xml/JaxbJokeReceiver.java
try {
URL url = URI.create( "https://sv443.net/jokeapi/v2/joke/Any?format=xml"
).toURL();
Data data = JAXB.unmarshal( url, Data.class );
System.out.println( data.getSetup() );
System.out.println( data.getDelivery() );
System.out.printf( "Not Safe for Work? %s%n", data.getFlags().isNsfw() );
System.out.printf( "Religious? %s%n", data.getFlags().isReligious() );
System.out.printf( "Political? %s%n", data.getFlags().isPolitical() );
System.out.printf( "Racist? %s%n", data.getFlags().isRacist() );
System.out.printf( "Sexist? %s%n", data.getFlags().isSexist() );
}
catch ( MalformedURLException e ) {
System.err.println( "malformed URL has occurred" );
e.printStackTrace();
}
catch ( DataBindingException e ) {
System.err.println( "failure in a JAXB operation" );
e.printStackTrace();
}
JAXB.unmarshal(…) allows constructing Java objects from an XML stream from various data
sources, including a URL. So if we build a URL object and put it on the endpoint of the joke, unmar‑
shal(…) directly returns a Data object. The Data object then provides different getters, and the
10 • Process XML, JSON, and Other Data Formats 261
data can be read. Two exceptions can occur: the format of the URL could be invalid, which gives us a
MalformedURLException, or the XML format cannot be mapped to the JavaBean, in which case the
result is a DataBindingException.
@SuppressWarnings( "unchecked" )
public static Map<Object, Object> news( long id ) {
String url = "https://hacker‑news.firebaseio.com/v0/item/%d.json";
In this solution, the fromJson(…) method takes an InputStream of an open network connection as
the first parameter and the target type, which is Map, as the second parameter. The Jsonb object is an
AutoCloseable and can be used with the try‑with‑resources block. The program catches exceptions
and returns an empty Map if an exception is thrown. Since the Class object can only express the raw
type Map but not the generics, implicit type conversion Map<String, String> is possible, but the
compiler would give a notice; a @SuppressWarnings("unchecked") stops the compiler warning.
The bext solution uses the HttpClient API, which is more powerful than the URL class way in
terms of configuring authenticator, thread pool, proxy, SSL context, and more.
com/tutego/exercise/json/HackerNewsJson.java
@SuppressWarnings( "unchecked" )
public static Map<Object, Object> news( long id ) {
HttpClient client = HttpClient.newHttpClient();
catch ( Exception e ) {
return Collections.emptyMap();
}
}
262 Java Programming Exercises
The program creates the Jsonb object without any extras and falls back to a default configuration with
HttpClient.newHttpClient(). The HttpRequest is set to the URL, and then a timeout is set. The
result should be in a format that fromJson(…) can accept; here InputStream or String is suitable,
for example. The InputStream is advantageous because it needs the least memory when reading.
import java.util.*;
enum FontWeight {
normal, bold
}
JSON‑B directly accesses the instance variables and takes the lowercase identifiers for the JSON object.
JSON‑B will directly map lists to JSON arrays. Enumerations are also directly written and can be read
back in. With Map<String, String> terminal arbitrary key‑value pairs of strings can be used,
which are not bound to special instance variables, but come into the associative memory.
com/tutego/exercise/json/EditorPreferences.java
• The output should be formatted, because configuration files are made for users, so the file
should not be as short as possible and save all spaces, but have breaks and inserts.
Internally, EditorPreferences creates a Settings object that reconstructs load() from the JSON
file and writes the save() method. fromJson(…) and toJson(…) of the Jsonb object are responsible
for the actual mapping.
The usage can be as follows:
com/tutego/exercise/json/EditorPreferencesDemo.java
The class Jsoup has the static method parse(…), which can build the HTML document from different
sources. In our case, we choose directly the URL object. When accessing the network, a timeout must be
given to Jsoup, which we set to 1000 milliseconds. The parse(…) method returns an org.jsoup.
nodes.Document object. There are two ways to access this Document and extract elements:
The proposed solution works with the select(…) method. img[…] stands for all img tags, while src~=
specifies via a regular expression what should be true for the src attribute, namely, that the strings match
\.(png|gif|jpg), i.e., have the file extension .png, .gif or .jpg. The (?i) flag activates the search
regardless of the case.
The result of the select(…) method is of type Elements, a subclass of ArrayList. A List is
Iterable and can be conveniently traversed with an extended for loop. Each element in this list is of
type Element. We could use the attr("src") call to get the set URL of the image, but more useful is
the absUrl(…) method, which resolves the URL.
If we later download the images, then we can’t directly use this URL as the filename because there
are illegal symbols there that cause problems in the file system. The string method replaceAll(…)
returns a new, cleaned‑up string that we can use as a filename. Since the allowed length of the file name
might be limited on the operating system, a block reduces the length to a maximum of 128 characters. The
next step is to build a URL object and open an input stream to this image and copy it to the local file system
via Files.copy(…); we have written the code before in the ImageDownloader task. However, this
time we put the images in a temporary directory.
The solution consists of three methods. The first method getScreenCapture() returns a byte[]
with the screen content as JPEG. Java can do this via the Robot class, which is intended for automation.
(The Robot class can be used to move the cursor and send keystrokes.) The result of createScre‑
enCapture(…) for the whole screen size is of type BufferedImage, an internal image format. To
convert it to JPEG format, the program resorts to the ImageIO.write(…) method, which first writes the
BufferedImage to a ByteArrayOutputStream and then converts it to a byte array and returns it.
The second method is appendImage(…); it appends an image to an existing XWPFDocument.
Since each image is placed in its paragraph, a Paragraph is built first and then the image is added via
addPicture(…). The method expects an input stream to the picture as well as a unique identifier, and
size information. The image is scaled a bit.
The main method main(…) opens a new XWPFDocument, then takes a screen capture, appends it
to the document, waits for a second, then takes another screen capture until the desired maximum number
is reached. The document is only in memory so far. Files.createTempFile(…) creates a file in the
temporary directory and writes the official document to this file.
while ( true ) {
266 Java Programming Exercises
TrueZIP uses its own Path implementation TPath for its work. The constructor can be passed a String,
Path, URI, or File object. If we use our ZIP file and have constructed TPath, newDirectoryS‑
tream(..) returns all directory contents, caching them in a list of Path objects.
The infinite loop starts and selects a random file from the list. An input stream is opened and then
decorated with BufferedInputStream. This is necessary because the audio system requires a special
feature on input streams, namely, that markers are supported. The input stream of TrueZIP does not sup‑
port this, at least in the current version.
Thereafter, the AudioInputStream can be opened and the Clip can be played. The duration of
the clip can be queried via getMicrosecondLength(…), and this is how long we wait after starting
playback. We add a small buffer of 50 microseconds on top. The clip is closed, and the next loop cycle
follows.
NOTE
1 https://jakarta.ee/specifications/jsonb/3.0/apidocs/jakarta.json.bind/jakarta/json/bind/jsonbconfig
Database Access
with JDBC 11
Java Database Connectivity (JDBC) provides access to various relational databases and enables the
execution of SQL statements on a relational database management system (RDBMS). A JDBC driver
implements the JDBC API. This chapter focuses on an example using the JDBC API to allow Captain
CiaoCiao to store user information in a database for a pirate dating service.
Prerequisites
• java.sql.DriverManager
• java.sql.Connection
• java.sql.SQLException
• java.sql.Statement
• java.sql.Statement
• java.sql.ResultSet
• java.sql.Date
• java.sql.ResultSetMetaData
Prepare H2 Database ⭑
H2 is such a compact program that the database management system, the JDBC driver, and a small admin
interface are bundled together in a JAR (Java Archive).
Include the following dependency in the Maven POM:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>runtime</scope>
</dependency>
DATABASE QUERIES
Each database access runs through the following stages:
Task:
• Using the DriverManager, query all logged‑in JDBC drivers, and output the class name to
the screen.
The first SQL statement deletes all entries in the database in H2. Then CREATE TABLE creates a new
table with different columns and data types. Each pirate has a unique ID assigned by the database; we
refer to automatically generated keys.
The SQL in the book follows a naming convention:
A Java SE program uses the DriverManager to establish a connection using the getConnection(…)
method. A JDBC URL contains details about the database and connection details, such as server and port.
In the case of H2, the JDBC URL is simple if no server should be contacted but the RDBMS should be
part of the own application:
If the database pirates‑dating does not exist, it will be created. getConnection(…) returns the
connection afterward. Connections must always be closed. The try‑with‑resources handles the closing,
as can be seen in the code above.
If the complete RDBMS runs as part of its application, this is called embedded mode. In embedded
mode, a started Java application uses this database exclusively and multiple Java programs cannot con‑
nect to this database. Multiple connections are only possible with one database server. H2 can do this
as well; those interested can learn the details from the H2 website: https://www.h2database.com/html/
tutorial.html.
270 Java Programming Exercises
Task:
• Put a file create‑table.sql in the resources directory of the Maven project. Copy the SQL script
into the file.
• Create a new Java class, and load the SQL script from the classpath.
• Establish a connection to the database, and execute the loaded SQL script.
The primary key id is explicitly absent from the statement because this column is automatically uniquely
assigned.
Task:
• Establish a new connection to the database, create a Statement object, and send the INSERT
INTO to the database with executeUpdate(…).
• A generated key can be supplied by a JDBC driver. To add a second pirate, you can output
the generated key (which is a long) to the screen. When executeUpdate(…) is called, it
returns an int. This indicates something about the executed statement, but what is it?
11 • Database Access with JDBC 271
• Create a new class and put the following array into the program:
String[] values = {
"'anygo', 'amiga_anker@cutthroat.adult', 11, DATE '2000‑05‑21',
'Living the dream'",
"'SweetSushi', 'muffin@berta.bar', 11, DATE '1952‑04‑03', 'Where are
all the bad boys?'",
"'Liv Loops', 'whiletrue@deenagavis.camp', 16, DATE '1965‑05‑11',
'Great guy'" };
• From the data in the array, create SQL‑INSERT statements, add them to the Statement with
addBatch(…), and submit the statements with executeBatch().
• executeBatch() returns an int[]; what is inside?
• Create a new class, and include the following declaration in the code:
List<String[]> data = Arrays.asList(
new String[]{ "jacky overflow", "bullet@jennyblackbeard.red", "17",
"1976‑12‑17", "If love a crime" },
new String[]{ "IvyIcon", "array.field@graceobool.cool", "12",
"1980‑06‑12", "U&I" },
new String[]{ "Lulu De Sea", "arielle@dirtyanne.fail", "13",
"1983‑11‑24", "You can be my prince" }
);
• Loop over the list data, fill a PreparedStatement, and submit the data.
• All insert operations should be done in one large transactional block.
272 Java Programming Exercises
Request Data ⭑
Our recent program inserted new rows in the database; it’s time to read them out!
Task:
• The ResultSet can not only be read, but also modified, so that data can easily be written
back to the database, and
• The cursor on the result set can not only be moved down with next(), but can also be arbi‑
trarily positioned or set relatively upward.
Captain CiaoCiao wants to scroll through all the pirates of the databases in an interactive application.
Task:
Pirate Repository ⭑⭑
Every major application relies on external data in some way. From the domain‑driven design (DDD), there
is the concept of a repository. A repository provides CRUD operations: create, read, update, and delete.
The repository is an intermediary between the business logic and the data store. Java programs should
only work with objects, and the repository maps the Java objects to the data store and, conversely, convert
the native data in, for example, a relational database to Java objects. In the best case, the business logic
has no idea whatsoever what format the Java objects are stored in.
To exchange objects between the business logic and the database, we want to use a custom Java
record Pirate. (Before Java 16, you must use a class, and it can be immutable and have a parameterized
constructor.) Objects that are mapped to relational databases, and have an ID, are called Entity‑Bean in
Java jargon. Entity Bean
com/tutego/exercise/jdbc/PirateRepositoryDemo.java
record Pirate(
Long id,
String nickname,
String email,
int swordLength,
11 • Database Access with JDBC 273
LocalDate birthdate,
String description
) { }
The business logic retrieves or writes the data via the repository. Each of these operations is expressed by
a method. Each repository looks a bit different because the business logic wants to retrieve or write back
different information from or to the data store.
Task:
In modeling the application, it has been found that a PirateRepository is needed and must pro‑
vide three methods:
com/tutego/exercise/jdbc/PirateRepositoryDemo.java
SUGGESTED SOLUTIONS
org.h2.Driver
If at this point the output is empty, this is an error and the following programs will not work because the
H2 driver is missing. In this case, the dependency should be checked again.
For Java to load the SQL file as a resource from the classpath, it is placed in the src/main/resources folder
for any Maven project. Instead of putting the SQL file in the root directory, it can be put in the same direc‑
tory as the class. In the proposed solution, the class is fully qualified com.tutego.exercise.jdbc.
CreateTable, which means create‑table.sql is in the symmetric directory src/main/resources/com/
tutego/exercise/jdbc/create‑table.sql.
Accessing resources from the classpath is accomplished with .class.getResourceAs‑
Stream(…), in our case we don’t need to pay attention to the class loader; if Java programs otherwise
load resources from the classpath, this may be relevant. When using getResourceAsStream(…),
the program receives an InputStream or null if the resource is not found. If the file doesn’t exist, a
NullPointerException will already be thrown. The program should handle this accordingly.
11 • Database Access with JDBC 275
The InputStream class has a readAllBytes() method, and we can pass the bytes into the
constructor of String so that we have the file contents.
After loading the SQL script, it can be executed. The database connection is established and a
statement is requested. Statement objects provide the execute(String) method, which exe‑
cutes arbitrary SQL. The method is rarely used because usually, returns are relevant, like the number of
changed rows or in the case of a SELECT the result.
The JDBC API reports errors via SQLException, a checked exception. In the proposed solution,
the exception is caught, and errors are printed on the command line. Exception handling in JDBC is a
bit more complicated because the SQLException object can contain other exceptions in a chain. In
addition, SQLException objects contain a status code that documents the exact error. The follow‑
ing proposed solutions are somewhat simplified in that they do not have a catch block for handling
SQLException; instead, the exceptions are passed up to the method caller with throws.
String sql1 =
"INSERT INTO Pirate " +
"(nickname, email, swordlength, birthdate, description) " +
"VALUES ('CiaoCiao', 'captain@goldenpirate.faith', 18, " +
"DATE '1955‑11‑07', 'Great guy')";
statement.executeUpdate( sql1 );
String sql2 =
"INSERT INTO Pirate " +
"(nickname, email, swordlength, birthdate, description) " +
"VALUES ('lolalilith', 'mefix@bumblebee.space', 12, " +
"DATE '1973‑07‑20', 'I'm 99% perfect')";
int rowCount = statement.executeUpdate( sql2, Statement.RETURN_GENERATED_KEYS );
if ( rowCount != 1 )
throw new IllegalStateException( "INSERT didn't return a row count of 1" );
executeUpdate(…) is used whenever an SQL statement like INSERT, UPDATE, or DELETE is exe‑
cuted. The first variant of executeUpdate(…) gets the SQL string and executes it. In the second call to
executeUpdate(…), the first argument is Statement.RETURN _ GENERATED _ KEYS which tells
the database to pass the generated key along.
With the second executeUpdate(…), the program also remembers the return, which says some‑
thing about the number of modified records; it is 0 if the SQL statement has no return. If the INSERT in
our example inserts a new record, then the return value will be 1, which we can use as a confirmation.
The return is not the generated key. It can’t be because keys can also be strings, for example, and
the return type of executeUpdate(…) is always just int. The generated key is retrieved in a second
276 Java Programming Exercises
statement, getGeneratedKeys(), via the Statement object. The method returns a ResultSet, as
we will also see for SELECT statements later. The next() method determines if there is another row and
fills the ResultSet with information, and the first column contains the generated primary key of type
long. If next() returns false, there is no key to query.
String[] values = {
"'anygo', 'amiga_anker@cutthroat.adult', 11, "
+ "DATE '2000‑05‑21', 'Living the dream'",
"'SweetSushi', 'muffin@berta.bar', 11, "
+ "DATE '1952‑04‑03', 'Where are all the bad boys?'",
"'Liv Loops', 'whiletrue@deenagavis.camp', 16, "
+ "DATE '1965‑05‑11', 'Great guy'" };
The variable sqlTemplate contains a formatting string into which arbitrary VALUES can be injected
later. With an extended for loop, the program runs over the entries of the array and connects the SQL
template to the data. The resulting string is passed to addBatch(…). The executeBatch(…) method
executes the collected SQL statements, and the return is an array containing exactly as many elements as
SQL statements were executed in the batch. In the array, the cells contain the number of modified rows,
just as we have seen before with the executeUpdate(…) method.
The proposed solution does something else; it resets the auto‑commit mode. By default, the JDBC
driver puts each SQL statement it sends into its transactional block. For batch processing, it is undeter‑
mined whether the entire batch occurs in a transaction, parts of it, or each SQL statement. The Javadoc
writes:
The commit behavior of executeBatch is always implementation‑defined when an error occurs
and auto‑commit is true.
In the proposed solution, the batch should take place in a transaction. To accomplish this, the
auto‑commit mode must first be switched off. After submitting via executeBatch() the transaction is
completed with commit(). If the transaction is successful and there is no exception, all statements are
committed.
11 • Database Access with JDBC 277
connection.setAutoCommit( false );
connection.commit();
}
Request Data
com/tutego/exercise/jdbc/Select.java
while ( resultSet.next() ) {
String nickname = resultSet.getString( /* nickname column */1 );
int swordlength = resultSet.getInt( "swordlength" );
Date birthdate = resultSet.getDate( "birthdate" );
System.out.printf( "%‑20s%‑20s%10d%n",
nickname,
birthdate.toLocalDate().format(
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG) ),
swordlength );
}
}
The execute*() methods of the Statement object return different result types. In the case of execute‑
Query(…) the result is a ResultSet. The object allows access to the rows. A ResultSet will always
contain only the information about one row at a time. The method called next() sets a kind of cursor on
the next row of the result set and returns a boolean value with the information whether the next row could
be read or not. If next() returns true, the ResultSet contains the information about one row.
The content of a column can be read using two different approaches: with the column index, which in
SQL always starts at 1, or with the column name. There are also different get*(…) methods that convert
types. For all SQL data types, there are corresponding Java methods that give us the related Java type.
For example, the getString(…) method returns a String object for text columns. The JDBC driver
performs various conversions so that, for example, getString(…) works for any SQL column type. Java
has three own data types for SQL date and time values in the package java.sql: Date, Time (time),
and Timestamp (date and time). The data types allow converting to the data type of the Java Date‑Time
API. It returns getDate(…) a java.sql.Date object that toLocalDate() converts to a known
LocalDate that can be formatted with the usual API.
int NICKNAME_COLUMN = 2;
String sql = "SELECT * FROM Pirate ORDER BY nickname";
String jdbcUrl = "jdbc:h2:./pirates‑dating";
try ( Connection connection = DriverManager.getConnection( jdbcUrl );
Statement statement = connection.createStatement(
ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_READ_ONLY );
ResultSet srs = statement.executeQuery( sql ) ) {
if ( srs.last() )
System.out.printf( "%d rows%n", srs.getRow() );
srs.absolute( 1 );
System.out.println( srs.getString( NICKNAME_COLUMN ) );
else
srs.previous();
}
case "d", "n" ‑> {
if ( srs.isLast() )
System.out.println( "Already last" );
else
srs.next();
}
}
System.out.println( srs.getString( NICKNAME_COLUMN ) );
}
}
Before the ResultSet can be moved, the Statement object must be initialized correctly:
createStatement( ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_READ_ONLY );
The Javadoc names the parameters resultSetType and resultSetConcurrency. The constants
originate from ResultSet, and TYPE_SCROLL_SENSITIVE configures the request to be able to move
the cursor freely in ResultSet. Not every database and database driver supports this feature, so an
SQLFeatureNotSupportedException may be thrown.
If we succeed in building the Statement object, we can use the SELECT statement to build a
ResultSet. Already, the first question about the total number of elements can be realized using the
moving cursor. If we call last() of the ResultSet object, it sets the cursor to the last element. The
getRow() method then returns the current row, in our case the number of records in the result set. A call
to absolute(1) sets the cursor back to the first row. The ResultSet is always filled with the current
data; thus, if we access the column for the call name, we get exactly the contents of the row on which the
cursor is currently positioned.
For interactive use, the Scanner helps with console input. The loop is executed as long as the user
does not press q (quit). In the case of u or p the user wants to move the cursor up. This is possible if the
cursor is not already on the first line. This is checked by isFirst(). In the case of d or n, something
similar is tested; the cursor can be moved to the next line with next() unless the cursor is already on
the last line.
Pirate Repository
Before we start with the actual implementation, we should think about possible exceptions. Even though
any SQL query should succeed, all queries via the JDBC API return checked inconvenient exceptions.
Besides, a repository is supposed to hide the storage technology, and an SQLException does not come
along very well. The chosen solution introduces a new class DataAccessException:
com/tutego/exercise/jdbc/PirateRepositoryDemo.java
DataAccessException is an unchecked exception that wraps every exception. Internally, our reposi‑
tory methods will catch an SQLException and convert it to a DataAccessException.
280 Java Programming Exercises
The PirateRepository is more extensive, so we will deal with the type declaration, constants,
and constructor in the first part, and then with the individual methods in the next steps.
com/tutego/exercise/jdbc/PirateRepositoryDemo.java
class PirateRepository {
The PirateRepository class declares different constants for the SQL statements. We need one SQL
statement each to query all pirates, to query a specific pirate with the given ID, to insert new pirates, and
to update existing pirates. Only the first SQL statement to query all pirates does not use placeholders.
Otherwise, the SQL strings will be used later in the PreparedStatement.
The program does not declare any constants for the columns; each index is documented in the code.
The constructor takes the JDBC URL and stores it in an attribute so that later the individual methods
can establish a new connection to this data source. If the identifier is invalid, there will be an exception
later; null can be tested early.
We have already implemented the core of the individual methods in the previous tasks, so the JDBC
API is not new.
For the first method findAll():
com/tutego/exercise/jdbc/PirateRepositoryDemo.java
After the Statement is built, all records are selected. The while loop goes over the result set and calls
its own private method mapRow(…), which transfers a row in the ResultSet to a Pirate object. The
result is placed in the list, and when the cursor reaches the end of the result, the list is returned.
com/tutego/exercise/jdbc/PirateRepositoryDemo.java
mapRow(…) reads the columns with the known methods getLong(…), getString(…), getInt(…),
and getDate(…) and transfers the assignments to the constructor of Pirate. The Pirate has nothing
to do with the SQL data type Date, so toLocalDate() converts to a LocalDate object. Moving the
ResultSet object is no part of the mapRow(…) duties.
The findById(…) method also uses mapRow(…):
com/tutego/exercise/jdbc/PirateRepositoryDemo.java
After building a PreparedStatement object, the SQL query is sent and the ResultSet is evaluated.
There are two alternatives: the ResultSet could have exactly one result or none. If there is no result,
then there was no pirate for the ID and the return is Optional.empty(). However, if next() returns
true, mapRow(…) constructs a Pirate object from the ResultSet, which comes into the Optional
and is returned.
The save(…) method needs to detect whether the record must be updated or written again based on
an ID that has been set.
com/tutego/exercise/jdbc/PirateRepositoryDemo.java
The if statement checks exactly this case. If id == null, an internal method saveInsert(…) takes
care of inserting a new record, otherwise saveUpdate(…) updates the row. Both methods return a
Pirate object, which is also the return of save().
saveInsert(…) and saveUpdate(…) both work with PreparedStatement, where saveIn‑
sert(…) has to do a bit more work by asking for the generated key. Since Pirate objects are immutable,
a new Pirate is created with the ID set. saveUpdate(…) returns the passed Pirate object.
After a query, the ResultSet method getMetaData() returns the metadata as a ResultSetMetaData
object, and the number of columns in the result set is returned by getColumnCount(). Since a list is
desired as the result, a container of type ArrayList is built. The ResultSet is iterated through as
usual, and a LinkedHashMap is built as Map to preserve the order of the columns. A for loop goes
through the columns one by one from 1 to getColumnCount(), reading the column assignment with
the getObject(…) method and putting a new entry into the LinkedHashMap together with the col‑
umn name. After a loop traversing all columns, the associative memory of one row contains all column
mappings, and the Map is placed in the list. This is repeated for all rows, and at the end, there is a list of
associative stores that findAllPirates() returns.
Operating
System Access 12
Java developers often don’t realize how much the Java libraries abstract from the operating system. A few
examples: Java automatically sets the correct path separator (/ or \) for path entries, and automatically
sets the operating system’s usual end‑of‑line character for line breaks. When formatting for console input
and when parsing console input, Java automatically falls back on the set language of the operating system.
Not only does the Java library use these properties internally, but they are accessible to all. The prop‑
erties are hidden in various places, such as:
• In system properties.
• In platform MXBeans, like the OperatingSystemMXBean you get with
getSystemCpuLoad().
• In java.net.NetworkInterface for network cards and MAC address (media access con‑
trol address).
• In Toolkit for screen resolution.
• In GraphicsEnvironment for the installed fonts.
If special information is missing, Java programs can call external native programs and interact with them,
for example, to ask for more details. The tasks in this chapter focus on system properties and how to call
external programs.
Prerequisites
• java.lang.System
• java.util.Properties
• InputStream
• Process
• ProcessBuilder
CONSOLE
Java programs are not just algorithms; they interact with us and with the operating system. Two of the first
statements were System.out.println(...) and new Scanner(System.in).next()—a screen
output and console input are typical interfaces between program and user.
\u001B[
\u001B is the ESC character in the ASCII (American Standard Code for Information Interchange) alpha‑
bet, decimal 27.
In Java, escape sequences can easily be written to System.out or System.err, and there are
libraries such as https://github.com/fusesource/jansi that simplify this via constants and methods. The
only problem is that the different consoles do not necessarily recognize all escape sequences. By default,
Windows’ cmd.exe, for example, is unable to recognize any of them. However, support for some colors is
common on other consoles:
• Black: \u001B[30m
• Red: \u001B[31m
• Green: \u001B[32m
• Yellow: \u001B[33m
• Blue: \u001B[34m
• Magenta: \u001B[35m
• Cyan: \u001B[36m
• White: \u001B[37m
• Reset: \u001B[0m
• bold: \u001B[1m.
• underlined: \u001B[4m.
• inverted: \u001B[7m.
Task:
• Create a new class AnsiColorHexDumper, and put the following constants in the class:
• Write a new method printColorfulHexDump(Path) that reads the given file and prints
it as a hexdump on the console. A hex dump is the output of a file in hexadecimal notation, i.e.,
sequences like 50 4B 03 04 14 00 06 00 08 00 written in columns.
• Extend the program so that colors indicate the occurrence of certain bytes in the file. For
example, ASCII letters can appear in one color, but digits in another.
PROPERTIES
The term properties is used multiple times in Java. It stands for the JavaBean properties and for key‑value
pairs that can be used for configuration. When we talk about “properties” in this chapter, we always mean
key‑value pairs, especially the pairs that a Properties object manages.
• Create a new enumeration type OS with the constants WINDOWS, MACOS, UNIX, UNKNOWN.
• Add a static method current() which reads the name of the operating system with System.
getProperty("os.name") and returns the corresponding enumeration element as result.
• Write a program that can accept port information from different sources:
• On the command line, the port can be specified with ‑‑port=8000.
• If ‑‑port does not exist, an environment variable port should be evaluated, which can
also be set on the command line with ‑Dport=8020.
• If the environment variable does not exist, an assignment like port=8888 is to be evalu‑
ated in an application.properties file.
• If no specification is made at all, the port is set to 8080 by default.
$ java com.tutego.exercise.os.PortConfiguration
$ java com.tutego.exercise.os.PortConfiguration ‑‑port=8000
$ java ‑Dport=8020 com.tutego.exercise.os.PortConfiguration
12 • Operating System Access 287
If the computer is not a laptop with a battery, the output is “No instances available.” If the laptop is loaded,
the result for EstimatedRunTime is 71582788 (hexadecimal 4444444). Microsoft provides details at
https://docs.microsoft.com/en‑us/windows/win32/cimwin32prov/win32‑battery.
Task:
• In a Java program, start the Windows program wmic as an external process using
ProcessBuilder. Read the result of EstimatedChargeRemaining and
EstimatedRunTime.
• Consider that a laptop can be powered from the power grid or a desktop computer has no
battery.
Microsoft’s operating system provides an API named Windows Management Instrumentation (WMI)
that can be used to read and, in some cases, change settings on a computer. WMI is comparable to JMX on
the Java side. Because shell scripts often want to access this mapping for automation purposes, Microsoft
has created a command‑line interface Windows Management Instrumentation Command‑line (WMIC).
A WMI provider provides information about CPU usage, motherboard, network, battery status, and
much more. The following call gives a small overview:
> wmic /?
288 Java Programming Exercises
SUGGESTED SOLUTIONS
Instead of reading the input in one go with Files.readAllBytes(…), we open an input stream to
then read byte by byte. The for loop declares two variables: a variable i controls the new line, which
should always be set after 32 hexadecimal characters, and the variable b contains the read byte. The
InputStream is read as long as the result is not unequal ‑1, i.e., bytes are still available.
In the body of the for loop, the read byte is tested and depending on the assignment, the variable
color is initialized with the ANSI escape sequence. If the byte does not fall into the five categories, the
color is reset. Finally, the escape sequence is written together with the hexadecimal code and a newline is
set after a maximum of 32 hexadecimal characters in one line.
public enum OS {
WINDOWS,
MACOS,
UNIX,
UNKNOWN;
public static OS current() {
String osName = System.getProperty( "os.name" );
if ( osName == null ) return UNKNOWN;
osName = osName.toLowerCase();
return osName.contains( "windows" ) ? OS.WINDOWS :
osName.contains( "mac" ) ? OS.MACOS :
12 • Operating System Access 289
The method current() reads the name of the operating system from the property os.name. If it is
null, the answer is obvious. Otherwise, the method converts the name to lowercase for the comparisons
and tests for various substrings in a cascade with condition operators. If no case distinction is caught, the
name of the operating system is unknown.
The parseInt(String) method accesses Integer.parseInt(…) and catches the exceptions if the
string cannot be converted to a number. Our method catches the exception and returns an OptionalInt.
empty(), otherwise an OptionalInt with the parsed number.
Two more helper methods follow:
com/tutego/exercise/os/PortConfiguration.java
console passes and examines whether ‑‑port= is present. If so, the method truncates the front part and
returns the result of parseInt(String). If the method finds nothing, the OptionInt is empty().
portFromPropertyFile(…) evaluates the application.properties file in the classpath. File con‑
tents in the classpath are read in via the Class method getResourceAsStream(…) because that
also supports resources packaged in a JAR archive. We first build an empty Properties object, then
call the load(…) method, and pass the InputStream. If the file does not exist, the InputStream
will be empty, which will throw an exception, but catch will catch it, as well as possible errors during
loading. If the Properties object could be filled, we ask for the port property and convert it to an
OptionalInt via our own method; if it is filled, we return the value.
The last method is port(…) ties everything together.
com/tutego/exercise/os/PortConfiguration.java
First, we test the command line. If there is a hit, we don’t have to consider everything else, because passing
on the command line has the highest priority.
If there is no exit from the first step, we continue with the second step. System.
getProperty(String) reads a system property, we pass the return in parseInt(…). If the property
does not exist, our parseInt(…) will return an OptionalInt.empty(). If there is an assignment,
port(…) will return it.
If there was not the key with an associated value, we have to open the file and we are in step 3.
If there is no file or no port specification, comes the last step, step 4. We have no more options and
return as default port 8080.
Since our program calls WMIC twice with different parameters, the proposed solution introduces
a new method wmicBattery(…) that can be passed EstimatedChargeRemaining and
EstimatedRunTime in our case. This works well because both queries always expect a numeric value.
wmicBattery(…) declares a new array and passes it to the constructor of ProcessBuilder so
that the process can be started with start(). The return is the Process from which we get the output
in the next step, which is our InputStream. We don’t have to write anything into the process ourselves,
so a separate OutputStream is not necessary. We also don’t have to wait for the process to finish, which
happens automatically.
There are different approaches to extract the relevant parts from the input stream. This proposed
solution converts the InputStream into a BufferedReader so that the lines() method can be
used, which returns a Stream of all lines. We first remove the white space at the beginning and end of
each line, and then look only for lines that contain a number. This leaves either one line or no line. If a
line contains a number, it is converted, resulting in an IntStream where findFirst() gives us an
OptionalInt that is either empty if, for example, there is no number in the process output due to a miss‑
ing battery, or otherwise contains the number. Possible errors during processing are caught and logged;
the return is then an OptionalInt.empty().
In use, the method can be called as follows:
com/tutego/exercise/os/WmicBattery.java
shows. This is difficult to parse, which is why WMIC provides different output formats. At the end of the
command, a /format:csv can be appended, which makes the output much easier to process.
Node,Availability,BytesPerSector,Capabilities,CapabilityDescriptions,Caption,
CompressionMethod,ConfigManagerErrorCode,ConfigManagerUserConfig,CreationClas
sName,DefaultBlockSize,Description,DeviceID,ErrorCleared,ErrorDescription,Err
orMethodology,FirmwareRevision,Index,InstallDate,InterfaceType,LastErrorCode,
Manufacturer,MaxBlockSize,MaxMediaSize,MediaLoaded,MediaType,MinBlockSize,Mod
el,Name,NeedsCleaning,NumberOfMediaSupported,Partitions,PNPDeviceID,PowerMana
gementCapabilities,PowerManagementSupported,SCSIBus,SCSILogicalUnit,SCSIPort,
SCSITargetId,SectorsPerTrack,SerialNumber,Signature,Size,Status,StatusInfo,Sy
stemCreationClassName,SystemName,TotalCylinders,TotalHeads,TotalSectors,Total
Tracks,TracksPerCylinder
DESKTOP‑0P7C7G7,,512,{3;4},{Random Access;Supports Writing},SAMSUNG
MZVLB1T0HALR‑00000,,0,FALSE,Win32_DiskDrive,,Laufwerk,\\.\PHYSICALDRIVE1,,,,E
XA7201Q,1,,SCSI,,(Standardlaufwerke),,,TRUE,Fixed hard disk media,,SAMSUNG
MZVLB1T0HALR‑00000,\\.\PHYSICALDRIVE1,,,3,SCSI\DISK&VEN_NVME&PROD_SAMSUNG_MZV
LB1T0\5&1E3C5E74&0&000000,,,0,0,1,0,63,0025_3886_81B2_
A1AD.,,1024203640320,OK,,Win32_ComputerSystem,DESKTOP‑0P7
C7G7,124519,255,2000397735,31752345,255
DESKTOP‑0P7C7G7,,512,{3;4;10},{Random Access;Supports Writing;SMART
Notification},WDC WD40EZRX‑22SPEB0,,0,FALSE,Win32_DiskDrive,,Laufwerk,\\.\PHY
SICALDRIVE0,,,,80.00A80,0,,IDE,,(Standardlaufwerke),,,TRUE,Fixed hard disk
media,,WDC WD40EZRX‑22SPEB0,\\.\PHYSICALDRIVE0,,,1,SCSI\DISK&VEN_WDC&PROD_WD4
0EZRX‑22SPEB0\4&2D010F8D&0&000000,,,0,0,0,0,63, WD‑WCC4E2SHPE5N,,400078441728
0,OK,,Win32_ComputerSystem,DESKTOP‑0P7C7G7,486401,255,7814032065,124032255,255
The first line contains the column names, followed by the comma‑separated values.
If only selected keys are requested, they are listed after get, for example:
12 • Operating System Access 293
Furthermore, /format:list is good to parse because it produces the property format known under
Java, which is handy when there is a result. There are many other formats.
Reflection,
Annotations,
and JavaBeans
13
Reflection gives us the ability to look inside a running Java program. We can ask a class what properties
it has, and later call methods on arbitrary objects and read and modify object or class variables. Many
frameworks make use of the Reflection API, such as JPA (Jakarta Persistence API) for object‑relational
mapping or Jakarta XML Binding (JAXB) for mapping Java objects to XML structures. We will program
some examples ourselves that would not be possible without Reflection.
Annotations are a kind of self‑programmed modifiers. They allow us to enrich the source code with
metadata that can later be read via reflection or another tool. Often we are just users of other people’s
annotations, but in this chapter, we will also practice how to write our own annotation types.
Prerequisites
• java.lang.Class
• java.lang.reflect.Field
• java.lang.reflect.Method
• java.lang.reflect.Constructor
• java.lang.reflect.Modifier
REFLECTION API
The Reflection API can be used to examine arbitrary objects, and the following tasks use that to gener‑
ate UML (Unified Modeling Language) diagrams of arbitrary data types. The tasks focus on practical
applications; you can also do a lot of nonsense with the Reflection API, such as changing characters from
immutable strings, but that’s silly, and we would rather not do that.
The arrows ‑‑|> or <|‑‑ are represented regularly, ..|> or <|.. are stippled.
PlantUML generates from these text documents a representation of the following type:
PlantUML is open source, and you can install a command‑line program that converts the textual
description into a graph with the UML diagram. There are also websites like https://www.planttext.com
that can display live UML diagrams.
Task:
• For any class, of which only the fully qualified name is given, generate a PlantUML diagram
text, and output the text to the console.
• The diagram should show the type and its base types (superclasses and implemented
interfaces).
• The diagram should also recursively list the types of the superclasses.
296 Java Programming Exercises
Example:
class Radio {
isOn: Boolean
isOn() : Boolean
{static} format(number: int): String
}
Radio
isOn: boolean
isOn() : boolean
format(number: int): String
Task:
• Write a method that retrieves any Class object and returns a multi‑line string in PlantUML
syntax as the result.
• It is sufficient to include only the object/static variables, constructors, and methods, not the type
relationships.
Example:
‑ initIDs(): void
+ setSize(arg0: Dimension): void
+ setSize(arg0: double, arg1: double): void
+ setSize(arg0: int, arg1: int): void
+ getWidth(): double
+ getHeight(): double
}
@enduml
1;2;3
4;5;6
Task:
Example usage:
Bonus: if you use accesses to instance variables, the instance variables marked with the modifier tran‑
sient should not be written.
ANNOTATIONS
Annotations allow us to introduce metadata into Java code that can later read—usually via Reflection.
Annotations have become essential because many developers express configurations declaratively and
leave the actual execution to the framework.
@Csv
class Pirate {
@CsvColumn String name;
@CsvColumn String profession;
@CsvColumn int height;
@CsvColumn( format = "### €" ) double income;
@CsvColumn( format = "###.00" ) Object weight;
String secrets;
}
Task:
• Declare the annotation @Csv, which can only be set on type declarations.
• Declare the annotation @CsvColumn, which can only be set on instance variables.
• Allow a string attribute format at @CsvColumn, for a pattern that controls the formatting of
the number using a DecimalFormat pattern.
• Create a class CsvWriter with a constructor that stores a Class object as a type‑token
and also a Writer, where the CSV rows will be written later. The class CsvWriter can be
AutoCloseable.
• Create CsvWriter as a generic type CsvWriter<T>.
• Write two new methods.
• void writeObject(T object): Write an object.
• void write(Iterable<? extends T> iterable): Write multiple objects.
• The separator for the CSV columns is ';' by default, but should be able to be changed via a
method delimiter(char).
• Consider what error cases may occur and report them as an unchecked exception.
Example usage:
SUGGESTED SOLUTIONS
if ( clazz.getSuperclass() != null ) {
System.out.printf( "%s <|‑‑ %s%n",
clazz.getSuperclass().getSimpleName(),
clazz.getSimpleName() );
visitType( clazz.getSuperclass() );
}
At the heart of the solution is the custom method visitType(Class<?> clazz), which is called recur‑
sively. We don’t have to care about the object/class variables, methods, and constructors, but only about
the possible superclass and implemented interfaces.
The first case distinction takes care of the possible superclass. There can be at most one of these. On
the Class object, we call getSuperclass() and get either null if we have already landed at java.
lang.Object in the inheritance hierarchy, or just the superclass. If we have a superclass, the program
generates an arrow.
And if we found a superclass, it will have a superclass again, so we call the visitType(…) method
recursively.
The second part generates the arrows for the implemented interfaces. The extended for loop tra‑
verses all interfaces. If the class does not implement an interface, the loop is not executed. If there is an
interface, we ask for the name as well as the name of our own class and generate the arrow. Since this
interface can extend other interfaces, we again call visitType(…) recursively.
body.println( "}\n@enduml" );
return result.toString();
}
The focus is on the plantUml(Class) — method; it generates the text for the UML diagram for a
Class object. Since we need to build a string, we have several options. We could use the + operator to
concatenate strings, or we could use the StringBuilder and use the append(…) methods. However,
it would be handy if we could use a format string. Here we can use the PrintWriter class, which we
already know in an API‑like form from System.out. A PrintWriter offers us the nice print(…),
println(…), and printf(…) methods. A PrintWriter is an adapter that needs a target into which it
writes the generated string. We want to collect the result in a StringWriter.
Then we start to write the single segments of a UML diagram. First comes the class name, then
we put in the object/class variables, then the constructors, and finally the methods. The Class object
provides us with the data via corresponding methods. There is one thing in common for the object/class
variables, constructors, and methods, and that is their visibility. This can be queried via the base type
Member. The own method formatUmlVisibility(Member) translates the modifier into a string,
which we include in the PlantUML code for visibility.
Constructors and methods have another thing in common, a parameter list. Therefore, there is another
method formatParameters(Parameter[]) which generates a string for PlantUML from an array
of parameter objects. It is an array because a method or constructor can have multiple parameters, and
consequently, we need to ask for the name of each parameter, and the return type. This can be seen as a
step‑by‑step transformation, and this is where the Stream API is useful. First, we generate a Stream from
the array. In the next step, we transfer the Parameter object to a string. Here we proceed as follows:
the string consists of the name of the parameter, a colon, and a data type. After the map(…) operation, a
Stream<String> is created. This string is comma‑separated to a large result and returned.
13 • Reflection, Annotations, and JavaBeans 301
The solution consists of the desired method writeAsCsv(List<?> objects, Appendable out)
and a helper method accessField(…). Since we have a list of arbitrary elements, we must first traverse
the list. This is where the extended for loop is useful.
When we consider each item from the list, we need to generate a line for each item. This sequence of
operations, from an object to the CSV line, is realized by the Stream API. Taking an object as a starting
point, we first query the Class object and then all object/class variables with getFields(). The result
is an array that we want to enhance to a stream. The stream thus consists of Field objects, and since we
do not want to consider transient fields, we use the filter method from the stream to leave only the object/
class variables in the stream that are not transient.
In the next step, we need to read the instance variable. For this, we rely on a separate method. The rea‑
son is that lambda expressions and checked exceptions become syntactically cluttered because a checked
exception must be caught within the lambda expression. However, the Reflection API uses checked excep‑
tions frequently. The purpose of the custom method String accessField(Field, Object) is
to read an instance variable for a given object, and convert it to a string. The method catches a possible
checked exception and encapsulates it into an unchecked exception. The stream’s terminal operation col‑
lects all partial strings and concatenates them with a semicolon. Finally, the line, including an end‑of‑line
character, is written to the writer.
com/tutego/exercise/annotation/Csv.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.TYPE )
public @interface Csv {
}
com/tutego/exercise/annotation/CsvColumn.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.FIELD )
public @interface CsvColumn {
String format() default "";
}
com/tutego/exercise/annotation/CsvWriter.java
if ( fields.isEmpty() )
throw new IllegalArgumentException(
"Class does not contain any @CsvColumn" );
13 • Reflection, Annotations, and JavaBeans 303
this.clazz = clazz;
this.writer = Objects.requireNonNull( writer );
}
if ( ! clazz.isInstance( object ) )
throw new IllegalArgumentException(
"Argument is of type " + object.getClass().getSimpleName()
+ " but must be of type " + clazz.getSimpleName() );
if ( fieldValue == null )
return "";
if ( format.isBlank() )
return Objects.toString( fieldValue );
if ( isNumericType( fieldValue ) )
return new DecimalFormat( format ).format( fieldValue );
The constructor takes a Class object and a Writer and performs checks and preprocessing.
1. First, the constructor performs a test whether what is to be written later has the marking
annotation Csv; if not, there is a runtime exception. This way, this test does not have to be
repeated later when writing. Since the types will also always be the same when writing later,
and reflection at runtime should be avoided for performance reasons, the constructor fetches
the corresponding Field objects and stores them. Only the Field objects annotated with @
CsvColumn are placed in the internal list. If this list is empty, there is an exception because
this would mean that there is nothing to write. Moreover, the constructor remembers the Class
object for a later test because generics are just a trick of the compiler and at runtime wrong
types could be foisted after all.
2. The Writer is stored internally, and as usual, an early test ensures that this Writer is not
null. Our class implements AutoCloseable, and the implemented close() method closes
the underlying Writer. Thus, CsvWriter can be used well in a try‑with‑resources block.
The delimiter, that is the CSV separator for the columns, can be reassigned by delimiter(char).
The method returns the current CsvWriter, so that calls cascade well in a fluent API way. Whether the
delimiter is reasonable or not is not tested by the method. In principle, the delimiter could be a 'a', '\n'
or '\u000'.
writeObject(T) writes a single object that the write(Iterable<? extends T>) method
can well access. The forEach(…) method provided by Iterable iterates over all the data and calls
writeObject(T) on each element.
First, writeObject(T) performs a type check to see if the argument is type‑compatible with the
one declared in the constructor via the Class object. clazz.isInstance(…) is the dynamic variant
of instanceof. If the type does not match, there is an exception. In the next step, the Stream walks
overall Field elements, gets the value of the instance variable converted to a string for each Field with
getFieldValue(…), and finally assembles all strings with the delimiter for the CSV output. At the end
of the line, there is a line break. The resulting string is written to Writer and a possible IOException
is caught and translated into an unchecked exception. getFieldValue(…) is a separate method that
hides the complexity for accessing and formatting numeric values.
The getFieldValue(…) method gets the object with the data and the Field object for the instance
variable. The Field method get(…) returns the value stored in the instance variable. If it is null, there
is nothing to write, and we return the empty string.
13 • Reflection, Annotations, and JavaBeans 305
Now there are two possibilities: at the CsvColumn there is the format attribute with a formatting
pattern or not.
• If there is no formatting pattern, or the format consists only of blanks, then the string represen‑
tation of the value is returned.
• If there is a formatting pattern, isNumericType(…) checks whether the value in the attri‑
bute is numeric. This property could have been tested already in the constructor, but this would
have a disadvantage: if the type is Object for example and there is a Double behind it at
runtime, this is perfectly fine. Only at runtime, this can be tested. For a numeric attribute, the
format(…) method of DecimalFormat returns the string representation. If a formatting
pattern is given, but the type is not numeric, an IllegalStateException follows.
DecimalFormat can format different types automatically. The valid types are tested via the own method
isNumericType(…). These include Integer, Long, Double, BigInteger, and BigDecimal.
Epilogue
Kudos to those who persevered through the tasks, delved into the suggested solutions, and made it to the
finish line. The journey may have been long, but now the groundwork for a prosperous Java career has
been laid. But let’s be real, this is just the beginning. The truly triumphant ones consistently follow these
three steps:
In addition, there are websites and forums that regularly publish new tasks. A few free sites include:
PROJECT EULER
From the website: “Project Euler is a series of challenging math/programming problems that require more
than mathematical insight to solve.” Many of these problems come from mathematics, but developers must
also use sophisticated algorithms to solve them (https://projecteuler.net/).
DAILY PROGRAMMER
Reddit calls itself the “front page of the Internet” and consists of a large collection of forums called “sub‑
reddits”. One of these subreddits is Daily Programmer, which is all about challenging programmers of
all skill levels with weekly programming challenges. There are varying levels of difficulty (https://www.
reddit.com/r/dailyprogrammer/).
307
308 Epilogue
ROSETTA CODE
The charm of Rosetta Code is the diversity of programming languages. There are over 1,000 program‑
ming tasks and suggested solutions in more than 800 programming languages. We as Java developers can
also learn from solutions of other programming languages; probably less from a dinosaur such as COBOL,
but very much from functional programming languages (https://rosettacode.org/wiki/Rosetta_Code).
Some companies also use programming tasks as part of their recruitment process. This has given rise
to a business model where commercial vendors offer closed‑form tasks for applicants to complete, and the
HR department and chief developers evaluate the proposed solutions and retrieve metrics.
Another development is Competitive Programming, where the goal is to develop solutions and points
are awarded for successfully completing tasks, with the person with the most points declared the winner.