0% found this document useful (0 votes)
8 views

Concepts of Programming With Java

Concepts of Programming with Java

Uploaded by

dgvilyas
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views

Concepts of Programming With Java

Concepts of Programming with Java

Uploaded by

dgvilyas
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 91

Concepts of Programming with Java

Sébastien Jodogne, Ramin Sadré, Pierre Schaus

Sep 20, 2023


CONTENTS

1 Part 1: From Python to Java 1


1.1 Your first Java program with IntelliJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 How do Java programs look like? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4 The Java compiler and class files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.5 Primitive Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6 Arrays (fr. tableaux) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.7 Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.8 Conditional Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.9 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.10 Comparing things . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.11 Classes and Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.12 Mental model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.13 Working with objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.14 Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.15 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
1.16 Polymorphism . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.17 The class hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.18 ArrayList and Boxing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
1.19 Method overloading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

2 Part 2: Unit testing 43


2.1 Subtitle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

3 Part 3: Data-Structures and Algorithms 45


3.1 Time Complexity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.2 Space Complexity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.3 Algorithm Correctness . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

4 Part 4: Object Oriented Programming and Design Patterns 61


4.1 Interfaces and Abstract Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.2 Abstract Data Types (ADT) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.3 Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.4 Implementing your own iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
4.5 Delegation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.6 Observer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75

5 Part 5: Functional Programming 81


5.1 Functional Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.2 Higher Order Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81

i
5.3 Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.4 Immutable Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81

6 Part 6: Parallel Programming 83


6.1 Subtitle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83

7 Indices and tables 85

Bibliography 87

ii
CHAPTER

ONE

PART 1: FROM PYTHON TO JAVA

Part 1 of this book is intended for students and hobbyists who are already familiar with the basics of Python program-
ming, i.e., they know how to use variables, lists, functions, and plain data objects. A deeper knowledge of object-
oriented programming is not required.
The goal of the following sections is to make you quickly familiar with the important differences between Python and
Java and with the basic object oriented mechanisms of Java. More advanced topics, such as interfaces, abstract classes,
or lambda functions, will be seen in the other parts of the book.

1.1 Your first Java program with IntelliJ

1.1.1 Installing IntelliJ

You might have already used an Integrated Development Environment (IDE) to write Python programs. In this course,
we will do the same for programming in Java: we will use the free “Community Edition” of Intellij IDEA (we will
just call it “IntelliJ” in the following). You can download the installer from https://www.jetbrains.com/idea/download/
(scroll down to find the free Community Edition, you don’t need the commercial Ultimate Edition). Start the installer
and follow the instructions.
The second thing you will need for Java programming is a Java Development Kit (JDK). A JDK is a software package
that contains the tools that you need to build and run Java programs. The JDK also includes a very, very large library
of useful classes for all kinds of programming tasks. You can see the content of the library here: https://docs.oracle.
com/en/java/javase/20/docs/api/index.html.
Fortunately, IntelliJ can automatically download the JDK for you when you create a new project, so you don’t have to
worry about the JDK now. But if one day you want to write a Java application on a computer without IntelliJ, you have
to manually download the JDK from https://openjdk.org/ and install it.

1.1.2 Creating a new project

Start IntelliJ. A window will open where you can create a new project. Click on the corresponding button and you
should see a window like this one:

1
Concepts of Programming with Java

To create a new project, you have to enter a project name (in the field “Name”) and a location on your disk where you
want to store the project (in the field “Location”). Keep the other fields “Language”, “Build system”, and “Add sample
code” as shown in the above picture. But there is something to do for the field “JDK”: As you can see in the picture,
there was already JDK version 20 (and some other JDK versions) installed on my computer. If you have not already
installed a JDK on your computer, open the dropdown list and choose “Download JDK. . . ” as shown in the picture
below:

2 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

A small window should appear where you can select which JDK version to download and install:

Select version 20 from the vendor “Oracle OpenJDK” (actually, any version newer than 17 is fine for this book). You
can keep the location proposed by IntelliJ. Click the “Download” button and complete the JDK installation. Once
everything is ready, you can finally create your first Java project. IntelliJ will normally automatically open the new
project and show you the main window:

1.1. Your first Java program with IntelliJ 3


Concepts of Programming with Java

In the left part of the window, you see the project structure. Since we have select “Add sample code” in the project
creation window, IntelliJ has already created a “src” directory with one file in it: “Main.java” (the file ending “.java”
is not shown). When you double-click the file, its content is shown in the editor in the right part of the window.
Click on the right triangle in the upper right corner to start the program. A new view should appear at the bottom of
the window with the output of the program:

4 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

1.2 How do Java programs look like?

Here is source code of the example program automatically created by IntelliJ in your project:

public class Main {


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

And here is how an equivalent Python program would look like:

print('Hello world!')

Why does the Java code look more complicated than the Python code? First of all, unlike Python, Java doesn’t allow to
write a statement like print('Hello world!') directly in a source code file. In Java, all statements MUST be inside
a method and all methods MUST be inside a class. In our example, the statement System.out.println("Hello
world!") is in the method “main” and this method is in the class “Main”. Of course, a class in Java can have more
than one method, and a Java program can contain more than one class.
You probably have already learned about classes and methods in Python and you might remember that classes are
used to describe objects and methods are used to work with those objects. In our simple Java example, we don’t need
objects and all the complicated things that come with them (constructors, inheritance, etc.). The word static in the
line public static void main(String[] args) indicates that the method main behaves more like a traditional
function in Python and not like a method for objects. In fact, no object is needed to execute a static method like main.
We will learn more about this later.
The second thing you might have noticed is the word public appearing twice in the first two lines of the code:

public class Main {


public static void main(String[] args) {

The word public in the first line indicates that the class Main can be used by others. It is not strictly necessary for
this simple program and, in fact, our program will still work if you remove it (try it!). However, there is something
important you have to know about public classes: If a class is marked as public, the source file that contains the class
must have the same name as the class. That’s the reason why the file is called “Main.java” and the public class in the file
is called “Main” (Try to change the name of the class and see what happens!). Apart from that, the name “Main” for a
class doesn’t have any special meaning in Java. Our program would still work if we renamed the class to “Catweazle”
or “Cinderella”, as long as we don’t forget to rename the file as well. But note that all class names in Java (public or
not) start with an uppercase letter.
The public in the second line is much more important for our example. A Java program can only be executed if it
contains a method main that is public and static. Remove the public or static from the second line and see
what happens when you try to run the program. In general, a Java program always starts at the public static main
method. If your program contains multiple classes with a main method, you have tell IntelliJ which one you want to
start.
With this knowledge, can you guess what the following program prints?

public class Main {


static void printHello() {
System.out.print("How do ");
System.out.println("you do, ");
}

(continues on next page)

1.2. How do Java programs look like? 5


Concepts of Programming with Java

(continued from previous page)


public static void main(String[] args) {
printHello();
System.out.println("fellow kids?");
}
}

(By the way, have you noticed the difference between System.out.print and System.out.println?)
A .java file can contain more than class, however only one of them can be public. Here is the example from above with
two classes:

class MyOtherClass {
static void printHello() {
System.out.print("How do ");
System.out.println("you do, ");
}
}

public class Main {


public static void main(String[] args) {
MyOtherClass.printHello();
System.out.println("fellow kids?");
}
}

You can access the static content of a class from another class by using the name of the class, as demonstrated in the
line MyOtherClass.printHello() in the example.

1.3 Types

You might already know that Python is a strongly typed language. That means that all “things” in Python have a specific
type. You can see that by entering the following statements in the Python prompt:

>>> type("hello")
<class 'str'>
>>> type(1234)
<class 'int'>
>>> type(1234.5)
<class 'float'>
>>> type(True)
<class 'bool'>

Java is a strongly typed language, too. However, there is a big difference to Python: Java is also a statically typed
language. We will not discuss all the details here, but in Java that means that most of the time you must indicate for
every variable in your program what type of “things” it can contain.
Here is a simple Python program to calculate and print the area of a square:

def calculateArea(side):
return side * side

def printArea(message, side):


(continues on next page)

6 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

(continued from previous page)


area = calculateArea(side)
print(message)
print(area)

t = 3 + 4
printArea("Area of square", t)

And here is the equivalent Java program:

public class Main {


static int calculateArea(int side) {
return side * side;
}

static void printArea(String message, int side) {


int area = calculateArea(side);
System.out.println(message);
System.out.println(area);
}

public static void main(String[] args) {


int t = 3 + 4;
printArea("Area of square", t);
}
}

Let’s see what’s going on with the types in the Java code:
• The line int calculateArea(int side) indicates that the method calculateArea has a parameter side
of type int. Furthermore, the int at the beginning of int calculateArea(... specifies that this method can
only return a value of type int. This is called the return type of the method.
• The line void printArea(String message, int side) defines that the method printArea has a parame-
ter message of type String and a parameter side of type int. The method does not return anything, therefore
it has the special return type void.
• Inside the method printArea, we can see in the line int area = calculateArea(side) that the variable
area has the type int.
• (Exercise for you: look at the types that you can see in the main method. We will explain later why that method
always has a parameter args)
IntelliJ uses a special tool called the Java compiler that carefully verifies that there are no type errors in your program,
i.e., that you have not made any mistakes in the types of the variables, method parameters, and return types in your
program. Unlike Python, this type checking is done before your program is executed. You cannot even start a Java
program that contains type errors!
Here are some examples that contain type errors. Can you find the mistakes?
• int t = "Hello";
• boolean t = calculateArea(3);
• printArea(5, "Size of square"); (This example shows why it is easier to find bugs in Java than in Python)

1.3. Types 7
Concepts of Programming with Java

1.4 The Java compiler and class files

In the previous section, we mentioned that a special tool, the Java compiler, checks your program for type errors. This
check is part of another fundamental difference between Python and Java. Python is an interpreted language. That
means that when you start a program written in Python in an IDE or on the command line with

> python myprogram.py

the Python-Interpreter will do the following things:


1. Load the file “myprogram.py”,
2. Do some checks to verify that your program doesn’t contain syntax errors such as print('Hello'))))),
3. Execute your program.
Java, being a compiled language, works differently. To execute a Java program, there is another step done before your
program can be executed:
1. First, the Java code has to be compiled. This is the job of the Java compiler, a tool that is part of the JDK. The
compiler does two things:
• It verifies that your source code is a well-formed Java program. This verification process includes the type
checking described in the previous section.
• It translates your Java source code into a more compact representation that is easier to process for your
computer. This compact representation is called a class file. One such file will be created per class in your
program. In IntelliJ, you can find the generated class files in the directory “out” in your project.
2. If the compilation of your code was successful, the Java Virtual Machine (JVM) is started. The JVM is a
special program that can load and execute class files. The JVM doesn’t need the source code (the .java files) of
your program to execute it since the class files contain all the necessary information. When you are developing
software for other people, it’s usually the class files that you give to them, not the source code.
IntelliJ runs the Java compiler and starts the JVM for you when you press the green start button, but it’s perfectly
possible to do it by hand on the command line without an IDE:

> javac Main.java # javac is the compiler and part of the JDK.
# It will generate the file Main.class

> java Main # this command starts the JVM with your Main class

1.5 Primitive Types

1.5.1 Many primitive types. . .

As explained, Java requires that you specify the type of all variables (including method parameters) and the return types
of all methods. Java differs between primitive types and complex types, such as arrays and objects. The primitive types
are used for numbers (integers and real numbers), for boolean values (true and false) and for single characters (‘a’, ‘b’,
etc.). However, there are several different number types. The below table shows all primitive types:

8 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

Type Possible values Example


int −231 ..231 − 1 int a = 3;
long −263 ..263 − 1 long a = 3;
short −215 ..215 − 1 short a = 3;
byte −27 ..27 − 1 byte a = 3;
float 1.4 * 10−45 ..3.4 * 1038 float a = 3.45f;
double 4.9 * 10−324 ..1.7 * 10308 double a = 3.45;
char 0..216 − 1 char a = 'X';
boolean true, false boolean a = true;

As you can see, each primitive type has a limited range of values it can represent. For example, a variable of type int
can be only used for integer numbers between −231 and 231 − 1. If you don’t respect the range of a type, very strange
things will happen in your program! Try this code in IntelliJ (copy it into the main method of your program):

int a = 123456789;
int b = a * 100000; // This is too large for the int type!
System.out.println(b); // What will you get here?

For most examples in this book, it will be sufficient to use int (for integer numbers) and float (for real numbers).
The types long and double provide a wider value range and more precision, but they are slower and your program
will consume more memory when running.
Java supports the usual arithmetic operations with number types, that is + (addition), - (subtraction), * (multiplication),
/ (division), and % (modulo). There is also a group of operators that can be used to manipulate integer values on bit
level (for example, left shift << and bitwise and &), but we will not discuss them further here.
The char type is used to work with individual characters (letters, digits,. . . ):

char c = 'a';

You might wonder why this type is shown in the above table as a type with values between 0 and 65535. This is because
Java represents characters by numbers following a standard called Unicode. Consequently, you can do certain simple
arithmetic operations with characters:

char c = 'a';
c++;
System.out.println(c); // prints 'b'

You can find more information about Unicode on https://en.wikipedia.org/wiki/Unicode.

1.5.2 Type casting

Java performs automatic conversions between values of different types if the destination type is “big” enough to hold
the result. This is called automatic type casting. For this reason, these two statements are allowed:

float a = 34; // the int value 34 is casted to float 34.0f


float b = 6 * 4.5f; // int multiplied by float gives float

But this is not allowed:

int a = 4.5f; // Error! float is not automatically casted to int


float b = 4.5f * 6.7; // Error! float * double gives double

1.5. Primitive Types 9


Concepts of Programming with Java

You can force the conversion by doing a manual type cast, but the result will be less precise or, in some situations, even
wrong:

int a = (int) 4.5f; // this will give 4


float b = (float) (4.5f * 6.7);

The Java class Math provides a large set of methods to work with numbers of different types. It also defines useful
constants like Math.PI. Here is an example:

double area = 123.4;


double radius = Math.sqrt(area / Math.PI);

System.out.println("Area of disk: " + area);


System.out.println("Radius of disk: " + radius);

The complete documentation of the Math class can be found at https://docs.oracle.com/en/java/javase/20/docs/api/


java.base/java/lang/Math.html.

1.5.3 What is a variable? A mental model

When working with variables of primitive types, you can imagine that every time your program reaches a line in your
code where a variable is declared, the JVM will use a small part of the main memory (RAM) of your computer to store
the value of the variable.

Java code In memory during execution


a: 3

b: 4

int a = 3;
int b = 4;

When you assign the content of a variable to another variable, the value is copied:

Java code In memory during execution


a: 4

b: 4

a = b;

The same also happens with the parameters of methods; when you call a method with arguments, for example
calculateArea(side), the argument values are copied into the parameter variables of the called method. Look
at the following program and try to understand what it does:

public class Main {


static void f(int x) {
x = x + 1;
}

public static void main(String[] args) {


int i = 3;
f(i);
(continues on next page)

10 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

(continued from previous page)


System.out.println(i);
}
}

The above program will print “3” because when you call the method f, the content of the variable i will be copied into
the parameter variable x of the method. Even if the method changes the value of x with x = x + 1, the variable i
will keep its value 3.
Note that it is illegal to use a local variable, i.e., a variable declared inside a method, before you have assigned a value
to it:
public static void main(String[] args) {
int a = 2;
int b;
int c;

int d = a * 3; // This is okay

b = 3;
int e = b * 3; // This is okay

int f = c * 3; // Error! "c" has not been initialized.


}

1.5.4 Class variables

In our examples so far, all variables were either parameter variables or local variables of a method. Such variables are
only “alive” when the program is inside the method during execution. However, you can also have variables that “live”
outside a method. These variables are called class variables because they belong to a class, not to a method. Similar
to static methods, we mark them with the keyword static:
public class Main {

static int a = 3; // this is a class variable

static void increment() {


a += 5; // this is equivalent to a = a + 5
}

public static void main(String[] args) {


increment();
System.out.println(a);
}
}

In contrast to local variables, class variables do not need to be manually initialized. They are automatically initialized
to 0 (for number types) or false (for the boolean type). Therefore, this code is accepted by the compiler:
public class Main {

static int a; // is equivalent to a = 0

(continues on next page)

1.5. Primitive Types 11


Concepts of Programming with Java

(continued from previous page)


static void increment() {
a += 5;
}

public static void main(String[] args) {


increment();
System.out.println(a);
}
}

Be careful when you have class variables and parameter or local variables with the same name:

public class Main {

static int a = 3;

static void increment(int a) {


a += 5; // this is the parameter variable
} =>3

public static void main(String[] args) {


increment(10);
System.out.println(a);
}
}

In the method “increment”, the statement a += 5 will change the value of the parameter variable a, not of the class
variable. We say that the parameter variable shadows the class variable because they have the same name. Inside the
method increment, the parameter variable a has priority over the class variable a. We say that the method is the scope
of the parameter variable.
In general, you should try to avoid shadowing because it is easy to make mistakes, but if you really need to do it for
some reason, you should know that it is still possible to access the class variable from inside the scope of the parameter
variable:

public class Main {

static int a = 3;

static void increment(int a) {


Main.a += 5; // we want the class variable!
} =>8

public static void main(String[] args) {


increment(10);
System.out.println(a);
}
}

12 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

1.6 Arrays (fr. tableaux)

1.6.1 Working with arrays

If you need a certain number of variables of the same primitive type, it can be useful to use an array type instead. Arrays
are similar to lists in python. One big difference is that when you create a new array you have to specify its size, i.e.,
the number of elements in it:

int[] a = new int[4]; // an array of integers with 4 elements

Once the array has been created, you can access its elements a[0], a[1], a[2], a[3]. Like class variables, the elements
of an array are automatically initialized when the array is created:

int[] a = new int[4]; // all elements of the array are initialized to 0


a[2] = 5;
int b = a[1] + a[2];
System.out.println(b); // prints "5" because a[1] is 0

Note that the size of an array is fixed. Once you have created it, you cannot change the number of elements in it. Unlike
Python lists, arrays in Java do not have slice() or append() methods to add or remove elements. However, we will see
later the more flexible ArrayList class.

1.6.2 Mental model for arrays

There is an important difference between array variables and primitive-type variables. An array variable does not
directly represent the array elements. Instead, an array variable can be seen as a reference to the content of the array.
You can imagine it like this:

Java code In memory during execution


a

int[] a = new int[4];

This difference becomes important when you assign an array variable to another array variable:

Java code In memory during execution


a

int[] a = new int[4];


int[] b = a;

In that case, only the reference to the array is copied, not the array itself. This means that both variables a and b
are now referencing the same array. This can be shown with the following example:

1.6. Arrays (fr. tableaux) 13


Concepts of Programming with Java

int[] a = new int[4];


int[] b = a; // a and b are now references to the same array
b[2] = 5;
System.out.println(a[2]); // prints "5"

This also works when you give an array as an argument to a method:

public class Main {

static void five(int[] x) {


x[2] = 5;
}

public static void main(String[] args) {


int[] a = new int[4];
five(a);
System.out.println(a[2]); // prints "5"
}
}

1.6.3 Initializing an array

There is a convenient way to create and initialize an array in one single step:

int[] a = { 2, 5, 6, -3 }; // an array with four elements

But note that this short form is only allowed when you initialize a newly declared array variable. If you want to create
a new array and assign it to an existing array variable, you have to use a different syntax:

int[] a = { 2, 5, 6, -3 }
a = new int[]{ 1, 9, 3, 4 };

1.6.4 Multi-dimensional arrays

Arrays can have more than one dimension. For example, two-dimensional arrays are often used to represent matrices
in mathematical calculations:

int[][] a = new int[3][5]; // this array can be used to represent a 3x5 matrix
a[2][4] = 5;

You can imagine a two-dimensional array as an array where each element is again a reference to an array:

...

An int[3][5] is therefore an array of three arrays containing five elements each. The following code illustrates this:

14 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

int[][] a = new int[3][5];


int b[] = a[0]; // b is now a reference to an int array with 5 elements
b[3] = 7;
System.out.println(a[0][3]); // b[3] and a[0][3] are the same element

Again, there is a convenient way to create and initialize multi-dimensional arrays in one step:

// 3x3 Identity matrix from the Algebra course


int[][] a = {
{ 1, 0, 0 },
{ 0, 1, 0 },
{ 0, 0, 1 }
};

1.6.5 Partially initialized arrays

It is possible to create a “partially initialized” two-dimensional array in Java:

int[][] a = new int[3][];

Again, this is an array of arrays. However, because we have only specified the size of the first dimension, the elements
of this array are initialized to null. We can initialize them later:

int[][] a = new int[3][];


a[0] = new int[5]; // 5 elements
a[1] = new int[5]; // 5 elements
a[2] = new int[2]; // 2 elements. That's allowed!

As shown in the above example, the elements of a multi-dimensional array are all arrays, but they do not need to have
the same size.

1.6.6 Arrays and class variables

Array variables can be class variables (with the static keyword), too. If you don’t provide an initial value, the array
variable will be initialized with the value null:

public class Main {

static int[] a; // automatically initialized to null

public static void main(String[] args) {


// this compiles, but it gives an error during execution,
// because we have not initialized a
System.out.println(a[2]);
}
}

You can think of the value null as representing an invalid reference.

1.6. Arrays (fr. tableaux) 15


Concepts of Programming with Java

1.7 Loops

The two most common loop constructs in Java are the while loop and the for loop.

1.7.1 While loops

The while loop in Java is very similar to its namesake in Python. It repeats one or more statements (we call them the
body of the loop) as long a condition is met. Here is an example calculating the sum of the numbers from 0 to 9 (again,
the surrounding main method is not shown):

int sum = 0;
int i = 0;
while (i<10) {
sum += i; // this is equivalent to sum = sum + i
System.out.println("Nearly there");
i++; // this is equivalent to i = i + 1
}
System.out.println("The sum is " + sum);

Warning: The two statements inside the while loop must be put in curly braces {...}. If you forget the braces, only
the first statement will be executed by the loop, independently of how the line is indented:

int sum = 0;
int i = 0;
while (i<10) // oops, we forgot to put a brace '{' here!
sum += i; // this statement is INSIDE the loop
System.out.println("Nearly there"); // this statement is OUTSIDE the loop!!!
i++; // this statement is OUTSIDE the loop!!!

System.out.println("The sum is " + sum);

This is also true for other types of loops and for if/else statements.
To avoid “accidents” like the one shown above, it is highly recommended to always use braces for the body of a
loop or if/else statement, even if the body only contains one statement.

1.7.2 Simple For loops

There are two different ways how for loops can be used. The simple for loop is often used to do something with each
element of an array or list (We will learn more about lists later):

int[] myArray = new int[]{ 2, 5, 6, -3 };


int sum = 0;
for (int elem : myArray) {
sum += elem;
}
System.out.println("The sum is " + sum);

The for loop will do as many iterations as number of elements in the array, with the variable elem successively taking
the values of the elements.

16 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

1.7.3 Complex For loops

There is also a more complex version of the for loop. Here is again our example calculating the sum of the numbers
from 0 to 9, this time with a for loop:

int sum = 0;
for (int i = 0; i<10; i++) {
sum += i;
System.out.println("Nearly there");
}
System.out.println("The sum is " + sum);

The first line of the for loop consists of three components:


1. a statement that is executed when the loop starts. In our example: int i = 0.
2. an expression evaluated before each iteration of the loop. If the expression is false, the loop stops. Here: i<10.
3. a statement that is executed after each iteration of the loop. Here: i++.
The complex for loop is more flexible than the simple version because it gives you full control over what is happening
in each iteration. Here is an example where we calculate the sum of every second element of an array:

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


int sum = 0;
for (int i = 0; i<myArray.length; i += 2) {
sum += myArray[i];
}
System.out.println("The sum is " + sum);

In this example, we have done two new things. We have used myArray.length to get the size of the array myArray.
And we have used the statement i+=2 to increase i by 2 after each iteration.

1.7.4 Stopping a loop and skipping iterations

Like in Python, you can leave any loop with the break statement:

int sum = 0;
for (int i = 0; i<10; i++) {
sum += i;
if (sum>5) {
break;
}
}

And we can immediately go to the next iteration with the continue statement:

int sum = 0;
for (int i = 0; i<10; i++) {
if (i==5) {
continue;
}
sum += i;
}

1.7. Loops 17
Concepts of Programming with Java

But you should only use break and continue if they make your program easier to read. In fact, our above example
was not a good example because you could just write:

for (int i = 0; i<10; i++) {


if (i!=5) { // easier to understand than using "continue"
sum += i;
}
}

1.8 Conditional Statements

1.8.1 If/Else statements

As you have seen in some of the examples above, Java has an if statement that is very similar to the one in Python.
Here is an example that counts the number of negative and positive values in an array:

int[] myArray = new int[]{ 2, -5, 6, 0, -4, 1 };


int countNegative = 0;
int countPositive = 0;
for(int elem : myArray) {
if(elem<0) {
countNegative++;
}
else if(elem>0) {
countPositive++;
}
else {
System.out.println("Value zero found");
}
}
System.out.println("The number of negative values is " + countNegative);
System.out.println("The number of positive values is " + countPositive);

As with loops, be careful not to forget to use curly braces {...} if the body of the if/else statement contains more than
one statement. It is highly recommended to always use braces, even if the body contains only one statement.

1.8.2 Comparison and logical operators

The if statement requires a boolean expression, i.e., an expression that evaluates to true or false. There are several
operators for boolean values that are quite similar to the ones you know from Python.

boolean b1 = 3 < 4; // we also have <, >, <=, >=, ==, !=


boolean b2 = !b1; // "not" in Python
boolean b3 = b1 && b2; // "and" in Python
boolean b4 = b1 || b2; // "or" in Python

18 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

1.8.3 Switch statement

Imagine a program where you test a variable for different values:


// two int variables that represent our position on a map
int x = 0, y = 0;

// the directions in which we want to go


char[] directions = new char[]{'N', 'S', 'S', 'E', 'E', 'W'};

// let's go!
for (char c : directions) {
if(c=='N') {
y++; // we go north
}
else if(c=='S') {
y--; // we go south
}
else if(c=='W') {
x--; // we go west
}
else if(c=='E') {
x++; // we go east
}
else {
System.out.println("Unknown direction");
}
System.out.println("The new position is " + x + " , " + y);
}

Java has a switch statement that allows you to write the above program in a clearer way:
int x = 0, y = 0;

char[] directions = new char[]{'N', 'S', 'S', 'E', 'E', 'W'};

for (char c : directions) {


switch (c) {
case 'N' -> { y++; } // we go north
case 'S' -> { y--; } // we go south
case 'W' -> { x--; } // we go west
case 'E' -> { x++; } // we go east
default -> { System.out.println("Error! Unknown direction"); }
}
System.out.println("The new position is " + x + " , " + y);
}

Note that the above code only works with Java version 14 or newer. In older Java versions, the switch statement is
much more difficult to use:
switch (c) {
case 'N':
y++;
break; // if you forget the "break", very bad things will happen!
(continues on next page)

1.8. Conditional Statements 19


Concepts of Programming with Java

(continued from previous page)


case 'S':
y--;
break;
case 'W':
x--;
break;
case 'E':
x++;
break;
default:
System.out.println("Error! Unknown direction");
}

Since Java 8 is still widely used, you should familiarize yourself with both versions of the switch statement.

1.9 Strings

1.9.1 Working with strings

Variables holding string values have the type String. Strings can be concatenated to other strings with the + operator.
This also works for primitive types:

String s1 = "This is a string";


String s2 = "This is another string";
String s3 = s1 + "---" + s2 + 12345;
System.out.println(s3);

The String class defines many interesting methods that you can use to work with strings. If you check the documen-
tation at https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/String.html, you will notice that some
methods of the String class are static and some are not. For example, the static method valueOf transforms a number
value into a string:

double x = 1.234;
String s = String.valueOf(x);
System.out.println(s);

But most methods of the String class are not static, i.e., you have to call them on a string value or string variable.
Here are some frequently used methods:

String s = "Hello world";


int l = s.length(); // the length of the string
boolean b = s.isEmpty(); // true if the string has length 0
char c = s.charAt(3); // the character in the string at position 3
boolean b2 = s.startsWith("Hello"); // true if the string starts with "Hello"
int i = s.indexOf("wo"); // gives the position of "wo" in the string
String t = s.substring(2); // the string starting at position 2

There are also some methods for strings that are located in other classes. The most useful ones are the methods to
convert strings to numbers. For int values, there is for example the static method parseInt in the Integer class:

20 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

int i = Integer.parseInt("1234");

Similar methods exist in the classes Long, Float, Double, etc. for the other primitive types. All these classes are
defined in the package java.lang for which you can find the documentation at https://docs.oracle.com/javase/20/
docs/api/java/lang/package-summary.html.

1.9.2 Mental model for strings

Like array variables, string variables are references to the content of the string:

Java code In memory during execution


a

H e l l o

String a = "Hello";

1.10 Comparing things

Primitive-type values can be tested for equality with the == operator:

int i = 3;
if( i==3 ) {
System.out.println("They are the same!");
}

However, this will not work for arrays or strings. Since array and string variables only contain references, the == operator
will compare the references, not the content of the arrays or strings! The following example shows the difference:

int i = 3;
System.out.println( i==3 ); // true. Primitive type.

int[] a = {1,2,3};
int[] b = {1,2,3};
System.out.println( a==b ); // false. Two different arrays.

int[] c = a;
System.out.println( a==c ); // true. Same reference.

String s1 = "Hello" + String.valueOf(1234);


String s2 = "Hello1234";
System.out.println( s1==s2 ); // false. Two different strings.

Comparing arrays or strings with == is a very common mistake in Java. Be careful!


To compare the content of two strings, you must use their equals method:

String s1 = "Hello" + String.valueOf(1234);


String s2 = "Hello1234";
System.out.println( s1.equals(s2) ); // true

1.10. Comparing things 21


Concepts of Programming with Java

There is also an equals method to compare the content of two arrays, but it is a static method of the class Arrays in
the package java.util. To use this class, you have to import it into your program. Here is the complete code:

import java.util.Arrays;

public class Main {


public static void main(String[] args) {
int[] a = {1,2,3};
int[] b = {1,2,3};
System.out.println( Arrays.equals(a,b) ); // true
}
}

The Arrays class contains many useful methods to work with classes, such as methods to set all elements of an
array to a certain value, to make copies of arrays, or to transform an array into a string. See the documentation at
https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/Arrays.html.
You might wonder why we need the line import java.util.Arrays but we didn’t need to import the classes Math,
Integer or String in our other examples. That’s because those classes are in the package java.lang, which is the
only class that is automatically imported by the Java compiler.

1.11 Classes and Objects

1.11.1 Creating your own objects

Computer programs are about organizing data and working with that data. In some cases the primitive types, arrays
and strings are enough, but often you have data that is more complex than that. For example, imagine a program to
manage employees in a company. We can describe the fact that each employee has a name and a salary, in a new class
in our Java program:

class Employee {
String name; // the name of the employee
int salary; // the salary of the employee
}

Classes allow us to create new objects from them. In our example, each object of the class Employee represents an
employee, which makes it easy to organize our data:

class Employee {
String name;
int salary;
}

public class Main {


public static void main(String[] args) {
Employee person1 = new Employee(); // a new object!
person1.name = "Peter";
person1.salary = 42000;

Employee person2 = new Employee(); // a new object!


person2.name = "Anna";
person2.salary = 45000;
(continues on next page)

22 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

(continued from previous page)

int salaryDifference = person1.salary - person2.salary;


System.out.println("The salary difference is " + salaryDifference);
}
}

The two objects that we created and put into the local variables person1 and person2 are called instances of the class
Employee, and the two variables name and age are called instance variables of the class Employee. Since they are
not static, they belong to the instances, and each instance has its own name and age.

1.11.2 Initializing objects

In the above example, we first created the object, and then set the values of its instance variables:

Employee person1 = new Employee();


person1.name = "Peter";
person1.salary = 42000;

Like static variables, instance variables are automatically initialized with the value 0 (for number variables), with false
(for boolean variables), or with null (for all other types). In our example, this is dangerous because we could forget
to specify the salary of the employee:

Employee person1 = new Employee();


person1.name = "Peter";
// oops, the salary is 0

There are several ways to avoid this kind of mistake. One way is to initialize the variable in the class definition:

class Employee {
String name;
int salary = 10000;
}

Of course, this is only useful if you want that all employees start with a salary of 10000. The other way is to define
a constructor in your class. The constructor is a special method that has the same name as the class. It can have
parameters but it has no return type:

class Employee {
String name;
int salary;

// the constructor
Employee(String n, int s) {
this.name = n;
this.salary = s;
}
}

If you provide a constructor for your class, the Java compiler will verify that you use it to create new objects:

Employee person1 = new Employee("Peter", 42000);


// Okay. We have now a new employee with
(continues on next page)

1.11. Classes and Objects 23


Concepts of Programming with Java

(continued from previous page)


// person1.name "Peter"
// person1.salary 42000

Employee person2 = new Employee(); // not allowed. You must use the constructor!

In our example, the constructor took two parameters n and s and used them to initialize the instance variables name
and salary of a new Employee:code: object. But how does the constructor know which object to initialize? Do
we have to tell the constructor that the new object is in the variable person1? Fortunately, it’s easier than that. The
constructor can always access the object being constructed by using the keyword this. Therefore, the line

this.name = n;

means that the instance variable name of the new object will be initialized to the value of the parameter variable n. We
could even use the same names for the parameter variables and for the instance variables:

class Employee {
String name;
int salary;

Employee(String name, int salary) {


this.name = name;
this.salary = salary;
}
}

Like for class variables, we have to be careful with shadowing. Without this. in front of the variable name, the Java
compiler will assume that you mean the parameter variable. It’s a common mistake to write something like:

class Employee {
String name;
int salary;

Employee(String name, int salary) {


name = name; // oops, this.name is not changed here!
salary = salary;
}
}

24 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

1.12 Mental model

Like array variables and String variables, object variables contain a reference to the object in your computer’s main
memory. The object itself contains the instance variables. Note that an instance variable can be again a reference. For
our employee “Peter”, we get the following structure:

Java code In memory during execution


variable
person1

instance
ofclassEmployee
name salary
: 42000

string
"Peter"

Employee person1 =
new Employee("Peter", 42000);

Because of this, what we have already said about array variables and String variables also holds for object variables:
Assigning an object variable to another variable only copies the reference and comparing two object variables only
compares the references, not the content of the objects:

Employee person1 = new Employee("Peter", 42000);


Employee person2 = new Employee("Peter", 42000);
System.out.println( person1==person2 ); // false. Two different objects.

Employee person3 = person1;


System.out.println( person1==person3 ); // true. Same object.

1.13 Working with objects

Many things that you can do with primitive types and strings, you can also do them with objects. For example, you can
create arrays of objects. The elements of a new array of objects are automatically initialized to null, as shown in this
example:

Employee[] myTeam = new Employee[3];


myTeam[0] = new Employee("Peter", 42000);
myTeam[1] = new Employee("Anna", 45000);
System.out.println(myTeam[0].name); // is "Peter"
System.out.println(myTeam[1].name); // is "Anna"
System.out.println(myTeam[2].name); // Error! myTeam[2] is null

You can also have class variables and instance variables that are object variables. Again, they will be automatically
initialized to null, if you don’t provide an initial value. In the following example, we have added a new instance
variable boss to our Employee:

class Employee {
String name;
(continues on next page)

1.12. Mental model 25


Concepts of Programming with Java

(continued from previous page)


int salary;
Employee boss;

Employee(String name, int salary, Employee boss) {


this.name = name;
this.salary = salary;
this.boss = boss;
}
}

public class Main {


public static void main(String[] args) {
// Anna has no boss
Employee anna = new Employee("Anna", 45000, null);

// Anna is the boss of Peter


Employee peter = new Employee("Peter", 42000, anna);
}
}

Exercise for you: Take a sheet of paper and draw the mental model graph for the object representing Peter.
Question: In the above example, what value do we give to the boss:code: instance variable of an employee who has
no boss?

1.14 Methods

In the following example, we define a static method increaseSalary to increase the salary of an employee:

class Employee {
String name;
int salary;

Employee(String name, int salary) {


this.name = name;
this.salary = salary;
}
}

public class Main {


static void increaseSalary(Employee employee, int raise) {
// we only raise the salary if the raise is less than 10000
if (raise<10000) {
employee.salary += raise;
}
}

public static void main(String[] args) {


Employee anna = new Employee("Anna", 45000);
Employee peter = new Employee("Peter", 45000);

(continues on next page)

26 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

(continued from previous page)


// Anna and Peter get a salary raise
increaseSalary(anna, 2000);
increaseSalary(peter, 3000);

System.out.println("New salary of Anna is "+anna.salary);


System.out.println("New salary of Peter is "+peter.salary);
}
}

The above code works. But in Object-Oriented Programming (OOP) languages like Java, we generally prefer that
all methods that modify instance variables of an object should be put inside the class definition. In large program,
this makes it easier to understand who is doing what with an object. To implement this, we replace the static method
increaseSalary of the Main class by a non-static method in the Employee class:

class Employee {
String name;
int salary;

Employee(String name, int salary) {


this.name = name;
this.salary = salary;
}

void increaseSalary(int raise) {


if (raise<10000) {
this.salary += raise;
}
}
}

public class Main {


public static void main(String[] args) {
Employee anna = new Employee("Anna", 45000);
Employee peter = new Employee("Peter", 45000);

// Anna and Peter get a salary raise


anna.increaseSalary(2000);
peter.increaseSalary(3000);

System.out.println("New salary of Anna is "+anna.salary);


System.out.println("New salary of Peter is "+peter.salary);
}
}

Because increaseSalary is now a non-static method of Employee, we can directly call it on an Employee object.
No parameter employee is needed because, inside the method, the this keyword is a reference to the object on which
the method has been called. Therefore, we just write anna.increaseSalary(2000) to change the salary of Anna.

1.14. Methods 27
Concepts of Programming with Java

1.14.1 Restricting access

The nice thing about our increaseSalary method is that we can make sure that raises are limited to 10000 Euro :)
However, nobody stops the programmer to ignore that method and manually change the salary:

Employee anna = new Employee("Anna", 45000, null);


anna.salary += 1500000; // ha!

This kind of mistake can quickly happen in a large program with hundreds of classes. We can prevent this by declaring
the instance variable salary as private:

class Employee {
String name;
private int salary;

Employee(String name, int salary) {


this.name = name;
this.salary = salary;
}

void increaseSalary(int raise) {


if (raise<10000) {
this.salary += raise;
}
}
}

A private instance variable is only accessible inside the class. So the access anna.salary += 150000 in the
Main:code: class doesn’t work anymore. Mission accomplished. . .
Unfortunately, that’s a bit annoying because it also means that we cannot access anymore Anna’s salary in System.
out.println("New salary of Anna is "+anna.salary). To fix this, we can add a method getSalary:code:
whose only purpose is to give us the value of the private salary variable. Here is the new version of the code:

class Employee {
String name;
private int salary;

Employee(String name, int salary) {


this.name = name;
this.salary = salary;
}

void increaseSalary(int raise) {


if (raise<10000) {
this.salary += raise;
}
}

int getSalary() {
return this.salary;
}
}

(continues on next page)

28 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

(continued from previous page)


public class Main {
public static void main(String[] args) {
Employee anna = new Employee("Anna", 45000);

anna.increaseSalary(2000);

System.out.println("New salary of Anna is "+anna.getSalary());


}
}

1.15 Inheritance

1.15.1 Creating subclasses

Let’s say we are writing a computer game, for example an RPG. We implement weapons as objects of the class Weapon.
The damage that a weapon can deal depends on its level. The price of a weapon also depends on its level. The code
could look like this:

class Weapon {
private int level;
private String name;

Weapon(String name, int level) {


this.name = name;
this.level = level;
}

int getPrice() {
return this.level * 500;
}

int getSimpleDamage() {
return this.level * 10;
}

int getDoubleDamage() {
return this.getSimpleDamage() * 2;
}
}

public class Main {


public static void main(String[] args) {
Weapon weapon;

weapon = new Weapon("Small dagger", 2);


System.out.println("Price is " + weapon.getPrice());
System.out.println("Simple damage is " + weapon.getSimpleDamage());
System.out.println("Double damage is " + weapon.getDoubleDamage());
}
}

1.15. Inheritance 29
Concepts of Programming with Java

Before you continue, carefully study the above program and make sure that you understand what it does. Run
it in IntelliJ. Things are about to get a little more complicated in the following!
In our game, there is also a special weapon type, the Mighty Swords. These swords always deal a damage of 1000,
independently of their level. In Java, we can implement this new weapon type like this:

class MightySword extends Weapon {


MightySword(String name, int level) {
super(name,level);
}

@Override
int getSimpleDamage() {
return 1000;
}
}

According to the first line of this code, the class MightySword extends the class Weapon. We say that MightySword is
a subclass (or subtype) of Weapon, or we can say that Weapon is a superclass of MightySword. In practice, this means
that everything we can do with objects of the class Weapon we can also do with objects of the class MightySword:

public static void main(String[] args) {


Weapon weapon;

weapon = new MightySword("Magic sword", 3);


System.out.println("Price is " + weapon.getPrice());
System.out.println("Simple damage is " + weapon.getSimpleDamage());
System.out.println("Double damage is " + weapon.getDoubleDamage());
}

At first glance, there seems to be a mistake in the above “main” method. Why is the line

weapon = new MightySword("Magic sword", 3);

not a type error? On the left, we have the variable weapon of type Weapon and on the right we have a new object of
MightySword. But this is acceptable for the compiler because, Java has the following rule:
Rule 1: A variable of type X can hold a reference to an object of class X or to an object of a subclass of X.
Because of rule 1, the compiler is perfectly happy with putting a reference to a MightySword object in a variable
declared as type Weapon. For Java, MightySword instances are just special Weapon instances.
The next line of the “main” method looks strange, too:

System.out.println("Price is " + weapon.getPrice());

Our class MightySword has not defined a method getPrice so why can we call weapon.getPrice()? This is another
rule in Java:
Rule 2: The subclass inherits the methods of its superclass. Methods defined in a class X can be also used on
objects of a subclass of X.
Let’s look at the next line. It is:

System.out.println("Simple damage is " + weapon.getSimpleDamage());

Just by looking at this line and the line Weapon weapon at the beginning of the main method, you might expect that
weapon.getSimpleDamage() calls the getSimpleDamage method of the Weapon class. However, if you check the

30 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

output of the program, you will see that the method getSimpleDamage of the class MightySword is called. Why?
Because weapon contains a reference to a MightySword object. The rule is:
Rule 3: Let x be a variable of type X (where X is a class) and let’s assign an object of class Y (where Y is a
subclass of X) to x. When you call a method on x and the method is defined in X and in Y, the JVM will execute
the method defined in Y.
For instances of the class MightySword, calling getSimpleDamage:code: will always execute the method defined
in that class. We say that the method getSimpleDamage in MightySword overrides the method definition in the class
“Weapon”. For that reason, we have marked the method in MightySword with the so-called @Override annotation.
With the above three rules, can you guess what happens in the next line?

System.out.println("Double damage is " + weapon.getDoubleDamage());

According to rule 2, the class MightySword inherits the method getDoubleDamage of the class Weapon. So, let’s
check how that method was defined in the class Weapon:

int getDoubleDamage() {
return this.getSimpleDamage() * 2;
}

The method calls this.getSimpleDamage(). Which method getSimpleDamage will be called? The one defined
in Weapon or the one in MightySword? To answer this question, you have to remember rule 3! The this in this.
getSimpleDamage() refers to the object on which the method was called. Since our method is an object of the class
MightySword, the method getSimpleDamage of MightySword will be called. The fact that “getDoubleDamage” is
defined in the class Weapon does not change rule 3.

1.15.2 Super

There is one thing left in our MightySword class that we have not yet explained. It’s the constructor:

class MightySword extends Weapon {

MightySword(String name, int level) {


super(name,level);
}

...
}

In the constructor, the keyword super stands for the constructor of the superclass of MightySword, that is Weapon.
Therefore, the line super(name,level) simply calls the constructor as defined in Weapon.
super can be also used in methods. Imagine we want to define a new weapon type Expensive Weapon that costs exactly
100 more than a normal weapon. We can implement it as follows:

class ExpensiveWeapon extends Weapon {

ExpensiveWeapon(String name, int level) {


super(name,level);
}

@Override
int getPrice() {
(continues on next page)

1.15. Inheritance 31
Concepts of Programming with Java

(continued from previous page)


return super.getPrice() + 100;
}
}

The expression super.getPrice() calls the method getPrice as defined in the superclass Weapon. That means that
the keyword super can be used to call methods of the superclass, which would normally not be possible for overridden
methods because of rule 3.

1.15.3 The @Override annotation

This @Override annotation is not strictly necessary in Java (the compiler doesn’t need it), but it helps you to avoid
mistakes. For example, imagine you made a spelling error when you wrote the name of getSimpleDamage:

class MightySword extends Weapon {


MightySword(String name, int level) {
super(name,level);
}

@Override
int getSimpleDamag() { // oops, we forgot the "e" in "getSimpleDamage"
return 1000;
}
}

Because of your spelling error, the above code actually does not override anything. It just introduces a new method
getSimpleDamag. But thanks to the @Override annotation, IntelliJ can warn us that there is a problem.

1.15.4 Extending, extending,. . .

A subclass cannot only override methods of its superclass, it can also add new instance variables and new methods.
For example, we can define a new type of Mighty Swords that can do magic damage:

class MagicSword extends MightySword {


private int magicLevel;

MagicSword(String name, int level, int magicLevel) {


super(name,level); // call the constructor of MightySword
this.magicLevel = magicLevel;
}

int getMagicDamage() {
return this.magicLevel * 5;
}
}

As you can see, you can create subclasses of subclasses. Note that the constructor uses again super to first call the
constructor of the superclass and then initializes the new instance variable magicLevel.
How can we call the method getMagicDamage? Can we do this:

Weapon weapon = new MagicSword("Elven sword", 7, 3);


System.out.println(weapon.getMagicDamage());

32 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

The answer is no! Rule 3 is only applied to methods that are defined in the subclass and in the superclass. This is not the
case for getMagicDamage. In this situation, the Java compiler will not accept the call weapon.getMagicDamage()
because, just by looking at the variable declaration Weapon weapon, it cannot tell that the object referenced by the
variable weapon really has a method getMagicDamage. You might think that the compiler is a bit stupid here, but
remember that this is just a simple example and the programmer could try to do some strange things that are difficult
to see for the compiler:

Weapon weapon = new MagicSword("Elven sword", 7, 3);


weapon = new Weapon("Dagger", 1);
System.out.println(weapon.getMagicDamage()); // does not compile, fortunately!

To be able to call getMagicDamage, you have to convince the compiler that the variable contains a reference to a Magic
Sword object. For example, you could change the type of the variable:

MagicSword weapon = new MagicSword("Elven sword", 7, 3);


System.out.println(weapon.getMagicDamage());

In this way, it’s 100% clear for the compiler that the variable definitely refers to a MagicSword object (or to an object
of a subclass of MagicSword; remember rule 1).
Alternatively, you can do a type cast:

Weapon weapon = new MagicSword("Elven sword", 7, 3);


System.out.println(((MagicSword) weapon).getMagicDamage());

However, be careful with type casts. The compiler will accept them but if you do a mistake, you will get an error during
program execution:

Weapon weapon = new Weapon("Dagger", 1);


System.out.println(((MagicSword) weapon).getMagicDamage()); // oh oh...

1.16 Polymorphism

The three rules make it possible to write code and data structures that can be used with objects of different classes. For
example, thanks to rule 1, you can define an array that contains different types of weapons:

Weapon[] inventory = new Weapon[3];


inventory[0] = new Weapon("Dagger", 2);
inventory[1] = new MagicSword("Elven sword", 7, 3);
inventory[2] = new ExpensiveWeapon("Golden pitchfork", 3);

And thanks to rule 2 and 3, you can write methods that work for different types of weapons:

int getPriceOfInventory(Weapon[] inventory) {


int sum = 0;
for (Weapon weapon : inventory) {
sum += weapon.getPrice();
}
return sum;
}

Although the above method getPriceOfInventory looks like it is only meant for objects of class Weapon, it also
works for all subclasses of Weapon. This is called Subtype Polymorphism. If you have for example an object of

1.16. Polymorphism 33
Concepts of Programming with Java

class ExpensiveWeapon in the array, rule 3 will guarantee that weapon.getPrice() will call the method defined in
ExpensiveWeapon.
The conclusion is that there is a difference between what the compiler sees in the source code and what actually happens
when the program is executed. When the compiler sees a method call like weapon.getPrice() in your source code
it only checks whether the method exists in the declared type of the variable. But during program execution, what is
important is which object is actually referenced by the variable. We say that type checking by the compiler is static,
but method calls by the JVM are dynamic.

1.17 The class hierarchy

If we take all the different weapon classes that we created in the previous examples, we get a so-called class hierarchy
that shows the subclass-superclass relationship between them:

Object

Weapon

ExpensiveWeaponMightySword

MagicSword

The class Object that is above our Weapon class was not defined by us. It is automatically created by Java and is the
superclass of all non-primitive types in Java, even of arrays and strings! A variable of type Object therefore can refer
to any non-primitive value:

Object o;
o = "Hello"; // okay
o = new int[]{1,2,3}; // okay, too
o = new MagicSword("Elven sword", 7, 3); // still okay!

The documentation of Object can be found at https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/


Object.html. The class defines several interesting methods that can be used on all objects. One of them is the toString
method. This method is very useful because it is called by frequently used methods like String.valueOf and System.
out.println when you call them with an object as parameter. Therefore, if we override this method in our own class,
we will get a nice output:

class Player {
private String name;
private int birthYear;

Player(String name, int birthYear) {


this.name = name;
this.birthYear = birthYear;
}

@Override
public String toString() {
return "Player " + this.name + " born in " + this.birthYear;
}
}

(continues on next page)

34 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

(continued from previous page)


public class Main {
public static void main(String[] args) {
Player peter = new Player("Peter", 1993);
System.out.println(peter); // this will call toString() of Player
}
}

The method toString is declared as public in the class Object and, therefore, when we override it we have to declare
it as public, too. We will talk about the meaning of public later.
Another interesting method defined by Object is equals. We have already learned that we have to use the method
equals when we want to compare the content of two strings because the equality operator == only compares references.
This is also recommended for your own objects. However, comparing objects is more difficult than comparing strings.
For our class Player shown above, when are two players equal? The Java language cannot answer this question for
us, so we have to provide our own implementation of equals. For example, we could say that two Player objects are
equal if they have the same name and the same birth year:
import java.util.Objects;

class Player {
private String name;
private int birthYear;

Player(String name, int birthYear) {


this.name = name;
this.birthYear = birthYear;
}

@Override
public boolean equals(Object obj) {
if (this==obj) {
return true; // same object!
}
else if (obj==null) {
return false; // null parameter
}
else if (this.getClass()!=obj.getClass()) {
return false; // different types
}
else {
Player p = (Player) obj;
return p.name.equals(this.name) && p.birthYear==this.birthYear;
}
}

@Override
public int hashCode() {
return Objects.hash(this.name, this.birthYear);
}
}

public class Main {


public static void main(String[] args) {
(continues on next page)

1.17. The class hierarchy 35


Concepts of Programming with Java

(continued from previous page)


Player peter1 = new Player("Peter", 1993);
Player peter2 = new Player("Peter", 1993);
System.out.println( peter1.equals(peter2) ); // true
System.out.println( peter1.equals("Hello") ); // false
System.out.println( peter1.equals(null) ); // false
}
}

What’s happening in the above code? One difficulty with equals is that it can be called with a null argument or with
an object that is not an instance of Player. So, before we can compare the name and the birth year of a Player object
with another Player object, we first have to do some tests. One of them is whether the object on which equals was
called (this) and the other object (obj) have the same type:

else if (this.getClass()!=obj.getClass()) {

If all those tests pass we can finally compare the name and birth year of this and the other Player object.
Note that there are some other difficulties with equals that we will not discuss here. They are related to the hashCode
method that you have to always override together with equals, as shown above.

1.18 ArrayList and Boxing

1.18.1 ArrayList

Using the class Object can be useful in situations where we want to write methods that work with all types of objects.
For example, we have seen before that a disadvantage of arrays in Java over lists in Python is that arrays cannot change
their size. In the package java.util, there is a class ArrayList that can do that:

import java.util.ArrayList;

public class Main {


public static void main(String[] args) {
ArrayList list = new ArrayList();

// add two elements to the end of the list


list.add("Hello");
list.add(new int[]{1,2,3});

System.out.println( list.size() ); // number of elements


System.out.println( list.get(0) ); // first element
}
}

As you can see in the above example, the method add of ArrayList accepts any reference (including to arrays and
strings) as argument. Very simplified, you can imagine that the ArrayList class looks like this:

public class ArrayList {


// the added elements
private Object[] elements;

public void add(Object obj) {


(continues on next page)

36 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

(continued from previous page)


// this method adds "obj" to the array
// ...
}

public Object get(int index) {


// this method returns the object at position "index"
// ...
}
}

1.18.2 For loops on ArrayList

for loops also work on “ArrayList”:

ArrayList list = new ArrayList();


list.add("Hello");
list.add("World");

// simple for loop


for (Object obj : list) {
System.out.println(obj);
}

// complex for loop


for (int i=0; i<list.size(); i++) {
System.out.println( list.get(i) );
}

1.18.3 Boxing and unboxing

Unfortunately, primitive types are not subclasses of Object. Therefore, we cannot simple add an int value to an
ArrayList, at least not without the help of the compiler:

list.add(3); // does that work?

One way to solve this problem is to write a new class with the only purpose to store the int value in an object that we
can then add to the list:

class IntObject {
int value;

IntObject(value) {
this.value = value;
}
}

public class Main {


public static void main(String[] args) {
ArrayList list = new ArrayList();

(continues on next page)

1.18. ArrayList and Boxing 37


Concepts of Programming with Java

(continued from previous page)


list.add(new IntObject(3));
}
}

This trick is called boxing because we put the primitive-type value 3 in a small “box” (the :code: IntObject object). For-
tunately, we actually don’t have to write our own class IntObject, because the java.lang package already contains
a class that does exactly that:
// Integer is a class defined in the java.lang package
Integer value = Integer.valueOf(3);
list.add(value);

The java.lang package also contains equivalent classes Long, Float, etc. for the other primitive types.
Note that boxing is quite cumbersome and it is only needed in Java because primitive types are not subclasses of
Object. However, we get a little bit of help from the compiler. In fact, the compiler can do the boxing for you. This is
called autoboxing. You can just write:
list.add(3); // this automatically calls "Integer.valueOf(3)"

Autoboxing is not limited to the ArrayList class. It works for all situations where you assign a primitive-type value to
a variable that has a matching class type. The opposite direction, unboxing, is also done automatically by the compiler:
// autoboxing
// this is identical to:
// Integer value = Integer.valueOf(3);
Integer value = 3;

// auto-unboxing
// this is identical to:
// int i = value.intValue();
int i = value;

1.18.4 ArrayList and Generics

The way ArrayList uses Object to be able to store all kinds of objects has a big disadvantage. Since the get method
has the return type Object, we have to do a type cast if we want again the original type of the object that we added to
the list:
ArrayList list = new ArrayList();

list.add("Hello");
list.add("World");

int len = ((String) list.get(0)).length();

Although we know that the list only contains strings, the compiler needs the typecast before we can call the method
length. This is not only cumbersome, but can also lead to errors that only appear when the program is executed.
Fortunately, Java has a feature called Generics that allows us to simplify the above code:

ArrayList<String> list = new ArrayList<String>();

(continues on next page)

38 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

(continued from previous page)


list.add("Hello");
list.add("World");

int len = list.get(0).length();

The form ArrayList<String> tells the compiler that the add method of our list will only accept strings as argument
and the get method will only return strings. In that way, the type cast is not needed anymore (actually, the type cast is
still done but you don’t see it because the compiler automatically adds it in the class file).
You will see more examples of Generics later in this book. To give you a first taste, let’s see what the ArrayList class
looks like in reality:

public class ArrayList<E> { // type parameter E


private Object[] elements;

public void add(E obj) {


// ...
}

public E get(int index) {


// ...
}
}

The E that you can see in the first line and in the method definitions is a type parameter. It represents the type of the
element that we want to store in the list. By creating our list with

ArrayList<String> list = new ArrayList<String>();

we are telling the compiler that it should assume that E = String, and accordingly the methods add and get will be
understood as void add(String obj) and String get(int index).

1.19 Method overloading

1.19.1 Overloading with different parameters

In Java, it is allowed to have two methods with the same name as long as they have different parameters. This is called
method overloading. Here is an example:

class Player {
private String name;
private int birthYear;

Player(String name, int birthYear) {


this.name = name;
this.birthYear = birthYear;
}

public void set(String name) {


this.name = name;
}
(continues on next page)

1.19. Method overloading 39


Concepts of Programming with Java

(continued from previous page)

public void set(String name, int birthYear) {


this.name = name;
this.birthYear = birthYear;
}
}

If we call the set method, the Java compiler knows which of the two methods you wanted to call by looking at the
parameters:

Person person = new Person("Peter", 1993);


person.set("Pierre", 1993); // this is the set method with parameters String and int

1.19.2 Overloading with subclass parameters

You have to be careful when you write overloaded methods where the parameters are classes and subclasses. Here is a
minimal example of a Player class with such an overloaded method:

class Weapon {
// ...
}

class MightySword extends Weapon {


// ...
}

class Player {
Weapon weapon;
int power;

void giveWeapon(Weapon weapon) {


this.weapon = weapon;
this.power = 0;
}

void giveWeapon(MightySword weapon) {


this.weapon = weapon;
this.power = 10; // a Mighty Sword increases the power of the player
}
}

public class Main {


public static void main(String[] args) {
Player player = new Player();

Weapon weapon = new MightySword();


player.giveWeapon(weapon);

System.out.println(player.power);
}
}

40 Chapter 1. Part 1: From Python to Java


Concepts of Programming with Java

What will System.out.println(player.power) print after we gave a Mighty Sword to the player?
Surprisingly, it will print “0”. The method void giveWeapon(MightySword weapon) is not called although we
called giveWeapon with a MightySword object! The explanation for this is that the Java compiler only looks at the
type of the variable as declared in the source code when deciding which method to call. In our example, the type of
the variable weapon is Weapon, therefore the method void giveWeapon(Weapon weapon) is called. The compiler
ignores that the variable will contain a reference to a MightySword object during program execution.
Lesson learned: Method calls in Java are only dynamically decided for the object on which the method is called
(remember rule 3!). They are not dynamic for the arguments of the method.
The correct way to call giveWeapon for Mighty Swords is:

MightySword weapon = new MightySword();


player.giveWeapon(weapon);

or just

player.giveWeapon(new MightySword());

1.19.3 Overloading with closest match

What happens if we call an overloaded method but there is no version of the method that exactly matches the type of the
argument? Here is the same example as above, but with a third class MagicSword that is a subclass of MightySword:

class Weapon {
// ...
}

class MightySword extends Weapon {


// ...
}

class MagicSword extends MightySword {


// ...
}

class Player {
Weapon weapon;
int power;

void giveWeapon(Weapon weapon) {


this.weapon = weapon;
this.power = 0;
}

void giveWeapon(MightySword weapon) {


this.weapon = weapon;
this.power = 10;
}
}

public class Main {


public static void main(String[] args) {
(continues on next page)

1.19. Method overloading 41


Concepts of Programming with Java

(continued from previous page)


Player player = new Player();

player.giveWeapon(new MagicSword());

System.out.println(player.power);
}
}

Which one of the two giveWeapon will be called if the argument is a MagicSword object? In this situation, the compiler
will choose the method with the closest type to MagicSword, that is void giveWeapon(MightySword weapon).

42 Chapter 1. Part 1: From Python to Java


CHAPTER

TWO

PART 2: UNIT TESTING

Focus on JUnit 5

2.1 Subtitle

Blabla

43
Concepts of Programming with Java

44 Chapter 2. Part 2: Unit testing


CHAPTER

THREE

PART 3: DATA-STRUCTURES AND ALGORITHMS

3.1 Time Complexity

In the rapidly evolving world of computer science, the efficiency of an algorithm is paramount. As we strive to tackle
increasingly complex problems and manage growing volumes of data, understanding how our algorithms perform
becomes more important than ever.
This is where the concept of time complexity comes into play.
Time complexity provides a theoretical estimation of the time an algorithm requires to run relative to the size of the
input data. In other words, it allows us to predict the efficiency of our code before we even run it. It’s like having a
magic crystal ball that tells us how our algorithm will behave in the wild!
Let’s delve into the intricacies of time complexity and uncover the beauty and elegance of efficient code by studying
first a very simple sum mehtod that calculates the total sum of all the elements in an integer array provided in argument.

Listing 1: The sum method


public class Main {
public static int sum(int [] values) {
int total = 0;
for (int i = 0; i < values.length; i++) {
total += values[i];
}
return total;
}
}

One can measure the time it takes using System.currentTimeMillis() method that returns the current time in milliseconds
since the Unix Epoch (January 1, 1970 00:00:00 UTC). It is typically used to get a timestamp representing the current
point in time. Here is an example of how to use it to measure the time of one call to the sum method.

Listing 2: Measuring the time of sum with currentTimeMillis


public class Main {
public static void main(String[] args) {
int[] values = {1, 2, 3, 4, 5};
long startTime = System.currentTimeMillis();
int totalSum = sum(values);
long endTime = System.currentTimeMillis()
long duration = (endTime - startTime); // duration in milliseconds
}
}

45
Concepts of Programming with Java

Now, if one makes vary the size of values one can observe the evolution of execution time in function of the size of the
input array given in argument to sum and plot it. Here is what we obtain on a standard laptop.

Fig. 1: Evolution of time measures taken by sum on arrays of increasing size.

Undoubtedly, the absolute time is heavily reliant on the specifications of the machine the code is executed on. The
same code running on a different laptop could produce different timing results. However, it is noteworthy that the time
evolution appears to be linear with respect to the array size, as illustrated by the trend line. A crucial question arises:
could this have been foreseen without even running the code? The answer is affirmative! Hartmanis and Stearns
[HS65] layd down the foundations for such theoretical analyses from the source-code without (or pseudo-code, as
the algorithm itself is of greater significance) even without requiring running the code and measure time. This great
invention is explained next, but first things first, we need a simple computation model.

3.1.1 The Random Access Machine (RAM) model of computation

The RAM, or Random Access Machine, model of computation is a theoretical model of a computer that provides
a mathematical abstraction for algorithm analysis. In the RAM model, each ‘simple’ operation (such as addition,
subtraction, multiplication, division, comparison, bitwise operations, following a reference, or direct addressing of
memory) can be done in a single unit of time. It assumes that memory accesses (like accessing an element in an array:
value[i] obove) take constant time, regardless of the memory location. This is where it gets the name “random access”,
since any memory location can be accessed in the same amount of time.
This abstraction is quite realistic for many practical purposes, and closely models real computers (a bit like Newton
laws is a good approximation of general relativity).
Of course we can’t assume a loop is a ‘simple’ operation in the RAM model. One need to count the number of times
its body will be executed. The next code add comments on the number of steps required to execute the sum algorithm.

Listing 3: The sum method with step annotations


public static int sum(int [] values) { // n = values.length
int total = 0; // 1 step
for (int i = 0; i < values.length; i++) {
total += values[i]; // 2* n steps (one memory access and one␣
˓→addition executed n times)

}
return total; // 1 step
} // TOTAL: 2n + 2 steps

In practice, it is difficult to translate one step into a concrete time since it depends on many factors (machine, language,
compiler, etc). It is also not true that every operation takes exactly the same amount of time. Remember that it is

46 Chapter 3. Part 3: Data-Structures and Algorithms


Concepts of Programming with Java

just an approximation. We’ll further simplify our step-counting approach by utilizing classes of functions that easily
interpretable for practitioners like us.
Let us first realize in the next section that even for a consistent input size, the execution time of an algorithm can vary
significantly.

3.1.2 The Best-Case, worst case execution of an algorithm

Different inputs of the same size may cause an algorithm to take more or fewer steps to arrive at a result.
To illustrate this, consider the linearSearch method looking if an array contains a specific target value and returning
the first index having this value, or -1 if this value is not present in the array. It achieves this by iterating through the
array and returning the index of the first occurrence of the target value. If the target value isn’t present, it returns -1.

Listing 4: Linear Search algorithm


/**
* This method performs a linear search on an array.
*
* @param arr The input array.
* @param x The target value to search for in the array.
* @return The index of the target value in the array if found,
* or -1 if the target value is not in the array.
*/
public static int linearSearch(int[] arr, int x) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == x) {
return i;
}
}
return -1;
}

In this case, the number of steps the ‘linearSearch’ method takes to complete is heavily dependent on the position of the
target value within the array. If the target value is near the beginning of the array, the ‘linearSearch’ method completes
quickly. We call this the best-case scenario.
Conversely, if the target value is at the end of the array or not present at all, the method must iterate through the entire
array, which naturally takes more steps. We call this, the worst-case scenario.
For other algorithms, the number of operations required is primarily determined by the input size rather than the input
content. This characteristic is exemplified by the ‘sum’ method we previously analyzed.
The notation we are about to introduce for characterizing an algorithm’s execution time should allow us to represent
both the best and worst-case scenarios.

3.1. Time Complexity 47


Concepts of Programming with Java

3.1.3 The Big-O, Big-Omega and Big-Theta Clases of Functions

Let us assume that the number of steps an algorithm requires can be represented by the function 𝑇 (𝑛) where 𝑛 refers
to the size of the input, such as the number of elements in an array. While this function might encapsulate intricate
details about the algorithm’s execution, calculating it with high precision can be a substantial undertaking, and often,
not worth the effort.
For sufficiently large inputs, the influence of multiplicative constants and lower-order terms within the exact runtime
is overshadowed by the impact of the input size itself. This leads us to the concept of asymptotic efficiency, which is
particularly concerned with how an algorithm’s running time escalates with an increase in input size, especially as the
size of the input grows unboundedly.
Typically, an algorithm that is asymptotically more efficient will be the superior choice for all but the smallest of inputs.
This section introduces standard methods and notations used to simplify the asymptotic analysis of algorithms, thereby
making this complex task more manageable. We shall see asymptotic notations that are well suited to characterizing
running times no matter what the input.
Those so-called Big-Oh notations are sets or classes of functions. We have classes of function asymtotically bounded
by above, below or both:
• 𝑓 (𝑛) ∈ 𝒪(𝑔(𝑛)) ⇐⇒ ∃𝑐 ∈ R+ , 𝑛0 ∈ N : 𝑓 (𝑛) ≤ 𝑐 · 𝑔(𝑛) ∀𝑛 ≥ 𝑛0 (upper bound)
• 𝑓 (𝑛) ∈ Ω(𝑔(𝑛)) ⇐⇒ ∃𝑐 ∈ R+ , 𝑛0 ∈ N : 𝑓 (𝑛) ≥ 𝑐 · 𝑔(𝑛) ∀𝑛 ≥ 𝑛0 (lower bound)
• 𝑓 (𝑛) ∈ Θ(𝑔(𝑛)) ⇐⇒∃𝑐1 , 𝑐2 ∈ R+ , 𝑛0 ∈ N : 𝑐1 · 𝑔(𝑛) ≤ 𝑓 (𝑛) ≤ 𝑐2 · 𝑔(𝑛) ∀𝑛 ≥ 𝑛0 (exact bound)
What is common in the definitions of these classes of function is that we are not concerned about small constant. Instead
we care about the big-picture that is when 𝑛 becomes really large (say 10,000 or 1,000,000). The intuition for those
classes of function notations are illustrated next.

One big advantage of Big-Oh notations is the capacity to simplify notations by only keeping the fastest growing term
and taking out the numerical coefficients. Let us consider an example of simplification: 𝑓 (𝑛) = 𝑐 · 𝑛𝑎 + 𝑑 · 𝑛𝑏 with
𝑎 ≥ 𝑏 ≥ 0 and 𝑐, 𝑑 ≥ 0. Then we have 𝑓 (𝑛) ∈ Θ(𝑛𝑎 ). This is even true if 𝑐 is very small and 𝑑 very big!
The simplication principle that we have applied are the following: 𝒪(𝑐 · 𝑓 (𝑛)) = 𝒪(𝑓 (𝑛)) (for 𝑐 > 0) and 𝒪(𝑓 (𝑛) +
𝑔(𝑛)) ⊆ 𝒪(max(𝑓 (𝑛), 𝑔(𝑛)))). You can also use these inclusion relations to simplify: 𝒪(1) ⊆ 𝒪(log 𝑛) ⊆ 𝒪(𝑛) ⊆
𝒪(𝑛2 ) ⊆ 𝒪(𝑛3 ) ⊆ 𝒪(𝑐𝑛 ) ⊆ 𝒪(𝑛!)
As a general rule of thumb, when speaking about the time complexity of an algorithm using Big-Oh notations, you
must simplify if possible to get rid of numerical coefficients.

48 Chapter 3. Part 3: Data-Structures and Algorithms


Concepts of Programming with Java

3.1.4 Recursive Algorithms

Say something about recurence equation + Graphical Method.

3.1.5 Practical examples of different algorithms

To grasp a theoretical concept such as time complexity and Big O notation, concrete examples are invaluable. For each
of the common complexities, we present an algorithmic example and then break down the reasons behind its specific
time complexity. The following table provides an overview of the most prevalent complexity classes, accompanied by
algorithm examples we explain after.

Complexity (name) Algorithm


𝒪(1) (constant) Sum of two integers
𝒪(log 𝑛) (logarithmic ) Find an entry in a sorted array (binary search)
𝒪(𝑛) (linear) Sum elements or find an entry in a not sorted array
𝒪(𝑛 log 𝑛) (linearithmic) Sorting efficiently an array (merge sort)
𝒪(𝑛2 ) (quadratic) Sorting inefficiently an array (insertion sort)
𝒪(𝑛3 ) (cubic) Enumerating tripples in an array
𝒪(2𝑛 ) (exponential) Finding elements in an array summing to zero (Subset-sum)
𝒪(𝑛!) (factorial) Visiting all cities in a country minimizing the distance

Binary Search

The Binary search, also known as dichotomic search, is a search algorithm that finds the position of a target value
within a sorted array. It works by halving the number of elements to be searched each time, which makes it incredibly
efficient even for large arrays.
Here’s how the binary search algorithm works:
1. You start with the middle element of the sorted array.
2. If the target value is equal to this middle element, then you’ve found the target and the algorithm ends.
3. If the target value is less than the middle element, then you repeat the search with the left half of the array.
4. If the target value is greater than the middle element, then you repeat the search with the right half of the array.
5. You keep repeating this process until you either find the target value or exhaust all elements.
The execution of this search is illustrated on next schema searching for value 7 repeating 4 times the process until
finding it. On this array of 16 entries, the search will never require more than four trials so this is a worst-case scenario.
This algorithm has a time complexity of 𝒪(log 𝑛) because each time through the loop, the number of elements to be
searched is halved and in the worst case, this process is repeated log 𝑛 times. On the other hand, if one is lucky, the
search immediatly find the element at the first iteration. Therefore the best-case time complexity is Ω(1).
The Java code is a direct translation of the explanation of the algorithm.

Listing 5: Binary Search Algorithm


/**
* This method performs a binary search on a sorted array.
* The array remains unchanged during the execution of the function.
*
* @param arr The input array, which must be sorted in ascending order.
* @param x The target value to search for in the array.
(continues on next page)

3.1. Time Complexity 49


Concepts of Programming with Java

(continued from previous page)


* @return The index of the target value in the array if found,
* or -1 if the target value is not in the array.
*/
public static int binarySearch(int arr[], int x) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;

// Check if x is present at mid


if (arr[mid] == x)
return mid;

// If x greater, ignore left half


if (arr[mid] < x)
left = mid + 1;

// If x is smaller, ignore right half


else
right = mid - 1;
}

// If we reach here, then element was not present


return -1;
}

Tip: Notice that the expression left + (right - left) / 2 is preferred over the somewhat simpler (left + right) / 2 to
calculate the middle index in a binary search. At first glance, they seem to do the same thing, and indeed, they usually
do give the same result. The main advantage of using left + (right - left) / 2 over (left + right) / 2 comes into play when
you are dealing with large numbers. The problem with (left + right) / 2 is that the sum of left and right could exceed
the maximum limit of the integer in the Java language that is is 231 − 1, causing an integer overflow, which can lead to
unexpected results or errors. The one used left + (right - left) / 2 does not have this overflow risk problem.

50 Chapter 3. Part 3: Data-Structures and Algorithms


Concepts of Programming with Java

Tip: Keep in mind that when dealing with objects (as opposed to primitive types), we would want to use the equals
method instead of ==. This is because equals tests for logical equality, meaning it checks whether two objects are
logically equivalent (even if they are different instances). On the other hand, == tests for reference equality, which
checks whether two references point to the exact same object instance. For objects where logical equality is more
meaningful than reference equality, like Strings or custom objects, using equals is the appropriate choice.

Linear Search

We already have seen the The sum method algorithm and its Θ(𝑛) time complexity. Another example of a linear time
complexity algorithm is the Linear Search algorithm. The time complexity of the linear search algorithm is 𝒪(𝑛),
where n is the size of the array, because in the worst-case scenario (the target value is not in the array or is the last
element in the array), the algorithm has to examine every element in the array once. In the best-case scenario for the
linear search algorithm, the target value is the very first element of the array. Therefore, in the best-case scenario, the
time complexity of the linear search algorithm is 𝒪(1) or we can simply say that the algorithm is also in Ω(1).

Merge Sort

Merge sort is a divide-and-conquer algorithm for sorting lists or arrays of items using pair-wise comparisons. It works
by dividing the unsorted list into :math:`n sublists, each containing one element (a list of one element is considered
sorted), and then repeatedly merging sublists to produce newly sorted sublists until there is only one sublist remaining.
Here’s the basic idea behind merge sort:
• Divide: If the list is of length 0 or 1, then it is already sorted. Otherwise, divide the unsorted list into two sublists
of about half the size.
• Conquer: Sort each sublist recursively by re-applying the merge sort.
• Combine: Merge the two sublists back into one sorted list.
Here is a simple implementation of Merge Sort in Java:

Listing 6: Merge Sort Algorithm


private static void merge(int[] left, int [] right, int result[]) {
assert(result.length == left.length + right.length);
int index = 0, leftIndex = 0 , rightIndex = 0;
while (leftIndex != left.length || rightIndex != right.length) {
if (rightIndex == right.length ||
(leftIndex != left.length && left[leftIndex] < right[rightIndex])) {
result[index] = left[leftIndex];
leftIndex++;
}
else {
result[index] = right[rightIndex];
rightIndex++;
}
index++;
}
}

/**
* Sort the values increasingly
(continues on next page)

3.1. Time Complexity 51


Concepts of Programming with Java

(continued from previous page)


*/
public static void mergeSort(int[] values) {
if(values.length == 1) // list of size 1, already sorted
return;

int mid = values.length/2;

int[] left = new int[mid];


int[] right = new int[values.length-mid];

// copy values[0..mid-1] to left


System.arraycopy(values, 0, left, 0, mid);
// copy values[mid..values.length-1] to right
System.arraycopy(values, mid, right, 0, values.length-mid);

// sort left and right


mergeSort(left);
mergeSort(right);

// merge left and right back into values


merge(left, right, values);
}

The Merge sort is a divide and conquer algorithm. It breaks the array into two subarrays, sort them, and then merges
these sorted subarrays to produce a final sorted array. All the operations and the data-flow of execution is best undersood
with a small visual example.

There are Θ(log 𝑛) layers of split and merge operations. Each requires Θ(𝑛) operations by summing all the split/merge
operations at one level. In the end, the time complexity of the merge sort algorithm is the product of the time complex-
ities of these two operations that is Θ(𝑛 log 𝑛).

52 Chapter 3. Part 3: Data-Structures and Algorithms


Concepts of Programming with Java

Insertion Sort

The insertion sort algorithm is probably the one you use when sorting a hand of playing cards. You start with one card
in your hand (the sorted portion). For each new card, you insert it in the correct position in your hand by moving over
any cards that should come after it.
The Java code is given next.

Listing 7: Insertion Sort Algorithm


/**
* This method sort the array using Insertion Sort algorithm.
*
* @param arr The input array.
*/
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i];
int j = i - 1;
// Move elements of arr[0..i-1], that are greater than key,
// to one position ahead of their current position
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}

For each element (except the first), it finds the appropriate position among the already sorted elements (all elements
before the current one), and inserts it there by moving larger elements up. Moving the larger elements up is the goal of
the inner while loop.
The time complexity of insertion sort is 𝒪(𝑛2 ) in the worst-case scenario, because each of the n elements could po-
tentially need to be compared with each of the n already sorted elements. However, in the best-case scenario (when
the input array is already sorted), the time complexity is 𝒪(𝑛), because each element only needs to be compared once
with the already sorted elements. Alternatively, we can simply say that the insertion sort algorithm runs in Ω(𝑛) and
𝒪(𝑛2 ).

Triple Sum

We consider a algorithm that checks if there exists at leat one combination of three elements in an array that sum up to
zero. Here an implementation in Java:

Listing 8: Triple Sum algorithm


/**
* This method checks if there are any three numbers in the array that sum up to zero.
*
* @param arr The input array.
* @return True if such a triple exists, false otherwise.
*/
public static boolean checkTripleSum(int[] arr) {
int n = arr.length;
(continues on next page)

3.1. Time Complexity 53


Concepts of Programming with Java

(continued from previous page)

for (int i = 0; i < n - 2; i++) {


for (int j = i + 1; j < n - 1; j++) {
for (int k = j + 1; k < n; k++) {
if (arr[i] + arr[j] + arr[k] == 0) {
return true;
}
}
}
}

return false;
}

In this program, checkTripleSum goes through each possible combination of three elements in the input array. If it finds
a triple that sums up to zero, it immediately returns true. If no such triple is found after checking all combinations, it
returns false. Since there are 𝑛 * (𝑛 − 1) * (𝑛 − 2)/6 possible combinations of three elements in an array of length
𝑛, and we’re checking each combination once, the time complexity of this method is 𝒪(𝑛3 ) and Ω(1). The best case
scenario occurs if the first three elements in the array sum to zero so that each loop is in its first iteration when the
return instruction occurs.

Subset-Sum

The subset sum problem is a classic problem in computer science: given a set of integers, is there a subset of the integers
that sums to zero? This is a generalization of the checkTripleSum problem we have seen before.
The algorithm we will use for solving the problem is a brute-force approach that will enumerate all subsets to solve this
problem. A common approach to enumerate all the subsets is to use recursion. We can consider each number in the set
and make a recursive call for two cases: one where we exclude the number in the subset, and one where we include it.
The Java code is given next. It calls an auxiliary method with an additional argument sum that is the sum of the elements
up to index i already included.

Listing 9: An algorithm for solving the Subset Sum problem


/**
* This method checks if there is a subset of the array that sums up to zero.
*
* @param arr The input array.
* @return True if there is such a subset, false otherwise.
*/
public static boolean isSubsetSumZero(int[] arr) {
return isSubsetSum(arr, 0, 0) || ;
}

private static boolean isSubsetSum(int[] arr, int i, int sum) {


// Base cases
if (i == arr.length) { // did not find it
return false;
}
if (sum + arr[i] == 0) { // found it
return true;
} else {
(continues on next page)

54 Chapter 3. Part 3: Data-Structures and Algorithms


Concepts of Programming with Java

(continued from previous page)


// Check if sum can be obtained by excluding / including the next
return isSubsetSumZero(arr, i + 1, sum) ||
isSubsetSumZero(arr, i + 1, sum + arr[i]);
}
}

The time complexity of this algorithm is 𝒪(2𝑛 ), because in the worst case it generates all possible subsets of the array,
and there are 2𝑛 possible subsets for an array of n elements. The worst-case is obtained when there is no solution and
that false is returned. The best time complexity is Ω(1) obtained when the first element in the array is zero so that the
algorithm immediatly returns true.
Note that this algorithm has an exponential time complexity (so far the algorithm we have studied were polynomial
e.g., 𝒪(𝑛3 )). Therefore, although this approach will work fine for small arrays, it will be quite slow for larger ones.

Tip: The question that arises is: Can we find an efficient algorithm to solve this problem more efficiently? By
“efficient”, we mean an algorithm that doesn’t take an exponential time to compute as the size of the input grows.
The answer is, maybe but we don’t know. Researchers stumbled upon a category of problems discovered in the early
1970’s, that share a common trait: they all seem hard to solve efficiently, but if you’re handed a potential solution, you
can at least verify its correctness quickly. The subset-sum problem belongs to this class. This category is called NP
(Nondeterministic Polynomial time). Now, within NP, there’s a special class of problems dubbed NP-complete. What
is so special about them? Well, if you can find an efficient solution for one NP-complete problem, you’ve essentially
found efficient solutions for all of them! The subset-sum problem is one of these NP-complete problems. Like its
NP-complete siblings, we don’t have efficient solutions for it yet. But remember, this doesn’t mean no efficient solution
exists; we just haven’t found one and it was also not yet proven that such an algorithm does not exist. This also doesn’t
mean that there are not faster algorithms for the subset sum problem that the one we have shown. For instance a dynamic
programming algorithm (out of scope of this introduction to algorithms) for subset-sum could avoid redundant work
but it still has a worst-case exponential time complexity.

Exercise
What is the time complexity of next algorithm? Characterize the best and worst case.

Listing 10: BitCount


/**
* Counts the minimum number of bits in the binary representation
* of a positive input number. Example: 9 requires 4 bits (1001).
* It halves it until it becomes zero counting the number of iterations.
*
* @param n The input number, which must be a positive integer.
* @return The number of bits in the binary representation of the input number.
*/
public static int bitCount(int n) {
int bitCount = 0;

while (n > 0) {
bitCount++;
n = n >> 1; // bitwise shift to the right, equivalent to dividing by 2
}

return bitCount;
}

3.1. Time Complexity 55


Concepts of Programming with Java

3.2 Space Complexity

Aside from the time, the memory is also a scarce resource that is worse analyzing for an algorithm. The space complex-
ity of an algorithm quantifies the amount of space or memory taken by an algorithm to run as a function of the length
of the input. Since this notion of space is subject to interpretation, let us separate it in two less ambiguous definitions.
* The auxiliary space is the extra space or the temporary space used by the algorithm during its execution. * The input
space is the space taken by the argument of the algorithm or the instance variables if any.
The definition of space complexity includes both: space complexity = auxiliary space complexity + input space com-
plexity.

3.2.1 Space Complexity of recursive algorithms

Notice that the extra space may also take into account the stack space in the case of a recursive algorithm. In such a
situation, when the recursive call happens, the current local variables are pushed onto the system stack, where they wait
for the call the return and unstack the local variables. More exactly, If a function A() calls function B() (which can be
A in case of recursion) inside it, then all the variables still in the scope of the function A() will get stored on the system
stack temporarily, while the function B() is called and executed inside the function A().
Let us compare the space and time complexity of an iterative and a recursive computation of the factorial of a number
expressed in function of 𝑛, the value of the number for which we want to compute the factorial.

Listing 11: Factorial


public class Factorial {
public static long factorialRecur(int n) {
if (n == 0) {
return 1;
} else {
return n * factorialRecur(n - 1);
}
}
public static long factorialIter(int n) {
long result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
}

Both have a time complexity of Θ(𝑛) but the space complexity of the iterative version is 𝑂(1) while the one of the
recursive version is Θ(𝑛). You may be a bit surprised by this result since not array of size 𝑛 is ever created in the
recursive version. True! but a stack is created of size n. A tack ? Yes a stack, but it is not visible and it is created
by the JVM. As explained before, every recursive call requires to store the local context or frame so that when the
recursion returns, the multiplication can be performed. It means that our execution stack for computing 10! will look
like [10*[9*[8*[7*[6*[5*[4*[3*[2*[1]]]]]]]]]]. This stack can be visualized by using the debugger and adding a break
point in the method. The call stack is show at the bottom left in IntelliJ and you can see what the local context is by
clicking on each frame.

56 Chapter 3. Part 3: Data-Structures and Algorithms


Concepts of Programming with Java

Tip: It is quite frequent to have time complexity larger than the space complexity for an algorithm but the opposite is
not true, at least for the auxiliary space complexity. The time complexity is necessarily at least the one of the auxiliary
space complexity since you always need the same order as elementary steps as the one of the consumed memory.

Tip: When an uncatched exception occurs, you can also visualize the output, the execution stack of the successive
calls from the main method up to the line of code that caused the exception to be thrown.

Improving the space complexity of merge sort

In the Merge Sort implementation, new arrays are created at each level of recursion. The overall space complexity is
thus of 𝒪(𝑛 log 𝑛), where 𝑛 is the number of elements in the input array. This is because, at each level of the recursion,
new arrays are created, adding up to 𝑛 elements per level, and the recursion goes log 𝑛 levels deep.
The time complexity required by our merge sort algorithm can be lowered to 𝒪(𝑛) for the auxiliary space. We can
indeed create a single temporary array of size 𝑛 once and reusing it in every merge operation. This temporary array
requires 𝑛 units of space, which is independent of the depth of the recursion. As such, the space complexity of this
version of the merge sort algorithm is 𝒪(𝑛), which is an improvement over the original version.

Listing 12: Merge Sort Algortithm with Temporary Shared Array


public class MergeSort {

private void merge(int[] arr, int[] temp, int leftStart, int mid, int rightEnd) {
int leftEnd = mid;
int rightStart = mid + 1;
int size = rightEnd - leftStart + 1;

(continues on next page)

3.2. Space Complexity 57


Concepts of Programming with Java

(continued from previous page)


int left = leftStart;
int right = rightStart;
int index = leftStart;

while (left <= leftEnd && right <= rightEnd) {


if (arr[left] <= arr[right]) {
temp[index] = arr[left];
left++;
} else {
temp[index] = arr[right];
right++;
}
index++;
}

System.arraycopy(arr, left, temp, index, leftEnd - left + 1);


System.arraycopy(arr, right, temp, index, rightEnd - right + 1);
System.arraycopy(temp, leftStart, arr, leftStart, size);
}

public void sort(int[] arr) {


int[] temp = new int[arr.length];
sort(arr, temp, 0, arr.length - 1);
}

private void sort(int[] arr, int[] temp, int leftStart, int rightEnd) {
if (leftStart >= rightEnd) {
return;
}
int mid = leftStart + (rightEnd - leftStart) / 2;
sort(arr, temp, leftStart, mid);
sort(arr, temp, mid + 1, rightEnd);
merge(arr, temp, leftStart, mid, rightEnd);
}

public static void main(String[] args) {


MergeSort mergeSort = new MergeSort();
int[] arr = {38, 27, 43, 3, 9, 82, 10};
mergeSort.sort(arr);
for (int i : arr) {
System.out.print(i + " ");
}
}
}

It is worth noting that in both versions of the algorithm, the time complexity remains the same: 𝒪(𝑛 log 𝑛). This is
because the time complexity of merge sort is determined by the number of elements being sorted (n) and the number
of levels in the recursion tree (log 𝑛), not by the amount of space used.

58 Chapter 3. Part 3: Data-Structures and Algorithms


Concepts of Programming with Java

3.3 Algorithm Correctness

A loop invariant is a condition or property that holds before and after each iteration of a loop. It’s used as a technique
for proving formally the correctness of an algorithm. The loop invariant must be true:
1. Before the loop begins (Initialization).
2. Before each iteration (Maintenance).
3. After the loop terminates (Termination). This often helps prove something important about the output.
The code fragment Bubble Sort with Loop Invariant illustrates a simple loop invariant for the Bubble Sort algorithm.
The loop invariant here is that after the i-th iteration of the outer loop, the largest i elements will be in their correct,
final positions at the end of the array.

Listing 13: Bubble Sort with Loop Invariant


public class Main {
public static void main(String[] args) {
int[] numbers = {5, 1, 12, -5, 16};
bubbleSort(numbers);

for (int i = 0; i < numbers.length; i++) {


System.out.print(numbers[i] + " ");
}
}

public static void bubbleSort(int[] array) {


int n = array.length;
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
if (array[j] > array[j+1]) {
// swap array[j] and array[j+1]
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
// Here, the loop invariant is that the largest i elements
// are in their correct, final positions at the end of the array.
}
}
}

In this example, the loop invariant helps us understand why the Bubble Sort algorithm correctly sorts the array. After
each iteration of the outer loop, the largest element is “bubbled” up to its correct position, so by the time we’ve gone
through all the elements, the array is sorted. The loop invariant holds at the initialization (before the loop, no elements
need to be in their final position), maintains at each iteration (after i-th iteration, the largest i elements are in their
correct positions), and at termination (when the loop is finished, all elements are in their correct positions, so the array
is sorted).

3.3. Algorithm Correctness 59


Concepts of Programming with Java

60 Chapter 3. Part 3: Data-Structures and Algorithms


CHAPTER

FOUR

PART 4: OBJECT ORIENTED PROGRAMMING AND DESIGN


PATTERNS

4.1 Interfaces and Abstract Classes

TODO, or did Ramin already covered it ?

4.2 Abstract Data Types (ADT)

In the context of data structures, an Abstract Data Type (ADT) is a high-level description of a collection of data and the
operations that can be performed on this data. It specifies what operations can be done on the data, without prescribing
how these operations will be implemented. In essence, an ADT provides a blueprint or an interface, and the actual
implementation details are abstracted away.
The actual workings of the operations are hidden from the user, providing a layer of abstraction. This means that the
underlying implementation of an ADT can change without affecting how users of the ADT interact with it.
Abstract Data Types are present in the Java Collections Framework. Let’s consider the List interface. This is an
Abstract Data Types. It defines an ordered collection of elements with duplicates allowed. List is an ADT because
it specifies a set of operations (add(E e),get(int index), remove(int index), size(), etc.) that you can perform on a list
without specifying how these operations are implemented. To get a concrete implementation you must use one of the
concrete classes that implement this interface, for instance ArrayList or LinkedList Whatever the one you choose the
high level contract described at the interface level remain the same, although depending on the instanciation you might
have different behaviors in terms of speed.

Listing 1: Example of usage of a Java List


import java.util.LinkedList;
import java.util.List;

public class LinkedListExample {

public static void main(String[] args) {

List<String> fruits; // declaring a List ADT reference

fruits = new LinkedList<>(); // Initializing it using LinkedList


// fruits = new ArrayList<>(); This would also work using ArrayList instead

// Adding elements
(continues on next page)

61
Concepts of Programming with Java

(continued from previous page)


fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");

// Removing an element
fruits.remove("Banana");
}
}

In the above example, you have seen a special notation using “<>” also called generics in java. Generics introduce
the concept of type parameters to Java, allowing you to write code that is parametrized by one or more types. This
enables you to create generic algorithms that work on collections of different types, classes, interfaces, and methods
that operate on a parameterized type. Generics offer a way to define and enforce strong type-checks at compile-time
without committing to a specific data type. The core idea is to allow type (classes and interfaces) to be parameters
when defining classes, interfaces, and methods.
In earlier versions of Java generics did not exit. You could add any type of objects to collections, which could lead to
runtime type-casting errors.

Listing 2: Example of ClassCastException at runtime


import java.util.LinkedList;
import java.util.List;

List list = new ArrayList();


list.add("hello");
list.add(1); // This is fine without generics
String s = (String) list.get(1); // ClassCastException at runtime

With generics, the type of elements you can add is restricted at compile-time, eliminating the potential for ClassCas-
tException at runtime. Generics enable you to write generalized algorithms and classed based on parameterized types,
making it possible to reuse the same method, class, or interface for different data types.

4.2.1 Stack ADT

Let us now study in depth an ADT called Stack. A stack is a collection that operates on a Last-In-First-Out (LIFO)
principle. The primary operations are push, pop, and peek.

Listing 3: Stack ADT


public interface StackADT<T> {
// Pushes an item onto the top of this stack.
void push(T item);

// Removes and returns the top item from this stack.


T pop();

// Returns the top item from this stack without removing it.
T peek();

// Returns true if this stack is empty.


(continues on next page)

62 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

(continued from previous page)


boolean isEmpty();
}

Implementing a Stack With Linked Structure

The LinkedStack is a stack implementation that uses a linked list structure to store its elements. Each element in the
stack is stored in a node, and each node has a reference to the next node. The top of the stack is maintained as a reference
to the first node (head) of the linked list.

Listing 4: Linked Stack ADT


public class LinkedStack<T> implements Stack<T> {
private Node<T> top;
private int size;

private static class Node<T> {


T item;
Node<T> next;

Node(T item, Node<T> next) {


this.item = item;
this.next = next;
}
}

@Override
public void push(T item) {
top = new Node<>(item, top);
size++;
}

@Override
public T pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
T item = top.item;
top = top.next;
size--;
return item;
}

@Override
public T peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return top.item;
}

@Override
(continues on next page)

4.2. Abstract Data Types (ADT) 63


Concepts of Programming with Java

(continued from previous page)


public boolean isEmpty() {
return top == null;
}

@Override
public int size() {
return size;
}
}

The state of the linked stack after pushing 1, 5 and 3 in this order is illustated on the next figure.

Implementing a Stack With an Array

Another method for implementing the Stack ADT is by utilizing an internal array to hold the elements. An implemen-
tation is given in the next fragment. The internal array is initialized with a size larger than the expected number of
elements in the stack to prevent frequent resizing.
An integer variable, often termed top or size, represents the current position in the stack. When pushing a new element
onto the stack, it’s added at the position indicated by this integer. Subsequently, the integer is incremented. The pop
operation reverses this process: the element at the current position is retrieved, and the integer is decremented. Both
the push and pop operations have constant time complexity: 𝑂(1).
However, there’s an inherent limitation when using arrays in Java: their size is fixed upon creation. Thus, if the stack’s
size grows to match the internal array’s size, any further push operation risks an ArrayIndexOutOfBoundsException.
To counteract this limitation, when the internal array is detected to be full, its size is doubled. This is achieved by
creating a new array with double the capacity and copying the contents of the current array to the new one. Although
this resizing operation has a linear time complexity of 𝑂(𝑛), where 𝑛 is the number of elements, it doesn’t happen
often.
Additionally, to avoid inefficiencies, if the size of the stack drops to one-quarter of the internal array’s capacity, the
array size is halved. This prevents the array from being overly sparse and consuming unnecessary memory.
Although resizing (either increasing or decreasing the size) requires 𝑂(𝑛) 𝑂(𝑛) time in the worst case, this cost is
distributed over many operations, making the average cost constant. This is known as amortized analysis. Thus, when
analyzed in an amortized sense, the average cost per operation over 𝑛 operations is 𝑂(1).

Listing 5: Array Stack ADT


public class DynamicArrayStack<T> implements Stack<T> {
private T[] array;
private int top;

@SuppressWarnings("unchecked")
public DynamicArrayStack(int initialCapacity) {
(continues on next page)

64 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

(continued from previous page)


array = (T[]) new Object[initialCapacity];
top = -1;
}

@Override
public void push(T item) {
if (top == array.length - 1) {
resize(2 * array.length); // double the size
}
array[++top] = item;
}

@Override
public T pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
T item = array[top];
array[top--] = null; // to prevent loitering

// shrink the size if necessary


if (top > 0 && top == array.length / 4) {
resize(array.length / 2);
}
return item;
}

@Override
public T peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return array[top];
}

@Override
public boolean isEmpty() {
return top == -1;
}

@Override
public int size() {
return top + 1;
}

@SuppressWarnings("unchecked")
private void resize(int newCapacity) {
T[] newArray = (T[]) new Object[newCapacity];
for (int i = 0; i <= top; i++) {
newArray[i] = array[i];
}
array = newArray;
(continues on next page)

4.2. Abstract Data Types (ADT) 65


Concepts of Programming with Java

(continued from previous page)


}
}

Evaluating Arithmetic Expressions with a Stack

A typical use of stacks is to evaluate arithmetic expressions as provided in the next algorithm.

Listing 6: Evaluating Expressions Using Stacks


public class ArithmeticExpression {
public static void main(String[] args) {
System.out.println(evaluate("( ( 2 * ( 3 + 5 ) ) / 4 )");
}

public static double evaluate(String expression) {

Stack<String> ops = new LinkedStack<String>();


Stack<Double> vals = new LinkedStack<Double>();

for (String s: expression.split(" ")) {


// INVARIANT
if (s.equals("(")) ;
else if (s.equals("+")) ops.push(s);
else if (s.equals("-")) ops.push(s);
else if (s.equals("*")) ops.push(s);
else if (s.equals("/")) ops.push(s);
else if (s.equals(")")) {
String op = ops.pop();
double v = vals.pop();
if (op.equals("+")) v = vals.pop() + v;
else if (op.equals("-")) v = vals.pop() - v;
else if (op.equals("*")) v = vals.pop() * v;
else if (op.equals("/")) v = vals.pop() / v;
vals.push(v);
}
else vals.push(Double.parseDouble(s));
}
return vals.pop();

}
}

The time complexity of the algorithm is clearly 𝑂(𝑛) where 𝑛 is the size of the input string:
• Each token (whether it’s a number, operator, or parenthesis) in the expression is read and processed exactly once.
• Pushing and popping elements from a stack take constant time, 𝑂(1).
• Arithmetic operations (addition, subtraction, multiplication, and division) are performed in constant time,
:math:`O(1).
To understand and convince one-self about the correctness of the algorithm, we should try to discover an invariant.
As can be seen, a fully parenthesized expression can be represented as a binary tree where the parenthesis are not
necessary:

66 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

The internal nodes are the operator and the leaf nodes are the values. The algorithm uses two stacks. One stack (ops)
is for operators and the other (vals) is for (reduced) values. The program splits the input string args[0] by spaces to
process each token of the expression individually.
We will not formalise completely the invariant here but give some intuition about what it is.
At any point during the processing of the expression:
1. The vals stack contains the results of all fully evaluated sub-expressions (reduced subtrees) encountered so far.
2. The ops stack contains operators that are awaiting their right-hand operands to form a complete sub-expression
(subtree) that can be reduced.
3. For every operator in the ops stack, its corresponding left-hand operand is already in the vals stack, awaiting the
completion of its subtree for reduction.
The figure displays the status of the stacks at three distinct stages for our brief example.
When we encounter an operand, it’s like encountering a leaf of this tree, and we immediately know its value, so it’s
pushed onto the vals stack.
When we encounter an operator, it’s pushed onto the ops stack. This operator is awaiting its right-hand operand to form
a complete subtree. Its left-hand operand is already on the vals stack.
When a closing parenthesis ) is encountered, it indicates the end of a fully parenthesized sub-expression, corresponding
to an entire subtree of the expression. This subtree is “reduced” or “evaluated” in the following manner:
1. The operator for this subtree is popped from the ops stack.
2. The right-hand operand (the value of the right subtree) is popped from the vals stack.
3. The left-hand operand (the value of the left subtree) is popped from the vals stack.
4. The operator is applied to the two operands, and the result (the value of the entire subtree) is pushed back onto
the vals stack.
This invariant captures the essence of the algorithm’s approach to the problem: it traverses the expression tree in a sort
of depth-first manner, evaluating each subtree as it’s fully identified by its closing parenthesis.

4.2. Abstract Data Types (ADT) 67


Concepts of Programming with Java

This algorithm taking a String in input is a an example of an interpreter. Interpreted programming languages (like
Python) do similarly but accept constructs that a slightly more complex that parenthetized arithmetic expressions.

4.3 Iterators

An iterator is an object that facilitates the traversal of a data structure, especially collections, in a systematic manner
without exposing the underlying details of that structure. The primary purpose of an iterator is to allow a programmer
to process each element of a collection, one at a time, without needing to understand the inner workings or specific
layout of the collection.
Java provides an Iterator interface in the `java.util package, which is implemented by various collection classes. This
allows objects of those classes to return iterator instances to traverse through the collection.
An iterator acts like a cursor pointing to some element within the collection. The two important methods of an iterator
are:
• hasNext(): Returns true if there are more elements to iterate over.
• next(): Returns the next element in the collection and advances the iterator.
The method remove() is optional and we will not use it in this course.
The next example show how to use an interator to print every element of a list.

Listing 7: Iterator Usage Example


import java.util.ArrayList;
import java.util.Iterator;

public class IteratorExample {


public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> it = list.iterator();
while (it.hasNext()) {
String element = it.next();
System.out.println(element);
}
}
}

Iterable should not be confused with Iterator. It is also an interface in Java, found in the java.lang package. An object
is “iterable” if it implements the Iterable interface wich has a signle method: Iterator<T> iterator();. This essentially
means that the object has the capability to produce an Iterator.
Many data structures (like lists, sets, and queues) in the java.util.collections package implement the Iterable interface
to provide a standardized method to iterate over their elements.
One of the main benefits of the Iterable interface is that it allows objects to be used with the enhanced for-each loop in
Java. Any class that implements Iterable can be used in a for-each loop. This is illustrated next that is equivalent to the
previous code.

68 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

Listing 8: Iterator Usage Example relying on Iterable for for-loops


import java.util.ArrayList;
import java.util.Iterator;

public class IteratorExample {


public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String element: list) {


System.out.println(element);
}
}
}

In conclusion, while they are closely related and often used together, Iterable and Iterator serve distinct purposes.
Iterable is about the ability to produce an Iterator, while Iterator is the mechanism that actually facilitates the traversal.

4.4 Implementing your own iterators

When implementing an iterator preperly, there are two possible strategies.


1. Fail-Fast: they throw ConcurrentModificationException if there is structural modification of the collection.
2. Fail-Safe: they don’t throw any exceptions if a collection is structurally modified while iterating over it. This is
because, they operate on the clone of the collection, not on the original collection and that’s why they are called
fail-safe iterators.
Fail-Safe iterator may be slower since one have to pay the cost of the clone at the creation of the iterator, even if we
only end-up iterating on a few elements. Therefore we will rather focus on the Fail-Fast strategy that is also the one
chosen most frequently in the impleentation of Java collections.
To implement a fail-fast iterator for our LinkedStack, we can keep track of a “modification count” for the stack. This
count will be incremented whenever there’s a structural modification to the stack (like pushing or popping). The iterator
will then capture this count when it’s created and compare its own captured count to the stack’s modification count
during iteration. If they differ, the iterator will throw a ConcurrentModificationException. The `LinkedStack class has
an inner `LinkedStackIterator`class that checks the modification count every time it’s asked if there’s a next item or
when retrieving the next item. It is important to understand that this is a non static inner classe. An inner class cannot
be instantiated without first instantiating the outer class and it is tied to a specific instance of the outer class. This is
why, the instance variables of the Iterator inner class can be intialized using the instance variables of the outer class.
The sample main method demonstrates that trying to modify the stack during iteration (by pushing a new item) results
in a ConcurrentModificationException.
The creation of the iterator has a constant time complexity, 𝑂(1).
1. The iterator’s current node is set to the top node of the stack. This operation is done in constant time since it’s
just a reference assignment.
2. Modification Count Assignment: The iterator captures the current modification count of the stack. This again is
a simple assignment operation, done in constant time.

4.4. Implementing your own iterators 69


Concepts of Programming with Java

No other operations are involved in the iterator’s creation, and notably, there are no loops or recursive calls that would
add to the time complexity. Therefore, the total time complexity of creating the LinkedStackIterator is 𝑂(1).

Listing 9: Implementation of a Fail-Fast Iterator for the LinkedStack


import java.util.Iterator;
import java.util.ConcurrentModificationException;

public class LinkedStack<T> implements Iterable<T> {


private Node<T> top;
private int size = 0;
private int modCount = 0; // Modification count

private static class Node<T> {


private T item;
private Node<T> next;

Node(T item, Node<T> next) {


this.item = item;
this.next = next;
}
}

public void push(T item) {


Node<T> oldTop = top;
top = new Node<>(item, oldTop);
size++;
modCount++;
}

public T pop() {
if (top == null) throw new IllegalStateException("Stack is empty");
T item = top.item;
top = top.next;
size--;
modCount++;
return item;
}

public boolean isEmpty() {


return top == null;
}

public int size() {


return size;
}

@Override
public Iterator<T> iterator() {
return new LinkedStackIterator();
}

private class LinkedStackIterator implements Iterator<T> {


(continues on next page)

70 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

(continued from previous page)


private Node<T> current = top;
private final int expectedModCount = modCount;

@Override
public boolean hasNext() {
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
return current != null;
}

@Override
public T next() {
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
if (current == null) throw new IllegalStateException("No more items to␣
˓→iterate over");

T item = current.item;
current = current.next;
return item;
}
}

public static void main(String[] args) {


LinkedStack<Integer> stack = new LinkedStack<>();
stack.push(1);
stack.push(2);
stack.push(3);

Iterator<Integer> iterator = stack.iterator();


while (iterator.hasNext()) {
System.out.println(iterator.next());
stack.push(4); // Will cause ConcurrentModificationException at the␣
˓→next call to hasNext

}
}
}

4.5 Delegation

We consider the book class below

Listing 10: Book


public class Book {
private String title;
private String author;
private int publicationYear;
(continues on next page)

4.5. Delegation 71
Concepts of Programming with Java

(continued from previous page)

public Book(String title, String author, int year) {


this.title = title;
this.author = author;
this.publicationYear = year;
}

// ... getters, setters, and other methods ...


}

We aim to sort a collection of Books based on their titles in lexicographic order. This can be done by implementing the
Comparable interface, requiring to define the compareTo method. The compareTo method, when implemented within
the Book class, leverages the inherent compareTo method of the String class.

Listing 11: Book Comparable


public class Book implements Comparable<Book> {
final String title;
final String author;
final int publicationYear;

public Book(String title, String author, int year) {


this.title = title;
this.author = author;
this.publicationYear = year;
}

@Override
public int compareTo(Book other) {
return this.title.compareTo(other.title);
}

public static void main(String[] args) {


List<Book> books = new ArrayList<>();
books.add(new Book("The Great Gatsby", "F. Scott Fitzgerald", 1925));
books.add(new Book("Moby Dick", "Herman Melville", 1851));
books.add(new Book("1984", "George Orwell", 1949));

Collections.sort(books); // Sorts the books by title due to the implemented␣


˓→ Comparable

for (Book book : books) {


System.out.println(book.getTitle());
}
}
}

Imagine that the books are displayed on a website, allowing visitors to browse through an extensive catalog. To enhance
user experience, the website provides a feature to sort the books not just by their titles, but also by other attributes: the
author’s name or the publication year.
Now, the challenge arises: our current Book class design uses the Comparable interface to determine the natural order-
ing of books based solely on their titles. While this design works perfectly for sorting by title, it becomes restrictive
when we want to provide multiple sorting criteria. Since the Comparable interface mandates a single compareTo

72 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

method, it implies that there’s only one “natural” way to sort the objects of a class. This design decision binds us to
sorting by title and makes it less straightforward to introduce additional sorting methods for other attributes.
A general important principle of object-oriented design is the Open/Closed Principle (OCP): a software module (like
a class or method) should be open for extension but closed for modification:
1. Open for Extension: This means that the behavior of the module can be extended or changed as the requirements
of the application evolve or new functionalities are introduced.
2. Closed for Modification: Once the module is developed, it should not be modified to add new behavior or features.
Any new functionality should be added by extending the module, not by making modifications to the existing
code.
The delegate design pattern can help us improve our design and is a nice example of the OCP. The delegation here
occurs when the sorting algorithm (within Collections.sort) calls the compare method of the provided Comparator
object. The responsibility of defining how two Book objects compare is delegated to the Comparator object, allowing
for flexibility in sorting criteria without modifying the Book class or the sorting algorithm itself.
This delegation approach with Comparator has a clear advantage over inheritance because you can define countless
sorting criteria without needing to modify or subclass the original Book class.
Here are the three Comparator classes, one for each sorting criterion:

Listing 12: Book Comparators


import java.util.Comparator;

public class TitleComparator implements Comparator<Book> {


@Override
public int compare(Book b1, Book b2) {
return b1.getTitle().compareTo(b2.getTitle());
}
}

public class AuthorComparator implements Comparator<Book> {


@Override
public int compare(Book b1, Book b2) {
return b1.getAuthor().compareTo(b2.getAuthor());
}
}

public class YearComparator implements Comparator<Book> {


@Override
public int compare(Book b1, Book b2) {
return Integer.compare(b1.getPublicationYear(), b2.getPublicationYear());
}
}

As next example shows, we can now sort by title, author or publication year by just proding the corresponding com-
parator to the sorting algorithm.

Listing 13: Book Comparators


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

(continues on next page)

4.5. Delegation 73
Concepts of Programming with Java

(continued from previous page)


public class Main {
public static void main(String[] args) {
List<Book> books = new ArrayList<>();
books.add(new Book("The Great Gatsby", "F. Scott Fitzgerald", 1925));
books.add(new Book("Moby Dick", "Herman Melville", 1851));
books.add(new Book("1984", "George Orwell", 1949));

Collections.sort(books, new TitleComparator()); // Sort by title


Collections.sort(books, new AuthorComparator()); // Sort by author
Collections.sort(books, new YearComparator()); // Sort by publication year
}
}

You are developing a document management system. As part of the system, you have a Document class that contains
content. You want to provide a printing capability for the Document.
Instead of embedding the printing logic directly within the Document class, you decide to use the delegate design
principle. This will allow the Document class to delegate the responsibility of printing to another class, thus adhering
to the single responsibility principle.
Complete the code below.

Listing 14: Book Comparators


// The Printer interface
interface Printer {
void print(String content);
}

// TODO: Implement the Printer interface for InkjetPrinter


class InkjetPrinter ... {
...
}

// TODO: Implement the Printer interface for LaserPrinter


class LaserPrinter ... {
...
}

// Document class
class Document {
private String content;
private Printer printerDelegate;

public Document(String content) {


this.content = content;
}

// TODO: Set the printer delegate


public void setPrinterDelegate(...) {
...
}

(continues on next page)

74 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

(continued from previous page)


// TODO: Print the document using the delegate
public void printDocument() {
...
}
}

// Demo
public class DelegateDemo {
public static void main(String[] args) {
Document doc = new Document("This is a sample document content.");

// TODO: Set the delegate to InkjetPrinter and print


...

// TODO: Set the delegate to LaserPrinter and print


...
}
}

4.6 Observer

In computer science, it is considered a good practice to have a loose coupling between objects (the opposite is generally
called a spagetti code). Loose coupling allows for more modular and maintainable code.
The Observer Pattern is a pattern that we can use to have a loose coupling between objects.
First show how to use it in the context of GUI development (Graphical User Interface) , and then will show how to
implement it.
In Java, the swing and awt packages facilitate the creation of Graphical User Interfaces (GUIs). Swing in Java uses a
system based on the observer pattern to handle events like button clicks.
On the next example we have a solitary button that, when clicked, responds with the message “Thank you” to the user.

Listing 15: Simple GUI with Action Listener


import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

class ButtonActionListener implements ActionListener {


@Override
public void actionPerformed(ActionEvent e){
JOptionPane.showMessageDialog(null,"Thank you!");
}
}

public class AppWithActionListener {


public static void main(String[] args) {
JFrame frame=new JFrame("Hello");
(continues on next page)

4.6. Observer 75
Concepts of Programming with Java

(continued from previous page)


frame.setSize(400,200);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

JButton button=new JButton("Press me!");


button.addActionListener(new ButtonActionListener());
frame.add(button);

frame.setVisible(true);
}
}

The ActionListener is an interface within Java that contains a single method: actionPerformed(). In our application,
we’ve implemented this interface within the ButtonActionListener class. When invoked, it displays a dialog with
the message “Thank you!” to the user. However, this setup remains inactive until we associate an instance of our
ButtonActionListener to a button using the addActionListener method. This ensures that every time the button is
pressed, the actionPerformed method of our listener gets triggered.
It’s worth noting that the inner workings of how the button manages this relationship or stores the listener are abstracted
away. What’s crucial for developers to understand is the contract: the listener’s method will be invoked whenever the
button is clicked. This process is often referred to as attaching a callback to the button. This concept echoes a well-
known programming principle sometimes dubbed the Hollywood principle: “Don’t call us, we’ll call you.”
Although we have registered only one listener to the button, this is not a limitation. Buttons can accommodate multiple
listeners. For example, another listener might track the total number of times the button has been clicked.
This setup exemplifies the observer design pattern from the perspective of end users, using the JButton as an illustration.
Let’s now delve into how to implement this pattern for custom classes.
Imagine a scenario where there’s a bank account that multiple people, say family members, can deposit into. Each
family member possesses a smartphone and wishes to be alerted whenever a deposit occurs. For the sake of simplicity,
these notifications will be printed to the console. The complete source code is given next.

Listing 16: Implementation of the Observable Design Pattern for an Ac-


count
public interface AccountObserver {
public void accountHasChanged(int newValue);
}

class MyObserver implements AccountObserver {


@Override
public void accountHasChanged(int newValue) {
System.out.println("The account has changed. New value: "+newValue);
}
}

public class ObservableAccount {


private int value ;
private List<AccountObserver> observers = new LinkedList();

public void deposit(int d) {


value += d;
for (AccountObserver o: observers) {
(continues on next page)

76 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

(continued from previous page)


o.accountHasChanged(value);
}
}

public void addObserver(AccountObserver o) {


observers.add(o);
}

public static void main(String [] args) {


ObservableAccount account = new ObservableAccount();
MyObserver observerFather = new MyObserver();
MyObserver observerMother = new MyObserver();
MyObserver observerGirl = new MyObserver();
MyObserver observerBoy = new MyObserver();

account.addObserver(observerFather);
account.addObserver(observerMother);
account.addObserver(observerGirl);
account.addObserver(observerBoy);

account.deposit(100); // we will see 4X "The account has changed. New Value:␣


˓→ 100"
account.deposit(50); // we will see 4X "The account has changed. New Value:␣
˓→ 150"
}
}

In this context, our bank account is the subject being observed. In our code, we’ll refer to it as the ObservableAccount.
This account maintains a balance, which can be incremented through a deposit function.
We require a mechanism to register observers (note: observers and listeners can be used interchangeably) who wish to
be informed about deposits. The LinkedList data structure is an excellent choice for this purpose: it offers constant-time
addition and seamlessly supports iteration since it implements the Iterable interface. To add an AccountObserver, one
would simply append it to this list. We’ve chosen not to check for duplicate observers in the list, believing that ensuring
uniqueness is the user’s responsibility.
Whenever a deposit occurs, the account balance is updated, and subsequently, each registered observer is notified by
invoking its accountHasChangedMethod, which shares the updated balance.
It’s important to note that the notification order is determined by the sequence of registration because we’re using a list.
However, from a user’s standpoint, depending on a specific order is inadvisable. We could have just as easily used a
set, which does not guarantee iteration order.
In this exercise, you will use the Observer pattern in conjunction with the Java Swing framework. The application
MessageApp provides a simple GUI where users can type a message and submit it. This message, once submitted,
goes through a spell checker and then is meant to be displayed to observers.
Your task is to make it work as exected: when a message is submitted, it is corrected by the spell checker and it is
appended in the text area of the app (use textArea.append(String text)).
It’s imperative that your design allows for seamless swapping of the spell checker without necessitating changes to the
MessageApp class. Additionally, the MessageSubject class should remain decoupled from the MessageApp. It must
not depend on it and should not even be aware that it exists.
Use the observer pattern in your design. You’ll have to add instance variables and additional arguments to some existing
constructors. When possible always prefer to depend on interfaces rather than on concrete classes when declaring

4.6. Observer 77
Concepts of Programming with Java

your parameters. With the progress of deep-learning we anticipate that we will soon have to replace the existing
StupidSpellChecker by a more advanced one. Make this planned change as simple as possible, without having to
change your classes.

Listing 17: Implementation of the Observable Design Pattern for an Ac-


count
import javax.swing.*;
import java.awt.event.*;

import java.util.ArrayList;
import java.util.List;

public class MessageApp {


private JFrame frame;
private JTextField textField;
private JTextArea textArea;
private JButton submitButton;

public MessageApp() {

frame = new JFrame("Observer Pattern with Swing");


textField = new JTextField(16);
textArea = new JTextArea(5, 20);
submitButton = new JButton("Submit");

frame.setLayout(new java.awt.FlowLayout());

frame.add(textField);
frame.add(submitButton);
frame.add(new JScrollPane(textArea));

// Hint: add an actionListner to the submitButon


// Hint: use textField.getText() to retrieve the text

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
(continues on next page)

78 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


Concepts of Programming with Java

(continued from previous page)


frame.setVisible(true);
}

public static void main(String[] args) {


SwingUtilities.invokeLater(new Runnable() {
public void run() {
new MessageApp();
}
});
}
}

interface SpellChecker {
String correct(String sentence);
}

class StupidSpellChecker implements SpellChecker {


public String correct(String sentence) {
return sentence;
}
}

interface MessageObserver {
void updateMessage(String message);
}

class MessageSubject {

private List<MessageObserver> observers = new ArrayList<>();


private String message;

public void addObserver(MessageObserver observer) {


observers.add(observer);
}

public void setMessage(String message) {


this.message = message;
notifyAllObservers();
}

private void notifyAllObservers() {


for (MessageObserver observer : observers) {
observer.updateMessage(message);
}
}
}

4.6. Observer 79
Concepts of Programming with Java

80 Chapter 4. Part 4: Object Oriented Programming and Design Patterns


CHAPTER

FIVE

PART 5: FUNCTIONAL PROGRAMMING

5.1 Functional Interfaces

5.2 Higher Order Functions

5.3 Streams

5.4 Immutable Collections

81
Concepts of Programming with Java

82 Chapter 5. Part 5: Functional Programming


CHAPTER

SIX

PART 6: PARALLEL PROGRAMMING

Blabla

6.1 Subtitle

Blabla

83
Concepts of Programming with Java

84 Chapter 6. Part 6: Parallel Programming


CHAPTER

SEVEN

INDICES AND TABLES

• genindex
• modindex
• search
• PDF version of the book

85
Concepts of Programming with Java

86 Chapter 7. Indices and tables


BIBLIOGRAPHY

[HS65] Juris Hartmanis and Richard E Stearns. On the computational complexity of algorithms. Transactions of the
American Mathematical Society, 117:285–306, 1965.

87

You might also like