COMP1110 Lecture Notes
[index] - Fabian Muehlboeck
COMP1110 Lecture Notes
[index]- Fabian Muehlboeck
This is a "forest" created using the Forester tool, a system of evergreen hypertext notes developed by, among others, Jon Sterling and Kento Okura. To search the forest, press Ctrl+K.
Lecture 1. Course Intro
[comp1110-2026s2-lec01a] - July 28, 2026
- Fabian Muehlboeck
Lecture 1. Course Intro
[comp1110-2026s2-lec01a]- July 28, 2026
- Fabian Muehlboeck
This lecture was largely going through the slides for Lecture 1. It summarised the Course Policies, How the Course Works, Assessments, and sources of Help, all of which you can read elsewhere on this site. We ended with me discovering that I did not have the right setup on my laptop to really demonstrate a Java program, so all of that will be in the next lecture.
Lecture 2. Procedural Programming
[comp1110-2026s2-lec01b] - July 30, 2026
- Fabian Muehlboeck
Lecture 2. Procedural Programming
[comp1110-2026s2-lec01b]- July 30, 2026
- Fabian Muehlboeck
2.1. Starting with Java
[comp1110-0K] - July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
2.1. Starting with Java
[comp1110-0K]- July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Example 2.1.1. A Java Greeter
[comp1110-0C] - July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Example 2.1.1. A Java Greeter
[comp1110-0C]- July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
In the lecture we created a source code file called Main.java, but rather than the boring Hello World! message that is so common in programming tutorials, we opted for a more apt message:
This code defines a function, called main, which takes no parameters as inputs and returns nothing (void) as outputs. The function doesn't produce any output because it instead relies on side-effects to perform its function. The function IO.println() will, given a value of type String, display that value on the terminal, followed by a newline. Each statement in a function body must be terminated by a semicolon ;.
To try our program out, we must compile it with the javac command, which will produce a matching .class file — these files are in a binary format that the Java Virtual Machine can execute. Then, we use java to run it:
Example 2.1.2. Reading Input
[comp1110-0E] - July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Example 2.1.2. Reading Input
[comp1110-0E]- July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Suppose we want to read some input from the user. The function IO.readln() (optionally) takes a prompt to give to the user and returns one line of input that the user typed on their console. Like so:
If we don't simply want to ignore whatever the user writes as an answer, we need to create a variable to store the user's input in. In Java, all variables have types. The type of the user's input is a String, so we'll declare a variable called answer and use it to store the user's input. Extending our Java greeter:
This example demonstrates two new concepts: assignment using the = operator, and concatenation with the + operator.
In Java, when we write:
It should not be understood as a "defining equation" or a mathematical definition of the variable answer. That is because (unlike in, say, Haskell, where variables are unchanging) the value of a variable in Java can change as the program executes. This is called mutability. Instead, the above line of code should be understood to mean "replace whatever value is in the answer variable with the "Hello" string".
Construction 2.1.3. if statements
[comp1110-0F] - Liam O'Connor
Construction 2.1.3. if statements
[comp1110-0F]- Liam O'Connor
Conditionals are a way for control flow to branch — based on a certain condition, we will either execute one branch of code or another. The most common conditional in Java is the if statement.
The else branch in the above code sketch is optional.
Example 2.1.4. Branching based on user input
[comp1110-0G] - July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Example 2.1.4. Branching based on user input
[comp1110-0G]- July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Further extending our Reading Input example:
Try this program out on a few inputs. Here we can see chaining of multiple if statements using else if. We can also see that the program keeps executing after the if statement concludes.
Warning 2.1.5. String comparisons
[comp1110-0H] - July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Warning 2.1.5. String comparisons
[comp1110-0H]- July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
If you are familiar with other programming languages, you might be wondering why we compared strings in Example 2.1.4 using a.equals(b) instead of something like a == b. While a == b is indeed the normal way to compare (e.g.) numbers, in Java equality for Strings is a little more complicated. In Example 2.1.4, a comparison answer == "yes" would always fail, even if the contents of answer were identical to the contents of the string "yes", because the two strings themselves have different identities, and exist at different locations in memory. Using the notation answer.equals("yes") instead compares the contents of the strings rather than their location, which is usually the functionality that we want.
Note 2.1.6. Case conversion
[comp1110-0I] - July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Note 2.1.6. Case conversion
[comp1110-0I]- July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
You may notice that Example 2.1.4 is sensitive to the case of the input string. We can change this by converting the string answer to lower case before our if statement executes, by adding this line:
or by replacing occurrences of answer with answer.toLowerCase(). In Java, there are lots of things you can do with Strings. Consult the Java API Documentation for Strings to see all String methods.
2.2. Top-down problem solving
[comp1110-0L] - July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
2.2. Top-down problem solving
[comp1110-0L]- July 30, 2026
- Liam O'Connor, Fabian Muehlboeck
Construction 2.2.1. Defining Functions
[comp1110-0N] - Liam O'Connor
Construction 2.2.1. Defining Functions
[comp1110-0N]- Liam O'Connor
As we have seen in Example 2.1.1, we can define functions in Java by stating their return type, then their name, then their parameters (types and names) in parentheses. The function body is surrounded by braces.
When the return type is not void, we must include a return statement in the function body, which, when executed, will immediately cease execution of the function body and return the result to the caller.
Definition 2.2.2. Top-down problem solving
[comp1110-0M] - Liam O'Connor
Definition 2.2.2. Top-down problem solving
[comp1110-0M]- Liam O'Connor
When employing top-down problem solving techniques, we assume we already have working implementations of functions to compute results for sub-problems, and use them to solve our main problem. Then, after we have implemented the main problem, we then tackle each sub-problem one by one. Sometimes it helps to apply this recursively: sub-problems get broken up into sub-sub-problems and so on.
By contrast, bottom-up problem solving means we construct functions that we are likely to need, and only afterwards combine them to produce a solution to our problem. While this has the advantage of being easier to test, as we will have runnable code at every stage, it also carries the risk that we will waste effort designing, writing and testing functions that we do not actually need.
Definition 2.2.3. The boolean type
[comp1110-0P] - Liam O'Connor
Definition 2.2.3. The boolean type
[comp1110-0P]- Liam O'Connor
The type boolean is for values that may be either true or false. We can combine boolean values with the conjunction (&&) and disjunction (||) operators. The expression a && b is true exactly when both a and b are true. The expression a || b is true if at least one of a or b is true. We can also negate boolean values with the negation (!) operator. The guards of if statements and while loops are always expressions of type boolean.
Construction 2.2.4. while loops
[comp1110-0Q] - Liam O'Connor
Construction 2.2.4. while loops
[comp1110-0Q]- Liam O'Connor
Loops are a way for control flow to repeat:
The above code will print the message 10 times total, because in each iteration, the variable i (which starts at 0) is increased by one, until it reaches 10, when the loop will exit as the guard (i < 10) is false.
Warning 2.2.5. Other forms of loops
[comp1110-0R] - Liam O'Connor
Warning 2.2.5. Other forms of loops
[comp1110-0R]- Liam O'Connor
In your prior programming experience(s), you may have seen other kinds of loops, such as for or do loops. While we don't demonstrate these loops in our examples for the first part of this course, you are permitted to use them in your own work.
In all cases, it is both possible and permissible to use while loops to solve looping problems.
Example 2.2.6. Password Strength Checker
[comp1110-0O] - Liam O'Connor, Fabian Muehlboeck
Example 2.2.6. Password Strength Checker
[comp1110-0O]- Liam O'Connor, Fabian Muehlboeck
It seems that every online service has a different bizarre set of requirements for passwords to be considered 'secure'. We came up with the following requirements in the lecture:
- Must be at least 10 characters long
- Every odd position should be an uppercase letter
- Must include "O_O"
Surely any password that meets these requirements would be 'secure' indeed.
We employ top-down problem solving here, defining first our main function in terms of currently unimplemented helper functions:
Currently our helper functions all return false because we have not yet implemented these checks, and it's a question of philosophy whether you want to make sure nothing passes that does not positively satisfy your criteria or nothing fails that does not positively violate them.
Example 2.2.7. lenOK
[comp1110-0S] - Liam O'Connor, Fabian Muehlboeck
Example 2.2.7. lenOK
[comp1110-0S]- Liam O'Connor, Fabian Muehlboeck
To implement the length check for Example 2.2.6, we will get the length of the password given using the length() method for strings:
We use String method length here to determine the length of a given String.
Warning 2.2.8. A common anti-pattern
[comp1110-0T] - Liam O'Connor
Warning 2.2.8. A common anti-pattern
[comp1110-0T]- Liam O'Connor
It's common for beginner programmers to use (sometimes multiple) if statements to achieve what can be achieved by simple boolean operators instead. For example, the following code can be greatly simplified:
Firstly, we can combine the if statements into one using boolean conjunction:
And, because we are simply returning the same value as the result of that boolean check a && b, we can elide the if statement completely:
Example 2.2.9. oddUppersOK
[comp1110-0U] - Liam O'Connor, Fabian Muehlboeck
Example 2.2.9. oddUppersOK
[comp1110-0U]- Liam O'Connor, Fabian Muehlboeck
To implement the odd-positioned uppercase character check for Example 2.2.6, we must loop through each odd-positioned character in the password.
We discussed that while indices are 0-based, what we meant by odd positions are those whose 1-based indices are odd (i.e. 0-based indices 0, 2, 4, etc. count as "odd").
Then, we can start from index 0 and increase our pos variable by 2 every step.
If any of the characters we encounter this way are not uppercase, we return false right away, which exits the loop immediately.
Warning.
- July 30, 2026
- Fabian Muehlboeck
The following content was not part of the lecture, but fits thematically within these notes. More about this stuff is in Lecture 02A.Warning.
- July 30, 2026
- Fabian Muehlboeck
2.3. Finishing the Password Checker
[comp1110-70] - Fabian Muehlboeck
2.3. Finishing the Password Checker
[comp1110-70]- Fabian Muehlboeck
Definition 2.3.1. The char type
[comp1110-0V] - Liam O'Connor
Definition 2.3.1. The char type
[comp1110-0V]- Liam O'Connor
The type char (characters) is for single symbols (a letter, a number, a newline, etc.). Strings are sequences of characters. In Java, we write characters with single-quotes, unlike Strings which require double-quotes. For example, the first character of the string "Hello" is 'H'. To get the nth character of a string, use the method charAt(), for example: "Hello".charAt(0) == 'H'.
Characters themselves are just numbers, where each symbol has a number according to this ASCII table (the story is more complicated for non-English characters, but this suffices for now). In particular, note that the digit character '4' and the number 4 are very different numbers, however the digits occur consecutively in the ASCII table, so we can convert digit characters to their associated number by subtracting the digit '0', that is: '4' - '0' == 4.
The module class Character includes a variety of useful functions for working with char, including Character.isLetterOrDigit() and Character.isDigit().
2.3.2. Loop invariants
[comp1110-0W] - Liam O'Connor, Fabian Muehlboeck
2.3.2. Loop invariants
[comp1110-0W]- Liam O'Connor, Fabian Muehlboeck
A loop invariant is a property that must be true of your variables at the start and end of each iteration of a loop. It typically takes the form of an equation or logical formula that relates the values of your variables together. They're not part of the programming language, but I will often write them down in a comment.
When I speak of the loop invariant for a particular loop, I'm looking for a loop invariant that describes the variables with enough detail to be useful to show that my function is correct.
When writing a loop, it is often helpful to figure out what the loop invariant is, as this will help you to convince yourself (and others) that the code you wrote is correct.
For example, the loop invariant for Example 2.2.9 is that pos <= pwd.length() and that every character at an odd position in pwd[0..pos-1] is uppercase. Here pwd[0..pos-1] refers to a substring of pwd, including the — it is not valid Java, but just notation for me to write a loop invariant.
This is true initially, as pos is zero and pwd[0..-1] is the empty string. Furthermore, it is true before and after each iteration, as we increase pos by 2 only after checking that pwd[pos] is uppercase.
Finally, when the loop terminates not via the early return, that means that the loop guard must be false, i.e pos >= pwd.length(). Combined with our invariant that pos <= pwd.length(), we can conclude that pos == pwd.length() when the loop finishes. This means that, when the loop finishes, the other part of our invariant 'every character at an odd position in pwd[0..pos-1] is uppercase' refers to pwd[0..pwd.length()-1] which is just pwd. Thus, we can use loop invariants to show that our function computes the result it's supposed to.
Example 2.3.3. smileyOK
[comp1110-0X] - Liam O'Connor, Fabian Muehlboeck
Example 2.3.3. smileyOK
[comp1110-0X]- Liam O'Connor, Fabian Muehlboeck
To implement the check that the emoji "O_O" (we called this "smiley" because the original proposal was for a smiley-face, but that turned out to conflict with the uppercase constraint) is present for Example 2.2.6, we must once again loop through each character in the password.
However, seeing as the emoji "O_O" is three characters long, we can exit the loop as soon as we reach pwd.length() - 2, as it would be impossible to see the emoji "O_O" in a string of length less than 3. If we find the word, we immediately exit the loop and return true.
I included the important parts of the loop invariant for the above loop. We always know that "O_O" is not the parts of pwd up to the next position (because in that case, it would have had to start in the previous position, pos-1). Before the loop runs, i is zero, so there is no position less than i to start from (thereby making the invariant true trivially). For each iteration, we increase i by one only after we check that the emoji "O_O" does not start there, thus maintaining the invariant. Thus, when the loop is over without early return and i == pwd.length() - 2, we can conclude that the emoji "O_O" does not appear anywhere in the string, and therefore return false.
Warning.
- Liam O'Connor, Fabian Muehlboeck
Warning.
- Liam O'Connor, Fabian Muehlboeck
We should always be careful to test edge cases. Why is it safe to put an empty string "" as input for pwd here?
Lecture 3. Arrays, Specs, Testing, and Searching, Part 1
[comp1110-2026s2-lec02a] - August 4, 2026
- Fabian Muehlboeck
Lecture 3. Arrays, Specs, Testing, and Searching, Part 1
[comp1110-2026s2-lec02a]- August 4, 2026
- Fabian Muehlboeck
3.1. Finishing the Password Checker
[comp1110-70] - Fabian Muehlboeck
3.1. Finishing the Password Checker
[comp1110-70]- Fabian Muehlboeck
Definition 3.1.1. The char type
[comp1110-0V] - Liam O'Connor
Definition 3.1.1. The char type
[comp1110-0V]- Liam O'Connor
The type char (characters) is for single symbols (a letter, a number, a newline, etc.). Strings are sequences of characters. In Java, we write characters with single-quotes, unlike Strings which require double-quotes. For example, the first character of the string "Hello" is 'H'. To get the nth character of a string, use the method charAt(), for example: "Hello".charAt(0) == 'H'.
Characters themselves are just numbers, where each symbol has a number according to this ASCII table (the story is more complicated for non-English characters, but this suffices for now). In particular, note that the digit character '4' and the number 4 are very different numbers, however the digits occur consecutively in the ASCII table, so we can convert digit characters to their associated number by subtracting the digit '0', that is: '4' - '0' == 4.
The module class Character includes a variety of useful functions for working with char, including Character.isLetterOrDigit() and Character.isDigit().
3.1.2. Loop invariants
[comp1110-0W] - Liam O'Connor, Fabian Muehlboeck
3.1.2. Loop invariants
[comp1110-0W]- Liam O'Connor, Fabian Muehlboeck
A loop invariant is a property that must be true of your variables at the start and end of each iteration of a loop. It typically takes the form of an equation or logical formula that relates the values of your variables together. They're not part of the programming language, but I will often write them down in a comment.
When I speak of the loop invariant for a particular loop, I'm looking for a loop invariant that describes the variables with enough detail to be useful to show that my function is correct.
When writing a loop, it is often helpful to figure out what the loop invariant is, as this will help you to convince yourself (and others) that the code you wrote is correct.
For example, the loop invariant for Example 2.2.9 is that pos <= pwd.length() and that every character at an odd position in pwd[0..pos-1] is uppercase. Here pwd[0..pos-1] refers to a substring of pwd, including the — it is not valid Java, but just notation for me to write a loop invariant.
This is true initially, as pos is zero and pwd[0..-1] is the empty string. Furthermore, it is true before and after each iteration, as we increase pos by 2 only after checking that pwd[pos] is uppercase.
Finally, when the loop terminates not via the early return, that means that the loop guard must be false, i.e pos >= pwd.length(). Combined with our invariant that pos <= pwd.length(), we can conclude that pos == pwd.length() when the loop finishes. This means that, when the loop finishes, the other part of our invariant 'every character at an odd position in pwd[0..pos-1] is uppercase' refers to pwd[0..pwd.length()-1] which is just pwd. Thus, we can use loop invariants to show that our function computes the result it's supposed to.
Example 3.1.3.
smileyOK
[comp1110-0X] - Liam O'Connor, Fabian Muehlboeck
Example 3.1.3. smileyOK
[comp1110-0X]- Liam O'Connor, Fabian Muehlboeck
To implement the check that the emoji "O_O" (we called this "smiley" because the original proposal was for a smiley-face, but that turned out to conflict with the uppercase constraint) is present for Example 2.2.6, we must once again loop through each character in the password.
However, seeing as the emoji "O_O" is three characters long, we can exit the loop as soon as we reach pwd.length() - 2, as it would be impossible to see the emoji "O_O" in a string of length less than 3. If we find the word, we immediately exit the loop and return true.
I included the important parts of the loop invariant for the above loop. We always know that "O_O" is not the parts of pwd up to the next position (because in that case, it would have had to start in the previous position, pos-1). Before the loop runs, i is zero, so there is no position less than i to start from (thereby making the invariant true trivially). For each iteration, we increase i by one only after we check that the emoji "O_O" does not start there, thus maintaining the invariant. Thus, when the loop is over without early return and i == pwd.length() - 2, we can conclude that the emoji "O_O" does not appear anywhere in the string, and therefore return false.
Warning.
- Liam O'Connor, Fabian Muehlboeck
Warning.
- Liam O'Connor, Fabian Muehlboeck
We should always be careful to test edge cases. Why is it safe to put an empty string "" as input for pwd here?
3.2. Introduction to Arrays
[comp1110-71] - Fabian Muehlboeck
3.2. Introduction to Arrays
[comp1110-71]- Fabian Muehlboeck
Definition 3.2.1. Arrays
[comp1110-15] - Liam O'Connor
Definition 3.2.1. Arrays
[comp1110-15]- Liam O'Connor
Arrays are a primary way to store linear, ordered collections of data. The elements of an array must all have the same type. An array type is written by appending [] to the type of the array elements. For example, the type of an array of int values is written as int[], whereas the type of an array of String values is written String[]. We create an array with the new keyword, like so:
The above code makes space in memory (called allocating) for a sequence of 50 int values, and stores the location of that space in memory in the newly-created variable arr.
By default, all ints in our array will be set to zero. If we wish to initialise our array with other values, we can include them in the new invocation like so:
We can access the contents of an array by index. The ith element of the array a (starting counting at zero) can be retreived using the notation a[i], for example:
We can also update elements of an array using this notation:
To get the length of an array a, we can write a.length:
Warning.
- Liam O'Connor
Warning.
- Liam O'Connor
For Strings, we write str.length() (i.e. calling a method, with the parentheses), but for arrays, we just write arr.length.
Example 3.2.2. Printing Array Elements (In reverse order)
[comp1110-72] - Fabian Muehlboeck
Example 3.2.2. Printing Array Elements (In reverse order)
[comp1110-72]- Fabian Muehlboeck
In the lecture, we went through the following example of looping through an array.
Note that, in contrast to earlier, we start our counter variable at the end (i = arr.length),
and reduce it before we use it in the body of the loop.
It is always a good idea to sanity-check your indices to prevent off-by-one errors (which would crash your program). Often, it suffices to look at the first and last index values.
In this case, i starts as arr.length, but the first valid index from the back is arr.length - 1.
Since we assign i to be i - 1 before looking at the array, that works out fine.
For the last index, the loop will end when i == 0, so the last time it could run is when i == 1.
In that case, because we again reduce i before accessing the array, the last index we will look at is 0,
which is both a valid index if the array is not empty (in which case, we would not be in the loop to begin with),
and also the last index we want to look at.
Finally, the loop invariant (§ 2.3.23.1.2) tracks the property that we wanted, which is that all elements of the array within a certain range have been printed, in such a way that the range coincides with the full array at the end of the loop.
Example 3.2.3. Searching for an Element in an Array, Generally
[comp1110-16] - Liam O'Connor, Fabian Muehlboeck
Example 3.2.3. Searching for an Element in an Array, Generally
[comp1110-16]- Liam O'Connor, Fabian Muehlboeck
Construction 3.2.4. import declarations
[comp1110-75] - Fabian Muehlboeck
Construction 3.2.4. import declarations
[comp1110-75]- Fabian Muehlboeck
Writing a program that does something new and amazing is great! But even in that best-case scenario, you will usually need your program to execute many little steps that are very common.
The most common of those are part of the core of your programming language, like numbers, Strings, and basic arithmetic operations. Other common things are often contained in a Standard Library, usually part of a standard language distribution, as in the case of the Java Development Kit (JDK) which you installed as part of our standard software setup. Finally, other people and organizations may publish their own libraries of code that you may be able to include in your own program.
Java's import declaration lets you "import" a library (or parts of it) so you can use its functionality in your code.
You typically put them at the top of any code file that uses them.
For example, to import the array-related part of the java.util library, you would write:
You can also import everything in a particular library using * as the last part of your imported library path, as in:
This may look convenient, but can easily cause name clashes and other maintenance issues, and as such is usually not a good idea.
Note 3.2.5. Array Equality
[comp1110-73] - Fabian Muehlboeck
Note 3.2.5. Array Equality
[comp1110-73]- Fabian Muehlboeck
We've seen earlier that String Equality requires special attention compared to the more common equality operator ==.
This is because the operator == expresses sameness, either of a numerical value, or of something larger within computer memory.
Sameness implies equality, but not necessarily the other way around.
We will look at the technical reasons for this later in the course.
Similar to Strings, Arrays are also larger things in computer memory. But in contrast to Strings, the contents of arrays can change. What that means is that if your idea of equality is that two arrays contain equal values in the same order, then that may be true at one point in your program, and false at a different point, as the contents of one or both of the arrays may change in the meantime. This means that the identity of each array matters a lot more than it does for Strings.
For that reason, a.equals(b) on arrays is the same as a == b.
However, the Java Standard Libraries contain a number of standard functions dealing with arrays, among them a suitable equality function.
To use it, you need to add an import declaration for java.util.Arrays to the top of your file.
You can then use the expression Arrays.equals(a,b) to compare two arrays for equality based on the equality of their elements.
Example 3.2.6. Arrays.equals
[comp1110-74] - Fabian Muehlboeck
Example 3.2.6. Arrays.equals
[comp1110-74]- Fabian Muehlboeck
Construction 3.2.7. Command Line Parameters
[comp1110-1L] - Liam O'Connor, Fabian Muehlboeck
Construction 3.2.7. Command Line Parameters
[comp1110-1L]- Liam O'Connor, Fabian Muehlboeck
When we invoke our program, we can pass it command line arguments:
If we add a parameter to our main function that has type String[], that array will contain each of those arguments:
When called as above, the above program will print the arguments in order, each on one line:
3.3. Specifications
[comp1110-17] - Liam O'Connor
3.3. Specifications
[comp1110-17]- Liam O'Connor
What does it mean for software to be correct? Typically there are two components:
- The software does what it's supposed to.
- The software doesn't do what it's not supposed to.
(often programmers acknowledge the former but forget the latter)
How do we determine what the software is (and is not) supposed to do? This is where specifications come in.
Definition 3.3.1. Preconditions and Postconditions
[comp1110-18] - Liam O'Connor
Definition 3.3.1. Preconditions and Postconditions
[comp1110-18]- Liam O'Connor
Specifications often take the form of preconditions and postconditions for a function.
- Preconditions state any assumptions made about inputs or parameters to the function.
- Postconditions state what the result(s) of a function will be, if successfully executed with inputs that satisfy the preconditions.
Sometimes these pairings of pre and post-conditions are called the contract for the function. If the caller satisfies the preconditions, the function is "contractually obliged" to satisfy the postconditions.
3.3.2. Writing Javadoc comments.
[comp1110-19] - Liam O'Connor
3.3.2. Writing Javadoc comments.
[comp1110-19]- Liam O'Connor
Javadoc is the standard Java documentation tool. Documentation for Java functions (such as we have seen for e.g. the Character class) are produced using this tool.
In Javadoc, each function is annotated with a special documentation comment, which looks like a typical multi-line comment, except it starts with two asterisks (/**) rather than just one:
We can include special commands in our Javadoc comments which are picked up by the tool and treated specially.
Javadoc includes markup specifically for the writing of specifications. We can describe
our parameters (and any preconditions associated with them) using the @param markup, and the @return markup to describe our return value (and any postconditions associated with them).
Example 3.3.3. Adding specs to contains
[comp1110-1A] - Liam O'Connor, Fabian Muehlboeck
Example 3.3.3. Adding specs to contains
[comp1110-1A]- Liam O'Connor, Fabian Muehlboeck
We can take the contains function from Example 3.2.3 and annotate it with a specification. It holds no assumptions about the input parameters, so the function has no nontrivial preconditions. Its postcondition,
however, captures the notion of correctness for this function:
Note how the loop invariant, allows us to reason that our postcondition (the @return clause above) is satisfied by our function.
3.4. Testing
[comp1110-1B] - Liam O'Connor
3.4. Testing
[comp1110-1B]- Liam O'Connor
Construction 3.4.1. assertions in Java
[comp1110-1C] - Liam O'Connor
Construction 3.4.1. assertions in Java
[comp1110-1C]- Liam O'Connor
The assert command in Java looks like this:
If you compile and run this program with the -ea (enable assertions) option, it will crash on the assert line and report an assertion violation, because 1 == 2 will evaluate to false.
If you give an assert an expression that evaluates to true, the program will continue executing as normal.
You can use assertions to check that your preconditions are satisfied in a function — it's better to crash and stop the program if the environment doesn't match your expectation than continue to run and potentially do something wrong.
Warning.
- Liam O'Connor
Always remember to run your program with the Warning.
- Liam O'Connor
-ea option, or you might be fooled into thinking your preconditions hold when they do not! For the first part of this course, we will use assert for testing.
Warning 3.4.2. A word from E.W. Dijsktra
[comp1110-1D] - Liam O'Connor
Warning 3.4.2. A word from E.W. Dijsktra
[comp1110-1D]- Liam O'Connor
Program testing can be used to show the presence of bugs, but never to show their absence!
Whenever you run a test, that is one possible execution out of potentially millions, or even infinite. Try to be thorough with your testing, but be aware of its limitations!
Example 3.4.3. Testing contains with assert
[comp1110-1E] - Liam O'Connor, Fabian Muehlboeck
Example 3.4.3. Testing contains with assert
[comp1110-1E]- Liam O'Connor, Fabian Muehlboeck
Take the contains function again from Example 3.3.3 and Example 3.2.3. Let us test it using assert:
Try to make each of these tests fail by modifying only the function contains.
Lecture 4. Arrays, Specs, Testing, and Searching, Part 2
[comp1110-2026s2-lec02b] - August 6, 2026
- Fabian Muehlboeck
Lecture 4. Arrays, Specs, Testing, and Searching, Part 2
[comp1110-2026s2-lec02b]- August 6, 2026
- Fabian Muehlboeck
4.1. Creating a finite type
[comp1110-2M] - Liam O'Connor, Fabian Muehlboeck
4.1. Creating a finite type
[comp1110-2M]- Liam O'Connor, Fabian Muehlboeck
Definition 4.1.1. Enums
[comp1110-2G] - Liam O'Connor, Fabian Muehlboeck
Definition 4.1.1. Enums
[comp1110-2G]- Liam O'Connor, Fabian Muehlboeck
An enum (short for enumeration) is a user-definable type whose values come from a fixed finite list. Instead of using arbitrary integers or strings, we define a small set of allowed values and give each one a name. For example:
This defines a new type called Month. A variable of type Month can only hold one of these twelve values. For example:
The names (which are by convention written in ALL_CAPS) inside the enum definition are called constants.
They are always written with the type name in front, such as Month.JANUARY or Month.DECEMBER.
This prevents confusion if different enums contain constants with the same name.
4.1.2. The benefits of enums
[comp1110-2J] - Liam O'Connor, Fabian Muehlboeck
4.1.2. The benefits of enums
[comp1110-2J]- Liam O'Connor, Fabian Muehlboeck
One advantage of enums is that they help make illegal states unrepresentable.
If we represent months using integers or strings, the program could accidentally produce values that are not valid months (such as "Spring" or -2).
By defining a Month enum, we restrict the variable to a fixed set of valid values (JANUARY, FEBRUARY, MARCH, and so on).
The compiler then ensures that no other values can appear.
This prevents many kinds of mistakes and makes programs easier to reason about, because whenever we see a variable of type Month, we know it must be one of those twelve possibilities.
Example 4.1.3. A Month from User Input
[comp1110-2H] - Liam O'Connor, Fabian Muehlboeck
Example 4.1.3. A Month from User Input
[comp1110-2H]- Liam O'Connor, Fabian Muehlboeck
The final line in the above function also demonstrates that enum types have a built in .toString() method that converts to the string representation of the constant name.
Warning 4.1.4. Enums are reference types
[comp1110-2I] - Liam O'Connor, Fabian Muehlboeck
Warning 4.1.4. Enums are reference types
[comp1110-2I]- Liam O'Connor, Fabian Muehlboeck
In Java, enum values are technically things that reside in memory, like Strings and arrays: each constant such as Month.JANUARY or Month.AUGUST has its own place in memory.
However, for each constant there is exactly one object created by the Java runtime.
This means every time the program refers to Month.JANUARY, it refers to the same object.
Because there is only one instance of each constant, it is safe to compare enum values using ==.
The comparison simply checks whether the two references point to the same object, which correctly tells us whether the two enum values are the same.
Construction 4.1.5. The switch statement on enums
[comp1110-2K] - Liam O'Connor, Fabian Muehlboeck
Construction 4.1.5. The switch statement on enums
[comp1110-2K]- Liam O'Connor, Fabian Muehlboeck
Java provides a convenient way to perform different actions depending on an enum value: the switch statement.
The switch examines the value of g. Each line specifies what to do for one possible value. If m is Month.MARCH, for example, the program prints "Mar".
Notice that inside the switch we write the constants without the Month. prefix. Java already knows that m has type Month, so it knows which constants we mean.
Warning 4.1.6. The old-style switch statement
[comp1110-2L] - Liam O'Connor, Fabian Muehlboeck
Warning 4.1.6. The old-style switch statement
[comp1110-2L]- Liam O'Connor, Fabian Muehlboeck
Older versions of Java used a different syntax for switch statements:
This version uses case labels and colons, and each branch usually ends with break.
If break is omitted, execution falls through into the next case, which often causes mistakes.
Therefore, in this course we will use the arrow form and not the older colon style.
Construction 4.1.7. The switch expression on enums
[comp1110-76] - Liam O'Connor, Fabian Muehlboeck
Construction 4.1.7. The switch expression on enums
[comp1110-76]- Liam O'Connor, Fabian Muehlboeck
switch is a control structure, like if and while.
In contrast to these two, however, switch can also be used as an expression form, which means it
can be used as the right-hand side of an assignment. For example:
In this case, it is important that each case of the switch expression returns a value of a suitable type.
If you need more complicated logic for a case, you can have a code block enclosed in curly braces, where the special
yield statement works like a sort of local return statement.
This form is not really important for this course, but you may encounter it in newer Java code.
Construction 4.1.8. Larger types and the default case for switch
[comp1110-78] - Fabian Muehlboeck
Construction 4.1.8. Larger types and the default case for switch
[comp1110-78]- Fabian Muehlboeck
The switch statements/expressions do not just work on enum values - in fact, they can distinguish between values of a wide variety of types.
Modern Java uses them to implement a kind of pattern-matching you might be used to from Haskell (but do not worry if you are not - if at all, we will only see that towards the end of this course).
You can also use them on strings or integers.
But those have many possible values - you don't really want to be writing cases for each and every one of them!
Any switch statement or expression can contain a default case for all other values not captures by previous cases.
In addition, individual cases can capture multiple values, like so:
And similarly, you can do the same for Strings:
The main difference here is that the latter works for all kinds of Strings, so you need to be careful about what your catch-all default case expresses. Here, the string "December" would return false, too!
4.2. Binary Search
[comp1110-77] - Fabian Muehlboeck
4.2. Binary Search
[comp1110-77]- Fabian Muehlboeck
Example 4.2.1. Guessing Game
[comp1110-13] - Liam O'Connor, Fabian Muehlboeck
Example 4.2.1. Guessing Game
[comp1110-13]- Liam O'Connor, Fabian Muehlboeck
In the lecture, we created a guessing game, choosing a "random" number based on a fair die roll.
Note 4.2.2. Bounding our Guesses
[comp1110-14] - Liam O'Connor, Fabian Muehlboeck
Note 4.2.2. Bounding our Guesses
[comp1110-14]- Liam O'Connor, Fabian Muehlboeck
If we know that the range of possible answers in Example 4.2.1 is \(0\) to \(100\), then we can algorithmically devise a procedure that will always guess the right answer:
- We know that the number is between \(0\) and \(100\). Guess \(50\). We get the report
Smaller!. - We know that the number is between \(0\) and \(49\). Guess \(25\). We get the report
Bigger!. - We know that the number is between \(26\) and \(49\). Guess \(37\). We get the report
Smaller!. - We know that the number is between \(26\) and \(36\). Guess \(31\). We get the report
Bigger!. - We know that the number is between \(32\) and \(36\). Guess \(34\). We succeed.
Following this procedure of always bisecting the possible range means that we will always get the answer in at most \(\log _2 100 \simeq 7\) guesses. In general, this bisecting means that for a range \(0\) to \(n\), we will get the answer in at most \(\log _2 n\) guesses.
Example 4.2.3. Binary Search
[comp1110-1J] - Liam O'Connor
Example 4.2.3. Binary Search
[comp1110-1J]- Liam O'Connor
If we add a precondition to the spec of contains, that the input array is sorted,
we can use a trick similar to our solution algorithm for our guessing game, which as we saw,
can reduce the amount of iterations of the program from linear in the size of the input to logarithmic.
The idea is that we consistently pick the middle index from the range of possible indices. If the value at this index is our target, we have found what we're looking for, and we're done. If the value at this index is larger than our target, that means our desired index is to the left, and reduces the range of possible indices by half. If it is smaller, then our desired index is to the other half, on the right of the selected midpoint.
This narrows the range of possible indices, which permits us to repeat this technique until we reach single elements.
4.3. Syntax sugar
[comp1110-1H] - Liam O'Connor
4.3. Syntax sugar
[comp1110-1H]- Liam O'Connor
There are a number of syntactic conveniences in Java that I have been avoiding (and likely will continue to avoid) in this course. Still, it's worth knowing these to understand code you find elsewhere
Construction 4.3.1. Three statement for loops
[comp1110-1F] - Liam O'Connor
Construction 4.3.1. Three statement for loops
[comp1110-1F]- Liam O'Connor
Many times throughout this course we have written while loops of the following format:
Sometimes, for greater conciseness, and to prevent forgetting to increment the counter,
Java programmers will instead write this loop as follows, using the for keyword:
For this course, I will not use this notation because the three statements inside the for
are executed at three separate locations in the code, so the for loop obscures code clarity slightly.
If you see this loop elsewhere though, its meaning is identical to the while loop above.
And you may feel free to use this loop if it's something to which you're already accustomed.
Construction 4.3.2. Update operators
[comp1110-1I] - Liam O'Connor
Construction 4.3.2. Update operators
[comp1110-1I]- Liam O'Connor
Instead of:
One may write:
Or even:
Also, instead of:
One may write:
Or even:
Note 4.3.3. Omitting braces
[comp1110-1G] - Liam O'Connor
Note 4.3.3. Omitting braces
[comp1110-1G]- Liam O'Connor
For for and while loops, as well as if statements, the braces may be omitted
if the body consists of just one statement. For example, the body of contains (Example 3.3.3 and
Example 3.2.3) can be rewritten as follows:
However, this abbreviation can be a danger, because minor syntax errors can lead to unexpected behaviour. For example:
This appears correct, but that's only because of indentation (which Java ignores). So this code is actually equivalent to:
Quite a different program indeed!
Lecture 5. Classes and Sorting
[comp1110-2026s2-lec03a] - August 11, 2026
- Fabian Muehlboeck
Lecture 5. Classes and Sorting
[comp1110-2026s2-lec03a]- August 11, 2026
- Fabian Muehlboeck
5.1. A Brief Intro to Classes and Objects
[comp1110-79] - Fabian Muehlboeck
5.1. A Brief Intro to Classes and Objects
[comp1110-79]- Fabian Muehlboeck
The core concept in object-oriented programming is that of an object. An object combines data and behaviour, though this is something we will discuss later in the course. For now, we introduce some basic ideas that you will see more of very soon.
Construction 5.1.1. Class Definitions
[comp1110-80] - Fabian Muehlboeck
Construction 5.1.1. Class Definitions
[comp1110-80]- Fabian Muehlboeck
In Java, objects are instances of classes.
A class definition is a kind of data definition, similar to what we have seen before for enums.
However, classes are much more powerful, and they are used to model pretty much everything in our programs that cannot be modeled by a simpler data type.
The core parts we will deal with for now are fields, which contain an object's data, and methods, which define an object's behaviour.
The basic definition of a class consists of the keyword class and a class name, followed by curly braces, like so:
This does define a new type Person.
To create a new instance of it (i.e. an object), you could write something simliar to what we did for arrays, using the keyword new:
This is not very useful, because the Person class above has no data or behaviour, but you can add those.
By convention, class names in Java should be nouns that start with a capital letter, and use capital letters to mark the start of any word internal to the name.
For example: LectureNote .
Construction 5.1.1.1. Fields
[comp1110-81] - Fabian Muehlboeck
Construction 5.1.1.1. Fields
[comp1110-81]- Fabian Muehlboeck
Every class can have arbitrarily many fields. At the very least, each field has a type and name, just like a local variable. There are some other properties that fields can have, which we'll see later. To define a field, you declare it just like a local variable:
You can also give the field a default value, just like a local variable definitions:
By convention, in Java, field names are camelCased.
That is, they start with a lower-case letter and otherwise contain only letters and digits,
with uppercase letters indicating the start of a new combined word, as in sumOfAllParts.
In principle, you can read and write the fields of an object similar to how you can read and write individual array elements, though the syntax is more like when you ask an array for its length:
Warning.
- Fabian Muehlboeck
Note that Warning.
- Fabian Muehlboeck
array.length has the special property that you cannot assign values to it.Example 5.1.1.2. Fields
[comp1110-82] - Fabian Muehlboeck
Example 5.1.1.2. Fields
[comp1110-82]- Fabian Muehlboeck
Say we want our Person class from earlier to have two fields: one representing the name of the person, and one representing their age.
Names would typically be represented as Strings, and in our case, we only care about completed years of age, so we use int as the type for that field.
We can then create a new Person value and write to its fields:
Warning.
- Fabian Muehlboeck
This is not how you would typically do this; we'll learn about the right way to do this later.Warning.
- Fabian Muehlboeck
Construction 5.1.1.3. Methods
[comp1110-83] - Fabian Muehlboeck
Construction 5.1.1.3. Methods
[comp1110-83]- Fabian Muehlboeck
So far, we have sometimes written code in separate functions from the main function.
But in every case so far, we had to give those functions all the arguments that they were supposed to work with.
Methods defined in a class implicitly have access to the fields defined in that class. This is because methods always need to be called on a specifc object, whose field values can then be used within the method.
So in principle, writing a method is just like writing a function, except inside a class. Within the class, you can all other methods within the class just like we called functions before.
By convention, in Java, method names are camelCased.
That is, they start with a lower-case letter and otherwise contain only letters and digits,
with uppercase letters indicating the start of a new combined word, as in sumOfAllParts.
Example 5.1.1.4. Methods
[comp1110-84] - Fabian Muehlboeck
Example 5.1.1.4. Methods
[comp1110-84]- Fabian Muehlboeck
Building on our example from Example 5.1.1.2, imagine we want to just have a way to
print a nice description for any of our basic Person objects.
To do this, we'll add a printDescription method to the class:
We can try this with the following main function:
This will print:
5.1.1.5. Have we seen Objects before?
[comp1110-85] - Fabian Muehlboeck
5.1.1.5. Have we seen Objects before?
[comp1110-85]- Fabian Muehlboeck
Yes! Every string and array is an object.
When we wrote guess.charAt(5), we were calling a method of the String class on the
particular String object referred to by guess.
Similarly, the difference between arr.length for arrays and str.length() for strings
is that the former is a field that we can access, and the latter is a method we have to call.
5.2. Understanding reference types
[comp1110-1U] - Liam O'Connor, Fabian Muehlboeck
5.2. Understanding reference types
[comp1110-1U]- Liam O'Connor, Fabian Muehlboeck
Example 5.2.1. A puzzling question
[comp1110-1P] - Liam O'Connor, Fabian Muehlboeck
Example 5.2.1. A puzzling question
[comp1110-1P]- Liam O'Connor, Fabian Muehlboeck
Without running this program, try to deduce what it will output:
You will only get the answer correct if you understand what happens in Java when values are passed to functions.
Definition 5.2.2. Passing by value (or copy)
[comp1110-1R] - Liam O'Connor, Fabian Muehlboeck
Definition 5.2.2. Passing by value (or copy)
[comp1110-1R]- Liam O'Connor, Fabian Muehlboeck
A function's variables, including its parameters, are local to that invocation of the function. When we invoke a function, some memory is set aside for a frame: storage space for the local variables in the function. The values of the arguments given in the function invocation are copied into the function's frame. This is called pass by value. When the function returns, the frame is no longer needed and destroyed.
This is why the function fun3 has no effect on its arguments in example Example 5.2.1.
Definition 5.2.3. Value types and reference types
[comp1110-1Q] - Liam O'Connor
Definition 5.2.3. Value types and reference types
[comp1110-1Q]- Liam O'Connor
Variables of type int and char (for example) are called value types. This means that a variable declared of type, say int:
Will contain a value, a single integer. When left uninitialised as above, they are initialised to a default value of 0.
By contrast, array types like int[] and char[] are what's called reference types. This means that a variable declared of an
array type will not contain the array data itself, but instead will contain the location in memory where the data may be found. This location is called a reference or pointer.
Any type name that begins with a capital letter (e.g. String) is also a reference type. This is why Strings should not be compared with == — it would compare locations, not contents.
Definition 5.2.3 combined with Definition 5.2.2 tells us that when we call a function like fun1 or fun2 in Example 5.2.1, the array data itself is not copied into the function's frame, but instead just the
reference is. When fun1 or fun2 are called, the variable arr in their frame has the same value as the variable arr in the frame of main — they both point
to the same array. This means that when we call fun1, the contents of the array in main are observably changed, as the array indexing arr[0] follows the same pointer that is in main.
When we call fun2, however, nothing changes in the function main, because we don't follow the pointers, but instead just change the value of the local variable arr to point to a new array, then immediately discard it.
5.2.4. The Waterloo Java Visualizer
[comp1110-1S] - Liam O'Connor
5.2.4. The Waterloo Java Visualizer
[comp1110-1S]- Liam O'Connor
The Waterloo Java Visualizer is a great way to visualise what's going on in these examples. Here's a link to Example 5.2.1 being visualised in it. Click "forward" to step through the program's execution.
Note
that it doesn't support Java 25, so I had to include some cruft that Java 25 has since made obsolete like (a method-free) class, public static etc. We will learn about these things later in the course but they are not relevant for now.
5.2.5. Reference vs. Value Equality
[comp1110-1T] - Liam O'Connor, Fabian Muehlboeck
5.2.5. Reference vs. Value Equality
[comp1110-1T]- Liam O'Connor, Fabian Muehlboeck
The difference between value types and reference types explains the differences between == and other methods to decide equality.
For value types, == happens to capture regular equality - for example, there are no two int values that represent the number 32 but are somehow different.
On the other hand, for reference types, == describes reference equality, identifying whether two references point to the same object in memory.
Since there may be two different objects representing the same value, == is too strict in many cases.
We have seen this for String equality and Array equality.
It is the same way in general for objects of arbitrary classes, except that for those, by default, no other notion of equality comparison exists.
We'll see later how to define our own equality comparison.
Finally, Enums are a special case: they are a reference type, but with a guarantee that each case of a Enum refers to the same object in memory.
As such, comparing the references to Enum values using == is fine - as for ints, there are no two distinct values representing the same case.
5.3. Back to Search
[comp1110-86] - Fabian Muehlboeck
5.3. Back to Search
[comp1110-86]- Fabian Muehlboeck
Example 5.3.1. CLI driver for search procedures
[comp1110-1M] - Liam O'Connor
Example 5.3.1. CLI driver for search procedures
[comp1110-1M]- Liam O'Connor
To make manual testing easier, we'll make a suitable main function that reads a certain number of inputs
one number at a time using IO.readln() and then calls our search function.
The flexibility is nice, by but running this and manually entering a bunch of numbers every time can be a bit of a slog. You can therefore create a file, say "numbers.txt" (the file extension does not really matter), that contains those numbers:
You can of course create several different files with different lists of numbers.
To use those files, you can call your program in a special way. If you normally start the program by writing:
You can instead write:
The combination of a less-than sign < with a filename after it tells your terminal emulator to
read that file and use it as the standard input for the program, instead of prompting the user.
We use the same idea in Homework 1.
Example 5.3.2. Sortedness Checker
[comp1110-1K] - Liam O'Connor, Fabian Muehlboeck
Example 5.3.2. Sortedness Checker
[comp1110-1K]- Liam O'Connor, Fabian Muehlboeck
5.3.3. Searching Arbitrary Arrays
[comp1110-87] - Fabian Muehlboeck
5.3.3. Searching Arbitrary Arrays
[comp1110-87]- Fabian Muehlboeck
Binary Search is very efficient, but depends on the array being sorted. This may not be true of any array you might encounter. So, as a first attempt, you might want to use the sortedness checker from Example 5.3.2 to see if the array is sorted, and then depending on that either use binary search or the linear search procedure we saw in Example 3.2.3.
The problem with the above approach is that when we are checking whether the array
is sorted, we have to go through every element of the array.
At that point, we might just check whether those elements are equal to needle
right away.
The general idea is more reasonable if there are several things that we want to search for. In that case, we only have to go through the array once, and if it happens to be sorted, we can then use binarySearch many times, efficiently finding each element.
But better yet, in that case, we might just want to sort the array to guarantee that we can do those later searches efficiently.
5.4. Understanding sorting
[comp1110-1V] - Liam O'Connor
5.4. Understanding sorting
[comp1110-1V]- Liam O'Connor
Definition 5.4.1. A spec and a test suite for sorting
[comp1110-1W] - March 10, 2026
- Liam O'Connor
Definition 5.4.1. A spec and a test suite for sorting
[comp1110-1W]- March 10, 2026
- Liam O'Connor
Any in-place sorting algorithm, no matter how it is implemented, should satisfy the following spec:
Assuming we had such a function, we might test it (using proper array comparison) as follows:
Definition 5.4.2. Quadratic sorting methods
[comp1110-1X] - August 11, 2026
- Liam O'Connor, Fabian Muehlboeck
Definition 5.4.2. Quadratic sorting methods
[comp1110-1X]- August 11, 2026
- Liam O'Connor, Fabian Muehlboeck
We demonstrated two sorting algorithms in the lecture:
- Selection sort: We divide the array into an "unsorted zone" and a "sorted zone". The sorted zone begins empty at the start of the array, and the unsorted zone begins by occupying the whole array. Each loop iteration, we look through the unsorted zone, find the minimum, and swap it with the first element of the unsorted zone (right next to the sorted zone). This means that the sorted zone can be expanded by one element. Repeat until the unsorted zone is of size zero.
- Bubble sort: Here, the "sorted zone" grows from the end of the array rather than the beginning.
Loop through the unsorted zone comparing adjacent elements.
If they are not in order, swap them.
This will result in the highest element reaching the top.
This means we can increase the size of the sorted zone.
Repeat until all sorted.
If we go through the unsorted zone without doing any swaps, we can immediately stop — the array is sorted. This is called an early exit trick.
I briefly mentioned another one at the start of the workshop:
- Insertion sort: The most popular of these three, and somewhat similar to selection sort, in that the
sorted zone begins empty and grows from the left. Instead of finding the minimum, however, we take the first element from
the unsorted zone and remember it (call it
X) and shift everything greater thanXin the sorted zone to the right one spot. Then we placeXin the slot left over.
These algorithms are called "quadratic" because they take a quadratic amount of key operations (in terms of the size of the array), in this case comparisons and moves/swaps to perform.
While insertion sort is generally the most popular of the three, each of them have their own advantages, depending on your particular situation. On arrays:
- Selection sort minimizes the number of times you have to move an array element.
- Bubble sort only swaps adjacent elements and has an easy way to exit early on sorted arrays.
- Insertion sort can minimize the number of comparisons that you do (these can become expensive on more complex data types).
Which of these you actually get depends on the particulars of your implementation, and minor variations (say starting from the other direction) can make a huge difference for certain scenarios.
Example 5.4.3. Selection sort
[comp1110-1Y] - August 11, 2026
- Liam O'Connor, Fabian Muehlboeck
Example 5.4.3. Selection sort
[comp1110-1Y]- August 11, 2026
- Liam O'Connor, Fabian Muehlboeck
Example 5.4.4. Bubble sort
[comp1110-1Z] - August 11, 2026
- Liam O'Connor
Example 5.4.4. Bubble sort
[comp1110-1Z]- August 11, 2026
- Liam O'Connor
Bubble sort swaps a lot of elements, but the key idea is that once it finds the maximum element in the unsorted part of the array, it keeps "bubbling" it up further and further, until we reach the end of the unsorted part, where a sorted array of elements larger than the new element is waiting. In that sense, it works like selection sort (either one of them could also just work in the other direction), but with more elements being moved around. We include the "early exit" trick mentioned by Definition 5.4.2:
Definition 5.4.5. In-place
[comp1110-20]
Definition 5.4.5. In-place
[comp1110-20]An algorithm is in-place if only modifies existing structures, rather than create new ones. For example, for
sorting an in-place function returns void, as they don't produce a new array, they
just modify an old one. All our sorting algorithms in this section are in-place.
Lecture 6. Exceptions and File IO
[comp1110-2026s2-lec03b] - August 13, 2026
- Fabian Muehlboeck
Lecture 6. Exceptions and File IO
[comp1110-2026s2-lec03b]- August 13, 2026
- Fabian Muehlboeck
6.1. Exceptions
[comp1110-88] - Fabian Muehlboeck
6.1. Exceptions
[comp1110-88]- Fabian Muehlboeck
6.1.1. Why Java Programs Crash
[comp1110-89] - Fabian Muehlboeck
6.1.1. Why Java Programs Crash
[comp1110-89]- Fabian Muehlboeck
Generally, programs crash when they try to execute an invalid operation. For example, so far we have seen that a program crashes when we try to access an array at an invalid index (say a negative index or in index larger than or equal to the array's size). Another reason might be that we are trying to divide a number by zero, or to parse an integer from a string that does not represent a number. There are many more potential reasons, some of which are related to things computers simply cannot or should not do, and some of which just relate to things that should not happen within the logic of a particular program.
In many modern programming languages, including Java, all of these kinds of problems are handled through a single exception mechanism. Exceptions are thrown at the point where the program itself or the language runtime detects an error. They then try to find an exception handler, which may execute code in response to the exception, and potentially re-throw it. If no handler is found, the program crashes, and simply prints information about the exception that made it crash.
In Java, exceptions are objects belonging to certain exception classes, for example NumberFormatException or ArrayIndexOutOfBoundsException.
The different classes make it possible to distinguish between different kinds of exceptions, and because exceptions are objects, they can carry relevant data about the particular problem that caused the exception to be thrown.
In general, the exception classes belong to one of three groups:
- Checked Exceptions
This is the standard kind of exception in Java, meant to express both error cases in the normal program logic and certain standard kinds of errors that are common and should usually be handled gracefully. Java is a bit of an outlier among major programming languages in that its compiler enforces that a programmer either handles such exceptions where they can occur or explicitly declares that such exceptions might be thrown within their code.
- Runtime Exceptions
These encompass all exceptions we have seen in the course so far. These are either very special or so common that it would be highly impractical to demand that they be checked or annotated everywhere they could occur, and so get an exemption from the above rule.
- Errors
These are the kinds of problems that might occur in the execution of a program, but are not necessarily the program's fault, and in any case, one cannot reasonably expect that a program can recover from them. Generally, you should not try to handle those.
Construction 6.1.2. Try-Catch-Finally
[comp1110-90] - Fabian Muehlboeck
Construction 6.1.2. Try-Catch-Finally
[comp1110-90]- Fabian Muehlboeck
Like if, while, and switch, try-catch-finally is a control flow construct,
written as follows:
You can generally have arbitrarily many catch-blocks after a try-block, and at most one finally-block. There must be at least one catch-block or a finally-block after a try-block.
Example 6.1.3. Parsing integers safely
[comp1110-91] - Fabian Muehlboeck
Example 6.1.3. Parsing integers safely
[comp1110-91]- Fabian Muehlboeck
Consider the following program:
The program asks the user for two numbers and then calculates their product. This calculation only makes sense if the user actually enters integers. If they do not, the program crashes:
The crash happens in the line that defines the variable x above, specifically when calling Integer.parseInt with a string that is not a number.
The error message describes both the kind of exception (java.lang.NumberFormatException, in short NumberFormatException) and the place where it happened.
We can handle this error by wrapping the call to Integer.parseInt in a try-catch block.
Because we do this twice, we factor this functionality out into a new function, like so:
Here, getNumber becomes a safer version of the combination of Integer.parseInt and IO.readln - it keeps asking the user again and again until they enter a string that parses without throwing an exception.
Definition 6.1.4. Null
[comp1110-2R] - Liam O'Connor, Fabian Muehlboeck
Definition 6.1.4. Null
[comp1110-2R]- Liam O'Connor, Fabian Muehlboeck
In Java, null is a special value that can be used as an instance of any reference type.
It is also the default value of any reference type.
That is, when you create an array of a reference type without giving the specific values in it,
for example using new String[10], the array will initially be filled with null.
Besides being a default value, null may be used intentionally to mark the optional absence of values.
More modern language often use special option types for this, and either disallow null entirely or require a special marker on a type to make it "nullable".
This is because when null can be anywhere, it is usually the cause of many bugs. Tony Hoare called the invention of null his "billion-dollar mistake".
The thing about null is that while you can assign it to any variable of a reference type or return it from any method that needs to return something of a reference type,
you cannot use it in any interesting way without causing a NullPointerException.
The only things that are safe to do are assignments of it, passing it as an argument (this may cause a bug further down the line) and comparisons using the == operator.
In particular, if null has been assigned to a variable x, any use of the "dot" operator to access a field or method of x will cause an exception.
Example 6.1.5. NullPointerException
[comp1110-92] - Fabian Muehlboeck
Example 6.1.5. NullPointerException
[comp1110-92]- Fabian Muehlboeck
If you try to access a field or method of a null value, Java will raise a NullPointerException:
This program crashes:
6.2. Files and File I/O
[comp1110-94] - Fabian Muehlboeck
6.2. Files and File I/O
[comp1110-94]- Fabian Muehlboeck
So far, we have navigated the file system and created directories using a terminal emulator,
and we have created and edited files using an editor like nano.
We can do the same things programmatically in Java, which lets our programs store data for the long term and exchange data with other programs more easily.
Construction 6.2.1. The File Class
[comp1110-95] - Fabian Muehlboeck
Construction 6.2.1. The File Class
[comp1110-95]- Fabian Muehlboeck
Java's File class specifies objects that represent abstract file paths, and through them, the directories and files in your file system.
To create an instance, you need to use the new operator, and give a path as an argument.
With the above f, you can now call a number of methods:
f.exists()Returns a boolean that is true if the file or directory exists, false if it does not
f.delete()Deletes the file or directory. A directory has to be empty for this to work.
f.getName()Returns the name of the file or directory - useful if you are just given a
Fileobject without having created it.f.getParent()Returns a different
Fileobject representing the parent directory of the file or directory, if that exists (the root directort "/" has no parent, so this would returnnull)f.isDirectory()Returns true if the path represents a directory
f.isFile()Returns true if the path represents a file
f.listfiles()If
frepresents a directory, returns an array containingFileobjects representing all the files contained in the directoryf.mkdir()Creates a directory at the path represented by
f
You can further use it as an argument to create FileReaders and FileWriters.
6.2.2. Character Encodings
[comp1110-7A] - Fabian Muehlboeck
6.2.2. Character Encodings
[comp1110-7A]- Fabian Muehlboeck
Files are essentially arrays of bytes stored on your computer.
Sometimes that is fine and the bytes are all we care about - this is the case for Java's .class files, which are not really human-readable.
For our purposes, however, we are mostly concerned with text files, which should consist of human-readable characters.
On modern computers, bytes consist of 8 bits (0-or-1 values), which means a byte can have 256 possible values, as can Java's char data type.
That is enough to store the latin alphabet, arabic numerals, and a number of standard punctuation characters, as in ASCII, but it is not enough to store various other ways of writing and the much larger set of special symbols people use, including emojis.
These days, most text files (especially on the web) are encoded using UTF-8, which has the advantage of being compatible with the important parts of ASCII, in that the first 128 symbols are encoded identically, so any two files that only use characters in that range look the same in UTF-8 and ASCII. Other symbols may use more than one byte, which makes things a bit more complicated.
On the other hand, Java internally encodes its Strings using UTF-16. Luckily, this is mostly something that you do not need to worry about. Java will by default assume that files are encoded in UTF-8 and do the necessary translation.
We mentions this only because of two things: if you exchange data with some other system and start seeing strange symbols, it might be an encoding issue, at which point you should follow up on this more. More importantly for us, though, is that the fact that different encodings exist explains the way that reading and writing files works in Java.
Construction 6.2.3. Reading Files
[comp1110-96] - Fabian Muehlboeck
Construction 6.2.3. Reading Files
[comp1110-96]- Fabian Muehlboeck
Interacting with files is risky business, much more so than all the other things we have done in the course so far. This is because they are shared with other programs and users that might work with them at the same time we do. Files might exist one moment and then not anymore, without our program having done anything, just because some other program deleted it. They might exist but cannot be opened, because the user running our program does not have permission to read it, or because there is some deeper problem with the file system.
For that reason, Java specifies a number of checked exceptions that you need to deal with when opening files and reading, usually under the umbrella of IOException.
In principle, when opening a file, you can have access to the raw bytes in the file.
However, for now, we use the FileReader class, which implicitly assumes that the file is encoded using UTF-8 (though you can specify a different encoding when creating the FileReader; that won't be necessary in this course).
The FileReader's role is to interpret the files in the appropriate encoding, and so it's core functionality is to read UTF-16 (Java's internal encoding) code points one by one.
It has a utility method that gives you all lines in a file at once, but that is often undesirable, especially for large files.
To be able to easily read a file line-by-line, we have to use a BufferedReader.
A BufferedReader requires some underlying reader, like a FileReader, to provide the actual data.
The main thing it does is provide a readLine() method, which reads the next line in the file since the last time you called it.
This is a way in which objects have behavior that changes their state. A FileReader starts at the beginning of a file and moves forward in it with every byte that it reads.
As the BufferedReader asks it to keep reading data until it reaches a line ending, each call of readLine() moves the internal state ahead by as many bytes as are needed to represent the line.
Construction 6.2.4. try-with-resources
[comp1110-97] - Fabian Muehlboeck
Construction 6.2.4. try-with-resources
[comp1110-97]- Fabian Muehlboeck
Opening a file reserves resources on your computer.
Conflicts over such resources may arise when multiple programs try to access the resources at the same time.
Additionally, not every operation on such resources will necessarily happen immediately.
For example, reading from and writing to files is somewhat expensive, so we use something like BufferedReader and (coming up) BufferedWriter, which try to combine several smaller operations into bigger ones, meaning that a number of smaller writes will not immediately take effect until there are enough of them.
If your program crashes in the meantime, it is likely that such data will be lost.
Closing an open file forces any remaining data to be written and any required resources to be returned to the operating system.
Therefore, it is a good idea to keep things clean and tidy, and close an open file as soon as you do not need it anymore (at least for the time being - you can always reopen it later).
All readers and writers have a close() method to do just that.
However, given that lots of exceptions can happen when interacting with files, guaranteeing that the file will be closed regardless of what happens can be tricky.
By default, this is what the finally block of the try-catch-finally construct is for:
Since this is a very standard pattern, a variant of try-catch-finally, called "try-with-resources", makes this more concise:
In this case, the BufferedReader and the FileReader will automatically be close in that order (reversed order of declaration) whenever program execution leaves the try-with-resources block.
The two code blocks here are not perfectly equivalent because the ordering of closing the reader and handling the IOException is different, but that typically does not really matter.
Construction 6.2.5. Writing Files
[comp1110-98] - Fabian Muehlboeck
Construction 6.2.5. Writing Files
[comp1110-98]- Fabian Muehlboeck
Writing files shares many considerations with reading files.
As such, the pattern is rather similar, but instead of a FileReader, we use a FileWriter, and instead of a BufferedReader, we use a BufferedWriter.
The BufferedWriter does not have a writeLine or printLine method.
Instead, you have to use the write method and add a newline character yourself:
Here, "\n" is a string consisting of a single newline character '\n'. Backslashes \ start what is called an escape sequence, expressing characters that are not easily printable.
To include a backslash itself, you have to write "\\".
The newline character '\n' (also called "line feed" or "LF") is the standard way of expressing line breaks on Linux and modern MacOS.
(Very) old Macs used a different character, '\r' (called "carriage return" or "CR", representing the idea of resetting the position of your typewriter to the start of a line).
Not to be outdone, Windows programs represent line breaks using a combination of the two, as "\r\n" (also described as "CRLF").
However, most Windows programs also recognize '\n' on its own as a line break (and BufferedReader considers any of the three to be terminating a line).
For this reason, it is usually fine to stick with just using '\n' as line break if you do not need to keep compatibility with specific Windows programs.
Alternatively, you can use BufferedReader's newLine method, which will write a system-specific line break (i.e. usually either '\n' or "\r\n") to your file at the current position:
Finally, BufferedReader does not necessarily write everything to your file right away.
If you want to make sure that everything you have written so far makes it to your file even if your program crashes at some later point, use the method flush().
In any case, flush will automatically be part of closing the file, say at the end of a try-with-resources.
Example 6.2.6. Reading and Writing from a File
[comp1110-99] - Fabian Muehlboeck
Example 6.2.6. Reading and Writing from a File
[comp1110-99]- Fabian Muehlboeck
In the lecture, we wrote a program that asks the user for some input, and if it has been run before, will display the input it got the last time we ran the program. We picked the name ".storage" for a file that would store the last input in between program runs. The "." at the start of the filename means this file should normally be hidden, as it is meant as internal data for programs, not for users to interact with outside of it. You can still find it and edit it manually if you wish.
Lecture 7. Recursive Data Structures
[comp1110-2026s2-lec04a] - August 18, 2026
- Fabian Muehlboeck
Lecture 7. Recursive Data Structures
[comp1110-2026s2-lec04a]- August 18, 2026
- Fabian Muehlboeck
7.1. More on classes
[comp1110-7E] - Fabian Muehlboeck
7.1. More on classes
[comp1110-7E]- Fabian Muehlboeck
So far, we have seen class definitions with fields and methods, but always had to initialise the fields of a new object explicitly. We can make this more convenient.
Definition 7.1.1. Class Member
[comp1110-7B] - Fabian Muehlboeck
Definition 7.1.1. Class Member
[comp1110-7B]- Fabian Muehlboeck
Member is the generic term for parts of a class, such as fields or methods.
Construction 7.1.2. Constructors
[comp1110-7C] - Fabian Muehlboeck
Construction 7.1.2. Constructors
[comp1110-7C]- Fabian Muehlboeck
A constructor is a class member that is used to initialise objects of a class when they are created.
By default, every class has a parameterless constructor that does nothing, and for a class named MyClass, can be called using:
You can define your own constructors. As soon as you do, the default parameterless constructor does not exist anymore. Constructors are special because, unlike normal methods, they do not have a name or return type. In Java, you specify them using the class type, as in:
Example 7.1.3. Initialising Objects
[comp1110-7D] - Fabian Muehlboeck
Example 7.1.3. Initialising Objects
[comp1110-7D]- Fabian Muehlboeck
The most common use of constructors is to initialise the fields of an object:
This way, new instances of the above class can be created using:
7.2. Recursion
[comp1110-2Z] - Liam O'Connor, Fabian Muehlboeck
7.2. Recursion
[comp1110-2Z]- Liam O'Connor, Fabian Muehlboeck
Definition 7.2.1. Recursive Type
[comp1110-7F] - Fabian Muehlboeck
Definition 7.2.1. Recursive Type
[comp1110-7F]- Fabian Muehlboeck
A recursive type is a type whose instances may "contain" other ("smaller") instances of the same type. It is typically described in terms of:
- At least one base case
- At least one recursive case (also called "inductive case")
Base cases describe those instances that do not contain further instances of the same type.
Recursive cases describe those instances that do contain further instances of the same type.
With this, we can build very large and potentially complex data structures.
Definition 7.2.2. Linked Lists
[comp1110-2S] - Liam O'Connor, Fabian Muehlboeck
Definition 7.2.2. Linked Lists
[comp1110-2S]- Liam O'Connor, Fabian Muehlboeck
A linked list is a way to store a sequence of items, one after another, where each item points to the next. Unlike arrays, it is not necessary to know ahead of time how many items the list will contain. It is a very common example of a recursive data type, where a type occurs as a field of itself. Here, we describe a linked list of integers by defining a recursive class type:
Here value is the first integer in the list, rest is the rest of the list (or null if there are no more integers). Therefore, null plays the role of the base case of our recursion,
while an IntList object (which contains another IntList) is the recursive case.
Definition 7.2.3. Recursive Function
[comp1110-7G] - Fabian Muehlboeck, Liam O'Connor
Definition 7.2.3. Recursive Function
[comp1110-7G]- Fabian Muehlboeck, Liam O'Connor
A recursive Function is a function that is defined in terms of itself. The idea here is that you can solve a problem by first solving a smaller part of it, and then adjusting that solution to become a solution for the whole problem. Like a recursive data type, it's code typically hase:
- At least one base case
- At least one recursive case (also called "inductive case")
Base cases describe those cases where a problem cannot be decomposed into smaller parts anymore - at this point, it should be easy to produce a solution for this relatively small problem.
Recursive cases describe those cases where you can first solve some smaller problems (by calling the same function, but with different input), and then combine those solutions into a solution for the current problem.
Recursive functions and recursive data types are a natural match, because the structure of the data can mirror the structure of your code: your function will have a base case for each base case of your recursive data type, and a recursive case for each recursive case of your recursive data type.
Example 7.2.4. Summing up a List of Integers
[comp1110-2U] - Liam O'Connor, Fabian Muehlboeck
Example 7.2.4. Summing up a List of Integers
[comp1110-2U]- Liam O'Connor, Fabian Muehlboeck
Recursive data types like linked lists are usually best handled by recursive functions:
However, in this case, because the last step performed in all recursive cases is to invoke the function recursively, We can translate the above program into a loop:
This works by starting at the first list cell, adding its value to the running total, then moving to the next cell by adjusting l to be l.next, and repeating until we reach null.
7.2.5. Stack Frames and Recursion
[comp1110-25] - Liam O'Connor, Fabian Muehlboeck
7.2.5. Stack Frames and Recursion
[comp1110-25]- Liam O'Connor, Fabian Muehlboeck
Compare Example 7.2.4 to this program that gives the same results:
All this program does is to give intermediate names to the results of looking at the value of an IntList node and of doing the recursive call.
This makes it much nicer to look at what is happening in the Java Visualizer (link to code above).
It also shows an important point about local variables.
Each time you call the sum function, the Java Visualizer shows you a new frame on top of the previous one.
These frames are part of the so-called call stack.
Local variables (the ones in the parameters of the function and the ones you define within the code) are local to each invocation of the recursive function, represented by a (call) stack frame.
Each frame corresponds to a particular function call that has not yet finished, and contains the local variables of that particular invocation.
This means that if you have n nested calls to sum, you will have n stack frames for sum on the call stack, on top of the frames from where sum was called originally (in our case, say, the main function).
Within those, there will be n different versions of l, each pointing to successive nodes in the linked list, plus at least n-1 versions of num,
each containing potentially different integer values.
If you change the value of a local variable within a function, you will only affect the value of that variable within the current function invocation's frame. All other frames will be unaffected, and when you call a function, the values of your local frame will be preserved until the function returns - the arguments get copied into the new frame.
The caveat here is when arguments are passed by reference, where the reference is copied to the new stack frame, but the value it points to is shared between caller and callee.
Definition 7.2.6. Stack Trace
[comp1110-7O] - Fabian Muehlboeck
Definition 7.2.6. Stack Trace
[comp1110-7O]- Fabian Muehlboeck
When your Java program crashes, it will typically print information about the exception that made it crash, along with a stack trace:
Here, the stack trace consits of the lines starting with the word "at". For every frame of your call stack, this will contain a line, in the same order as the call stack. That means that the top line is the place where the exception occurred. All the other lines indicate where the function whose frame is above that line was called.
This information can be very helpful when debugging your program, because it both tells you where the problem happened, and a lot of information about how you got there.
Warning 7.2.7. Stack Overflow
[comp1110-7P] - Fabian Muehlboeck
Warning 7.2.7. Stack Overflow
[comp1110-7P]- Fabian Muehlboeck
When using Recursion, your program might crash with a very long Stack Trace, and the specific exception is java.lang.StackOverflowError:
Essentially, your computer has some limited amount of space in its memory to store your stack frames, and when you have run out, a stack overflow happens, usually crashing your program.
This is usually because you got something wrong in writing your recursive code, and so your program loops forever, but with recursion, it eventually runs out of space. Make sure that your inputs to your recursive call are different (and "smaller") from the arguments to the current function!
Another possible reason is that you did write the recursive code correctly, but are operating on a data structure that is too large. This can happen, though you will usually not encounter such cases early in a computing degree. There are ways to increase the size reserved for stack frames, though it is not necessarily guaranteed that that will be a good solution. When working with large data, you might have to switch to iteration. However, don't see this as a reason to never use recursion - premature optimisation is never a good idea!
Definition 7.2.8. Tree
[comp1110-7H] - Fabian Muehlboeck
Definition 7.2.8. Tree
[comp1110-7H]- Fabian Muehlboeck
A tree is a recursive data type that consists of nodes, each of which may have some number of "child nodes", or children, which are themselves nodes of the tree. Each node has at most one parent, and the tree contains one node that has no parents, called the root of the tree. Nodes that have no children are also called leaves.
Each node can be treated as the root of a subtree starting with that node, ignoring its parent and any further ancestors.
When drawing a tree, we usually put the root on top, and the tree grows downwards. The following is a picture of a tree with a root node with two children. The first child only has one other child, while the second one has two more.
As the saying goes, this happened because computer scientists rarely go outside.
Trees are one of the most fundamental data structures in computer science. You will encounter them everywhere.
There is a large variety of potential tree data structures. Some have very rigid structures, where every node must either be a leaf or have the same number of children. Others allow nodes to have any number of children between some minimum or maximum. Some trees draw strict distinctions between leaves and inner nodes - oftentimes the distinction is that only one of them carries data. Some trees are formed by children knowing (pointing at) their parents, some trees are formed by parents knowing (point at) their children, in some trees, you can go in either direction.
Example 7.2.9. Linked Lists are Trees
[comp1110-7I] - Fabian Muehlboeck
Example 7.2.9. Linked Lists are Trees
[comp1110-7I]- Fabian Muehlboeck
A Linked List as defined earlier is a tree where nodes carry data and have either 0 or 1 children, and only parents know their children.
Example 7.2.10. Binary Trees
[comp1110-7J] - Fabian Muehlboeck
Example 7.2.10. Binary Trees
[comp1110-7J]- Fabian Muehlboeck
A Binary Tree is a tree where each node has 0 or 1 children on the "left", and 0 or 1 children on the "right" (for a total of between 0 and 2 children per node). Here is a suitable data definition:
We can now write a sum function to get the sum of all the numbers stored in the tree:
Note that because the tree has two fields that recursively point at other tree instances, we need to have two recursive function calls.
This example is where recursion gives you an advantage over iteration - in contrast to linked lists, it is much harder to write a while loop that does the same as the above recursive function. This is because the stack frame is now useful to store the intermediate result of calculating the sum of the values in the left child while we are calculating the sum of the values in the right child. If you try to write this with a loop, you will have to effectively manage a stack of such intermediate values yourself (there are some other tricks you will learn later in a computing degree that will sometimes let you avoid this, but nothing beats the recursion here in terms of its simplicity).
Example 7.2.11. The File System forms a Tree
[comp1110-7K] - Fabian Muehlboeck
Example 7.2.11. The File System forms a Tree
[comp1110-7K]- Fabian Muehlboeck
The file system mostly forms a tree where folders are nodes and files are leaves. Using the File class, you can get the children of a directory by calling listFiles, which returns an array of all files and directories contained within a directory:
Here, prefix is what is called an accumulator - as you go deeper into the recursion, the prefix string becomes bigger and bigger, accumulating information along the way.
Here, we use it to keep track of where we came from (essentially, it expresses the path from the root to the current node, as a string).
The Java File Class also has a getParentFile method, which allows you to access the parent of a node.
At the root of the file system, this returns null .
Lecture 8. Week 5 Test Preparation
[comp1110-2026s2-lec04b] - August 20, 2026
- Fabian Muehlboeck
Lecture 8. Week 5 Test Preparation
[comp1110-2026s2-lec04b]- August 20, 2026
- Fabian Muehlboeck
Warning.
- August 20, 2026
- Fabian Muehlboeck
This was a revision lecture in preparation for the week 5 test. No new content for the course will appear here.Warning.
- August 20, 2026
- Fabian Muehlboeck
Lecture 9. Recursion, Part II
[comp1110-2026s2-lec05a] - August 25, 2026
- Fabian Muehlboeck
Lecture 9. Recursion, Part II
[comp1110-2026s2-lec05a]- August 25, 2026
- Fabian Muehlboeck
9.1. More general kinds of recursion
[comp1110-7L] - Fabian Muehlboeck
9.1. More general kinds of recursion
[comp1110-7L]- Fabian Muehlboeck
The recursive functions we have seen so far directly matched the structure of a data type definition. Sometimes, such a structure is less obvious, and sometimes, we don't follow the structure of a data type that closely. But as long as you can argue that a recursive call is on "smaller" input (in the sense that we get close to a base case), recursion is a good option.
Example 9.1.1. Factorial
[comp1110-24] - Liam O'Connor
Example 9.1.1. Factorial
[comp1110-24]- Liam O'Connor
You may have seen this self-referential (recursive) definition of factorial before:
\[\begin {array}{lcl} 0! &=& 1 \\ n! &=& (n-1)! \cdot n \end {array}\]Try it out in the Java visualiser.
Warning 9.1.2. Factorial of Zero
[comp1110-7M] - Fabian Muehlboeck
Warning 9.1.2. Factorial of Zero
[comp1110-7M]- Fabian Muehlboeck
In the lecture, we used the following definition of factorial:
This is not the standard definition of factorial, which usually holds that 0! == 1.
Our definition above still matches the definition of factorial for every other valid input.
The main point here is that a function may have multiple base cases, some of which may never be hit coming from a recursive case.
Definition 9.1.3. Divide and Conquer
[comp1110-26] - Liam O'Connor
Definition 9.1.3. Divide and Conquer
[comp1110-26]- Liam O'Connor
Divide and conquer is a recursive problem solving strategy that solves a problem by following three distinct steps: Divide the problem into one or more subproblems that are smaller instances of the same problem; Conquer the subproblems by solving them recursively or directly if they are small enough; and Combine the subsolutions into the solution for the original problem. This approach is defined by reducing the problem size by a significant factor, such as halving the input in binary search, rather than just a constant amount (like in Factorial).
Example 9.1.4. Recursive binary search
[comp1110-27] - Liam O'Connor, Fabian Muehlboeck
Example 9.1.4. Recursive binary search
[comp1110-27]- Liam O'Connor, Fabian Muehlboeck
A recursive version of our iterative Binary Search. Note that we had to add additional parameters lower and upper to give us enough flexibility to use this function recursively. Such generalisation is often required when
working with recursive functions. Where needed, you can define another function without those parameters that simply calls the main one with suitable initial values.
Try it out in the Java visualiser.
9.1.5. Recursion and Loop Invariants
[comp1110-7N] - Fabian Muehlboeck
9.1.5. Recursion and Loop Invariants
[comp1110-7N]- Fabian Muehlboeck
Consider the iterative version of binary search, and specifically its loop invariant:
Example. Binary Search
[comp1110-1J] - Liam O'Connor
Example. Binary Search
[comp1110-1J]- Liam O'Connor
If we add a precondition to the spec of contains, that the input array is sorted,
we can use a trick similar to our solution algorithm for our guessing game, which as we saw,
can reduce the amount of iterations of the program from linear in the size of the input to logarithmic.
The idea is that we consistently pick the middle index from the range of possible indices. If the value at this index is our target, we have found what we're looking for, and we're done. If the value at this index is larger than our target, that means our desired index is to the left, and reduces the range of possible indices by half. If it is smaller, then our desired index is to the other half, on the right of the selected midpoint.
This narrows the range of possible indices, which permits us to repeat this technique until we reach single elements.
Contrast this with Recursive binary search. Notice how the loop invariant is essentially encoded in the preconditions for lower and upper.
The line of reasoning is the same - the loop invariant must be true at the end of each loop, just as the preconditions of the recursive function call have
to be true at each such recursive function call.
The precondition must be true at the first call of the recursive function, just as the loop invariant has to be true when entering the loop.
In exchange, we can assume that the loop invariant is true at the start of the loop and after it, just as we can assume the precondition holds at the start of the function, and (typically) in the base cases.
Example 9.1.6. Merge Sort
[comp1110-29] - Liam O'Connor, Fabian Muehlboeck
Example 9.1.6. Merge Sort
[comp1110-29]- Liam O'Connor, Fabian Muehlboeck
Merge sort is both an efficient sorting algorithm (although it is not in-place) and a classic example
of Divide and Conquer techniques. The algorithm works by dividing the input array in half, recursively sorting each half, and then merging the results. We can write an efficient merge function that combines two sorted lists into a single combined sorted lists. Note that we just loop through both arrays once, so this merge function is more efficient than simply appending the two arrays and then sorting them by other means.
This function consistutes the "combine" step in the divide and conquer strategy. Now all that is left is the "dividing" and "conquering":
Note: This functions efficiency can be slightly improved by only allocating one output array and passing it into each function, however this obscures the functionality of the algorithm.
Lecture 10. Recursion and Complexity
[comp1110-2026s2-lec05b] - August 27, 2026
- Fabian Muehlboeck
Lecture 10. Recursion and Complexity
[comp1110-2026s2-lec05b]- August 27, 2026
- Fabian Muehlboeck
Definition 10.1. Data invariants
[comp1110-44] - Liam O'Connor
Definition 10.1. Data invariants
[comp1110-44]- Liam O'Connor
A data invariant (also called a representation invariant) is a condition on the fields of a class that must hold after every constructor call and after every method call. It is not enforced by the compiler; it is the programmer's responsibility to maintain it.
Data invariants document the assumptions an implementation relies on internally. Like loop invariants, they make it easier to reason about correctness: if every method both assumes the invariant holds on entry and ensures it holds on exit, then by induction it holds throughout the lifetime of the object.
10.2. Binary Search Trees
[comp1110-7Q] - Fabian Muehlboeck
10.2. Binary Search Trees
[comp1110-7Q]- Fabian Muehlboeck
Definition 10.2.1. Binary Search Trees
[comp1110-2V] - Liam O'Connor, Fabian Muehlboeck
Definition 10.2.1. Binary Search Trees
[comp1110-2V]- Liam O'Connor, Fabian Muehlboeck
While we can replace linear search with binary search on sorted arrays to improve how many comparisons we have to do, this is not so straightforward with linked lists, even if sorted. The structure of the linked list requires linear traversal, rather than the random access required for binary search.
Instead, we will store sorted data differently so that finding elements can be much more efficient. Each node of a binary tree contains some data (in our case, an integer) and two branches:
valueis the integer at this node.leftis a tree of integers in the tree that are less than or equal to the value in the current node (Null if there are none).rightis a tree of integers in the tree that are greater than or equal to the value in the current node (Null if there are none).
The BST has a Data Invariant to ensure the conditions on the values stored in the left and right children.
Example 10.2.2. Inserting into a binary search tree
[comp1110-2W] - Liam O'Connor, Fabian Muehlboeck
Example 10.2.2. Inserting into a binary search tree
[comp1110-2W]- Liam O'Connor, Fabian Muehlboeck
Here BST is our example of a binary search tree type:
The above code (almost) always modifies an existing tree. A more "functional" version that always creates a new (slightly modified) copy of the original tree would look as follows:
Either version of the function ensures that the Data Invariant on the BST Definition is preserved.
Example 10.2.3. Looking up a BST
[comp1110-2X] - Liam O'Connor, Fabian Muehlboeck
Example 10.2.3. Looking up a BST
[comp1110-2X]- Liam O'Connor, Fabian Muehlboeck
To find out whether a tree contains a value, we determine what subtree of the binary search tree to descend into by comparing the desired value to the value stored in the root of the current subtree.
The correctness of the function depends on the Data Invariant specified in the BST Definition, similar
to the precondition on regular binary search that the array be sorted.
Here, this condition is implicitly given because haystack is a BST and any argument can always be assumed to satisfy any data invariants on it.
10.3. Algorithmic Complexity
[comp1110-2A] - Liam O'Connor
10.3. Algorithmic Complexity
[comp1110-2A]- Liam O'Connor
Algorithmic complexity is about how the running time (or space, see below) of an algorithm grows as the size of its input grows. If an input has size \(n\), we ask: what happens to the running time when \(n\) becomes large? Does it grow proportionally to \(n\)? To \(n^2\)? Or much more slowly?
Definition 10.3.1. Big O Notation
[comp1110-22] - Liam O'Connor
Definition 10.3.1. Big O Notation
[comp1110-22]- Liam O'Connor
A function \(f(x)\) is in the set \(\mathrm {O}(g(x))\) if and only if there exists constants \(c\) and \(n_0\) such that for any \(n\geq n_0\), \(f(n)\leq c \cdot g(n)\).
It's a common convention that when we say a function \(f(x)\) is \(\mathrm {O}(g(x))\), we mean that \(g(x)\) provides the tighest possible upper bound (as described above) for \(f(x)\).
10.3.2. Big O for algorithms
[comp1110-2B] - Liam O'Connor
10.3.2. Big O for algorithms
[comp1110-2B]- Liam O'Connor
By setting our function \(f(n)\) to be the running time taken by our algorithm on an input of size \(n\), we can use Big O Notation to describe the time complexity of our algorithms. This means that \(g(n)\) is an upper bound on how quickly \(f(n)\) can grow (up to a constant factor). For example, if an algorithm performs about \(3n + 5\) steps, then its running time grows proportionally to \(n\), so we say it is \(\mathrm {O}(n)\). As \(n\) becomes large, the constant \(5\) and coefficient \(3\) become less significant.
10.3.3. Ignoring constant factors
[comp1110-2C] - Liam O'Connor
10.3.3. Ignoring constant factors
[comp1110-2C]- Liam O'Connor
The Big O Notation ignores constant factors, and this is usually the correct approach. An algorithm that takes \(3n\) steps and one that takes \(100n\) steps are both linear (\(\mathrm {O}(n)\)). The second is slower, but if the input size doubles, both running times double. Big-O focuses on this growth behaviour, because for large inputs the growth rate matters far more than constant multipliers. A quadratic algorithm in \(\mathrm {O}(n^2)\) will eventually be far slower than any linear algorithm, even if the linear one has a large constant factor.
10.3.4. Complexity cases
[comp1110-2D] - Liam O'Connor
10.3.4. Complexity cases
[comp1110-2D]- Liam O'Connor
Often the running time of an algorithm depends on the arrangement of the input (such as in insertion sort). For this reason we talk about best-case, average-case worst-case complexity. Worst-case complexity is usually the most useful, because it gives a guarantee that the algorithm will never take longer than that bound.
Example 10.3.5. Time complexity of searching and sorting
[comp1110-2F] - Liam O'Connor
Example 10.3.5. Time complexity of searching and sorting
[comp1110-2F]- Liam O'Connor
- Linear search through an array of \(n\) elements: If the target happens to be the first element, the algorithm stops immediately; this is the best case and takes constant time, \(\mathrm {O}(1)\). In the worst case the element is last or not present, so the algorithm must check all \(n\) elements. The worst-case running time is therefore \(\mathrm {O}(n)\).
- Binary search on a sorted array. Each comparison eliminates half of the remaining elements. After one step at most \(\frac {n}{2}\) elements remain, then \(\frac {n}{4}\), then \(\frac {n}{8}\), and so on. After about \(\log _2 n\) steps only one element remains. Thus binary search runs in \(\mathrm {O}(\log n)\) time in the worst case.
- Selection sort repeatedly scans the remaining part of the array to find the smallest element. Even if the array is already sorted, it still performs the same number of comparisons, roughly \(\frac {n(n-1)}{2}\). Its running time therefore grows like \(n^2\), so it is \(\mathrm {O}(n^2)\) in both the best and worst cases.
- Insertion sort behaves differently. If the array is already sorted, each new element is compared once and left in place, so the running time grows linearly: \(\mathrm {O}(n)\). But if the array is in reverse order, each insertion shifts many elements and the total work grows like \(n^2\). Thus the best case is \(\mathrm {O}(n)\) and the worst case is \(\mathrm {O}(n^2)\).
- Bubble sort with early exit is similar. If the list is already sorted, it finishes after one pass, which is \(\mathrm {O}(n)\). In the worst case — where one small element appears out of order towards the end of the list — it performs many passes and becomes \(\mathrm {O}(n^2)\).
- Merge sort works differently. It divides the list in half, sorts each half, and then merges them. The list can be halved only about \(\log _2 n\) times, and each level of this process requires work proportional to \(n\) to perform the merges. The total work therefore grows like \(n \log n\). Unlike some previous sorts, this running time does not depend on the initial order of the input: both the best and worst cases are \(\mathrm {O}(n \log n)\).
10.3.6. Space complexity
[comp1110-2E] - Liam O'Connor
10.3.6. Space complexity
[comp1110-2E]- Liam O'Connor
We can also analyse how much memory an algorithm uses. This is called space complexity. Algorithms like insertion sort and selection sort operate directly on the array and use only a few extra variables, so their space complexity is \(\mathrm {O}(1)\). Merge sort, however, needs an additional temporary array during the merge step, so it requires \(\mathrm {O}(n)\) extra space.
10.3.7. Time complexity for BSTs
[comp1110-2Y] - Liam O'Connor
10.3.7. Time complexity for BSTs
[comp1110-2Y]- Liam O'Connor
The shape of a binary search tree has a big effect on how quickly (in terms of time complexity) we can look up or insert a student. Each operation involves following a path from the root down to a leaf, so the number of steps is roughly equal to the height of the tree. In a well-balanced tree, where the left and right branches of each node have about the same number of students, the height is approximately \(\log _2 n\) for \(n\) students. This means that each comparison eliminates roughly half of the remaining nodes, and lookups and insertions are fast, taking \(\mathrm {O}(\log n)\) steps. If the tree is unbalanced, for example if students are inserted in alphabetical order, the tree becomes like a long chain, with each new student added as the right child of the previous node. In this case, the height grows to \(n\), and lookups or insertions may require visiting every node, which is \(\mathrm {O}(n)\) time. Random insertion orders typically produce moderately well-balanced shape, so average lookup time is often close to \(\mathrm {O}(\log n)\) but the worst case could still be linear.
10.3.8. Analysing the Run-Time Complexity of a Function
[comp1110-7R] - Fabian Muehlboeck
10.3.8. Analysing the Run-Time Complexity of a Function
[comp1110-7R]- Fabian Muehlboeck
The basic idea here is that each expression and statement here has a cost.
Many standard expressions and statements have a constant cost - things like assignments, basic calculations with numbers, comparisons with ==, and so on.
Some things might be more expensive than others, but the key question you should always ask first when asked about the time complexity of a function is: "in terms of what?".
That is, what is our "n" (and potentially other variables)?
When we talk about the complexity of our searching and sorting functions, we always talk about the time complexity being in terms of the size of the given array. Oftentimes, it is quite clear what "n" should be, at other times, you can get very different results depending on your choice of parameter, so always keep in mind that it is important to be aware of what "n" is.
Once you have chosen some set of parameters you care about (i.e. at least one "n", but potentially more variables), you essentially want to look for places in your code where that size influences how much work you do. At the very least, this means looking for loops where the count of iterations depends on your parameters. You also have to watch out for function calls that use values depending on the parameters (say you pass your array to a helper function). In that case, you will have to check how that parameter influences the complexity of that function. You will therefore end up with a number of loops controlled by your parameters, and a number of other operations whose cost is not constant because they are influenced by your parameters.
Once you have your non-constant-cost operations and loops, you proceed as follows:
- look at your innermost loops (those that have no other loops controlled by your parameters nested within them).
Sum up the non-constant costs of any operations within them.
If there are no such operations, the cost of the code within the loop is constant itself, i.e. \(O(1)\).
Summing up complexity costs is essentially an operation looking for the maximum, i.e. \(O(n) + O(n) + O(log_2 n) = O(n)\), but \(O(n) + O(m) = O(n + m)\) if you do not have any specific information that one of
normis always larger than the other. - Now multiply the cost of the inner operations of the loop with how many times the loop will execute depending on your parameters (that is, a loop in binary search typically executes \(O(log_2 n)\) times, while a regular linear loop through an array or list executes \(O(n)\) times. If your inner operations have cost \(O(n)\), and your loop iterates \(O(n)\) times, you now have a cost of \(O(n * n) = O(n^2)\) for the whole loop).
- Collapse these inner loops to treat them as atomic operations of non-constant cost, and proceed again from step 1, until no loops are left. The complexity of your function is the sum (max) of the complexity all remaining operations.
Example 10.3.9. Analysing the Run-Time Complexity of a Function
[comp1110-7S] - Fabian Muehlboeck
Example 10.3.9. Analysing the Run-Time Complexity of a Function
[comp1110-7S]- Fabian Muehlboeck
Based on our simple rules, we now work an example of analysing the complexity of the following function in terms of the size of the given array:
This function contains two pairs of nested loops, and each loop mentions the variable n, which conveniently is the size of the given array.
We'll start by looking at the first innermost loop:
Array accesses and increasing the value of result are constant operations, so is checking a boolean in an if-condition.
That is, the body of the loop has time complexity \(O(1)\).
The loop itself starts from some value between 0 and n and goes up to n, without any constant limit as to how far away from n the start might be, and as such executes \(O(n)\) times.
Therefore, the overall time complexity of this loop is \(O(n * 1) = O(n)\).
We can now look at it's parent loop, having collapsed its inner loop:
This loop clearly executes n times, each with an \(O(n)\)-cost operation inside. As such, the total time complexity of this outer loop is \(O(n * n) = O(n^2)\).
Moving on to the second inner loop:
This one is more interesting. The inside of the loop again has a constant cost. But how often does the loop iterate? We start from the maximum of 0 and i-1, and go up to the minimum of i+2 and n.
Let's consider the cases:
- If
i-1is less than0, we start from0. Then:- If
i+2is less thann, we go up toi+2. Sincei-1is less than0,iis at most0, and thereforei+2is at most2. This means that the loop does at most 2 iterations, which is a constant. - Otherwise,
nis less than or equal toi+2, which we already established is at most2, and we again end up with at most 2, and therefore a constant number of, iterations.
- If
- If
i-1is greater than or equal to0, we start fromi-1. Then:- If
i+2is less thann, we go up toi+2. Sincei-1is3less thani+2, the loop does 3 iterations, which is a constant. - Otherwise,
nis less than or equal toi+2, which we already established is3more than the starting point. We again end up with at most 3, and therefore a constant number of, iterations.
- If
A constant number of iterations over a constant-cost operation means the total time complexity for the inner loop here is \(O(1 * 1) = O(1)\). Now for the parent loop:
This loop clearly executes n times, each with an \(O(1)\)-cost operation inside. As such, the total time complexity of this outer loop is \(O(n * 1) = O(n)\).
Having collapsed all our loops, we have:
The time complexity of this function is therefore: \(O(n + 1) = O(n)\), i.e. linear.
Lecture 11. Encapsulation and Abstraction
[comp1110-2026s2-lec06a] - September 1, 2026
- Fabian Muehlboeck
Lecture 11. Encapsulation and Abstraction
[comp1110-2026s2-lec06a]- September 1, 2026
- Fabian Muehlboeck
11.1. Encapsulation
[comp1110-7T] - Fabian Muehlboeck
11.1. Encapsulation
[comp1110-7T]- Fabian Muehlboeck
Construction 11.1.1. throwing exceptions
[comp1110-3I] - Liam O'Connor, Fabian Muehlboeck
Construction 11.1.1. throwing exceptions
[comp1110-3I]- Liam O'Connor, Fabian Muehlboeck
When a method is asked to do something impossible, like depositing a negative amount or withdrawing more than the balance of an account, we throw an exception. Modifying Example 11.1.6:
There are many types of exceptions, and you can create your own, but often the built-in types are sufficient.
IllegalArgumentException: Used for invalid inputs (e.g., refilling a negative amount).IllegalStateException: Used when the object is in an incorrect state for the requested action.
Construction 11.1.2. Packages
[comp1110-7U] - Fabian Muehlboeck
Construction 11.1.2. Packages
[comp1110-7U]- Fabian Muehlboeck
A Java Package is a collection of Java files that belong together. Those Java files all start with a line that specifies the package:
The above line specifies a package named package, which is nested in another package named the, and so on, with the outermost package's name being fully.
Packages form a hierarchy that mirrors the hierarchy of the file system - all Java files belonging to a package are stored in the same folder, whose name matches that of the package, and subfolders of that folder may contain Java files of a sub-package. Conversely, the parent folders of the package folder must match the name of any enclosing packages, and so on.
Package names are always written in lowercase, so they do not get confused with class names (which should always start with an upperclase letter).
Most of the classes in Java's standard library are stored within an outmost package named java.
For example, the Arrays class that contains methods to compare arrays for equality is contained in the package java.util.
The fully qualified name of the package is java.util, and the fully qualified name of the class is java.util.Arrays.
If you plan on creating libraries for wide-spread use, the convention is to use package names that start with the inverted top-level domain of your organization (or yourself).
For example, any packages published by Google are sub-packages of the com.google package, and any packages that I would publish through ANU would start with au.edu.anu.comp.
In general, packages prevent naming conflicts (consider how many different people have written classes named Person) - class names only need to be unique within a package.
The fully qualified names can always be used to distinguish multiple classes with the same name.
Files without a package declaration are in what is called the default package. Code in those files can access definitions in Java files in the same folder, and within explicit packages. However, no code in an explicit package can access code in the default package - that package is really only for small scale and ad-hoc code.
When a class is within a package, you need to use the fully qualified name of the class to run a main method in it.
For example, if you have a class Foo with a main method in the package bar.zorp, you need to call java as follows:
You also need to do this from the folder that contains the bar folder.
Construction 11.1.3. Import Declarations
[comp1110-7V] - Fabian Muehlboeck
Construction 11.1.3. Import Declarations
[comp1110-7V]- Fabian Muehlboeck
When a class is defined within an explicit package, you can always use its fully qualified name to use it.
However, this gets annoying very quickly.
Hence, you can use import declarations to import certain classes, which allows you to use them by their own (not fully qualified) name:
Sometimes, you want to simply import every class in a particular package:
This construction is generally discouraged, because it can create naming conflicts if a new class is added to a package that conflicts with another name in your package or another imported package, even if you were not intending to use that class.
Construction 11.1.4. Access Modifiers
[comp1110-7W] - Fabian Muehlboeck
Construction 11.1.4. Access Modifiers
[comp1110-7W]- Fabian Muehlboeck
Data type definitions (e.g. Enums and Class Definitions) and members may be given an access modifier. The following access modifiers exist in Java:
public- this means that the data type or member should be accessible by everyone, from anywhere.private- this means that the data type or member should only be accessible from within the scope of the outermost enclosing context.protected- this will be explained later and is not important for now.
In addition to these modifiers, the meaning of not having a modifier at all is called package private.
These data types or members are accessible from everywhere in the same package, as if they were public, but inaccessible from outside of that package, as if they were private.
Access modifiers are about syntactic scope, that is, about how code is written and nested.
In particular, any private field can be accessed from any code within the same class, not just from within the same object.
11.1.5. Values vs. Entities
[comp1110-3C] - Liam O'Connor, Fabian Muehlboeck
11.1.5. Values vs. Entities
[comp1110-3C]- Liam O'Connor, Fabian Muehlboeck
Think of the number 5. If you add 1 to it, you don't "change" the 5; you simply produce to a completely different value, 6. The number 5 is immutable and has no identity other than its value.
Enums behave like this, which is why you can compare them with ==.
Strings, on the other hand, are immutable, but have identity, so there can be two String entities that represent the same text, but are different entities, and so == does not work.
Now think about your bank account. If you deposit 100 dollars into your bank account, you don't get a new bank account, rather, your existing bank account is updated to reflect the new reality. This is also important when there are multiple people accessing the same bank account - they should see the same data, which would not be true if everyone kept creating new copies of it every time they change something. We saw this concept before when sorting arrays and when Inserting into a binary search tree.
Example 11.1.6. Bank account class
[comp1110-3E] - Liam O'Connor, Fabian Muehlboeck
Example 11.1.6. Bank account class
[comp1110-3E]- Liam O'Connor, Fabian Muehlboeck
In this snippet, the fiekd balance store the object's data, while the constructor defines how that data is initialised.
The balance field uses the private access modifier, while the constructor and the two other methods are public.
Definition 11.1.7. Encapsulation
[comp1110-3F] - Liam O'Connor, Fabian Muehlboeck
Definition 11.1.7. Encapsulation
[comp1110-3F]- Liam O'Connor, Fabian Muehlboeck
By marking fields as private, we hide the internal state. We then provide public methods (the interface) as the only way to interact with that state. This is important for two reasons:
- Integrity - we can maintain data invariants about the object's internal state: properties that should be true before and after each call to a method on the object. For example, in Example 11.1.6,because balance is private, a user cannot write
account.balance = -500;. They are forced to use deposit(), where we can include logic to ensure that the balance never goes negative. This enforces the data invariant that the account balance is always \(\geq 0\). - Abstraction - we can separate the interface (what methods are exposed to the
public) from the implementation (the private fields and method implementations). This means that users of this class need only know about the interface, which reduces cognitive load on the programmer — essential for large programs. It also enables flexibility. For example, in Example 11.1.6, if we later decide to store the balance in a different currency or a different data type, we only change the code inside the class. Anyone callinggetBalance()won't even notice the change.
Definition 11.1.8. Getter
[comp1110-3G] - Liam O'Connor
Definition 11.1.8. Getter
[comp1110-3G]- Liam O'Connor
In Example 11.1.6, our balance field is private, so we provide getter methods to allow the outside world to read the state without being able to write to it directly.
Note that we do not provide a setBalance method. This ensures that the only way to change the balance is through valid actions like deposit().
Definition 11.1.9. Setter
[comp1110-7X] - Fabian Muehlboeck
Definition 11.1.9. Setter
[comp1110-7X]- Fabian Muehlboeck
The counterpart to a getter is called a setter. This describes methods that take a parameter to set a particular property of an object.
A setter may do a number of checks on the given value before accepting it, may reject it (either by not changing any state or by throwing an exception).
Typical setters return void, but may also return the receiving object, the new value, the old value, or a value that indicates the success of the operation.
Setters may also affect the state of any sort of related values in order to preserve data invariants.
A setter does not necessarily have to store the given value directly somewhere, it may be that its representation changes.
However, it is generally an expectation for a setter that a corresponding setter would, upon success, return the given value.
As such, deposit in Bank account class is not a setter.
Example 11.1.10. Setter
[comp1110-7Y] - Fabian Muehlboeck
Example 11.1.10. Setter
[comp1110-7Y]- Fabian Muehlboeck
The following would be a valid setter in Bank account class:
However, for that specific data type, it is generally a bad idea to offer the possibility of arbitrarily changing the account balance to everyone.
11.2. Abstraction
[comp1110-7Z] - Fabian Muehlboeck
11.2. Abstraction
[comp1110-7Z]- Fabian Muehlboeck
Construction 11.2.1. Interfaces
[comp1110-3L] - Liam O'Connor, Fabian Muehlboeck
Construction 11.2.1. Interfaces
[comp1110-3L]- Liam O'Connor, Fabian Muehlboeck
An interface is a reference type in Java that specifies a set of method signatures but does not provide their implementations.
It serves as a contract: any class that implements the interface promises to provide the logic for those methods.
An interface is typically open: any number of classes can implement it across different parts of a codebase.
You cannot create instances of interfaces directly, they must always be objects of a class implementing the interface.
Example 11.2.2. Generalizing Accounts
[comp1110-8A] - Fabian Muehlboeck
Example 11.2.2. Generalizing Accounts
[comp1110-8A]- Fabian Muehlboeck
Here is an interface describing the operations in Bank account class on a high level:
This matches the structure of the public methods of the BankAccount class in Bank account class.
That is a necessary, but not sufficient condition to be able to use Account to abstract over BankAccount.
In Java, you need to explicitly declare that this is your intention, by adding an implements declaration to the definition of the BankAccount class:
With this declaration, the Java compiler will check that the class has a public method matching each of the signatures in the interface.
We can now write a method that deals with any form of Account:
The above method takes an Account object, prints its balance, deposits 500, and prints the balance again.
This works fine with any BankAccount.
In addition, it will also work with other kinds of accounts, such as the following one modelling a frequent flyer account:
This class behaves differently from a BankAccount. It starts with an opening balance of 10000 as a "new member bonus".
In addition, whenever points are deposited, the member gets a 1The important thing here is that both kinds of accounts behave according the specification given by the Account interface, which only specifies preconditions and method signatures, both satisfied by both implementations.
Definition 11.2.3. Abstract Data Types
[comp1110-3P] - Liam O'Connor
Definition 11.2.3. Abstract Data Types
[comp1110-3P]- Liam O'Connor
Abstraction is the elimination of detail. In art, a realistic sketch of a bull captures every muscle and shadow, whereas an abstract version strips those away to convey the "bull" in a few lines. You lose specific texture but gain a universal representation.
In computer science, the interface is that line drawing. We eliminate the details of how data is stored and processed, instead defining a type, called an abstract data type or ADT, by the behaviour of the operations it supports alone. This separation of interface from implementation is a very powerful concept in programming. This allows the rest of the system to interact with the type abstractly, without regard to implementation details. This reduction of detail minimizes the mental load on the programmer and makes the code more flexible.
Construction 11.2.4. Inner Class
[comp1110-8B] - Fabian Muehlboeck
Construction 11.2.4. Inner Class
[comp1110-8B]- Fabian Muehlboeck
An inner class is a class defined within some other class, usually for the purpose of encapsulation.
Inner classes have access to the private members of the enclosing class, and vice versa.
Their objects must be constructed for a particular parent object of the enclosing class (this is implicit when creating the new instance from within an instance method of the parent class),
so that accessing fields of the parent class makes sense.
Inner classes are often private, because in most cases the form part of the inner representation of a given class.
They do not have to be, though in most cases where their objects would be access from outside of the parent class, they would still be private, but implement a public interface.
Example 11.2.5. The List ADT
[comp1110-8C] - Fabian Muehlboeck
Example 11.2.5. The List ADT
[comp1110-8C]- Fabian Muehlboeck
One of the most standard abstract data types is that of a List. A list is an ordered collection of items - ordered means that there is an order to the items in the list, that is, there is a first item, a second item, and so on. The list is not necessarily "sorted". Lists may contain duplicate values. One can typically query the element at a particular index in the list, add elements to a list, remove elements from a list, and get the length of a list.
There are in fact multiple levels of abstraction. The above description applies to pretty much anything a computer scientist would call a "list". The ability to change the size of the list by adding or removing elements is the key thing that distinguishes lists from arrays (lists can be, and in many cases are, implemented using arrays). But the description is at a very high level, and leaves out certain details that vary a lot between different libraries and programming languages, for example whether lists are mutable entities or the above abstract operations construct a new, slightly changed copy of the original list every time.
In Java, the typical choice is for things to be mutable, and so a standard list interface might look as follows:
This interface describes the above basic operations.
Using void as the return type of the add and remove operations indicates that it is the particular list object itself that is changing when those methods are called.
A more "functional" version of this code would return a new List object for those methods.
There are multiple ways to implement this interface. Here, we choose a linked list.
The above class implements our List interface. It uses an inner class to have a linked list implementation inside - the advantage for the user here is that they do not need to distinguish the base case from the recursive case themselves anymore, the list implementation does this for them. The methods in the class are implemented with a mix of recursion and iteration for demonstration purposes. In general, one should use iteration here in Java.
One can now write code using just the List interface.
The only point at which the LinkedList class comes in is when the particular list object is constructed, though in that place, it could also be replaced with any other class implementing the List interface.
Lecture 12. Subtyping and Inheritance
[comp1110-2026s2-lec06b] - September 3, 2026
- Fabian Muehlboeck
Lecture 12. Subtyping and Inheritance
[comp1110-2026s2-lec06b]- September 3, 2026
- Fabian Muehlboeck
Warning.
- September 3, 2026
- Fabian Muehlboeck
Packages were actually discussed in this lecture, but are part of the lecture notes for lecture 06A, because they fit better there.Warning.
- September 3, 2026
- Fabian Muehlboeck
Construction 12.1. Extending interfaces
[comp1110-3R] - Liam O'Connor, Fabian Muehlboeck
Construction 12.1. Extending interfaces
[comp1110-3R]- Liam O'Connor, Fabian Muehlboeck
Interfaces can form hierarchies using the extends keyword. When one interface extends another, it inherits all method signatures from the parent. This allows you to create specialised versions of an abstraction without duplicating the original definitions.
Interfaces may extend arbitrarily many other interfaces.
Construction 12.2. Inheritance
[comp1110-5M] - Liam O'Connor, Fabian Muehlboeck
Construction 12.2. Inheritance
[comp1110-5M]- Liam O'Connor, Fabian Muehlboeck
Inheritance allows a class to be defined as an extension of another, inheriting its fields and methods.
The extending class is called the subclass; the class being extended is the superclass. In Java, inheritance is expressed with extends:
The subclass gains all the fields and methods of the superclass, and can add new ones or override existing ones.
Unlike interfaces, which define only a contract, class inheritance also reuses implementation.
Partly for this reason, each class can extend only one superclass.
If no specific superclass is specified using the extends keyword, a class is taken to extend the special class Object (in Java, every kind of reference type at least transitively extends Object, which forms the top of the type hierarchy).
Warning.
- Liam O'Connor, Fabian Muehlboeck
Constructors are not inherited. A subclass must define its own. To invoke the superclass constructor, use Warning.
- Liam O'Connor, Fabian Muehlboeck
super() as the first statement in the subclass constructor.
Construction 12.3. Overriding and super
[comp1110-5R] - Liam O'Connor
Construction 12.3. Overriding and super
[comp1110-5R]- Liam O'Connor
A subclass can override a method inherited from its superclass by redeclaring it with the same signature. The @Override annotation, which you may have already seen before, is optional but recommended: the compiler will catch cases where the signature doesn't actually match anything in the superclass.
Within an overriding method, super.method(...) calls the superclass's version of the method. This is useful when the subclass wants to extend rather than replace the superclass behaviour. Similarly, super() in a constructor calls the superclass constructor.
12.4. Dynamic Dispatch
[comp1110-8F] - Fabian Muehlboeck
12.4. Dynamic Dispatch
[comp1110-8F]- Fabian Muehlboeck
Exploiting the concept of subtyping, object-oriented programming gets its power via dynamic dispatch, which is the idea that when you call a method on an object, it is the run-time class of the receiver object that determines the code of the method, not the type of the receiver at compile time.
This is relatively obvious in the following example (using the definitions of Generalizing Accounts):
Since Account is an interface where the deposit method has no code, it should be clear that the code executed for deposit is the one defined in the PointsAccount class.
Now assume we create a subclass of PointsAccount:
The point of dynamic dispatch is that even when we could find some code to call based on the type of the receiver (that is, the code in the PointsAccount class),
the actual method that will be called is the one of the receiving object at runtime, i.e. the one defined in PremiumPointsAccount.
This is the key idea that distinguishes object-oriented code from non-object-oriented code. Programming languages are called object-oriented when they encourage a style of programming that rely heavily on this feature, as Java does. In contrast, Java code that does not rely on dynamic dispatch is not really object-oriented code.
Definition 12.5. Subtyping
[comp1110-33] - Liam O'Connor
Definition 12.5. Subtyping
[comp1110-33]- Liam O'Connor
A type T is said to be a subtype of U iff, whenever a U is required by a program, a T may be supplied.
12.6. Subtyping and the Type Hierarchy
[comp1110-8D] - Fabian Muehlboeck
12.6. Subtyping and the Type Hierarchy
[comp1110-8D]- Fabian Muehlboeck
You can think of types as essentially mathematical sets of values.
For example, the type int in Java is the set of all 32-bit signed integer values (i.e. from \(-2^{31}\) to \(2^{31}-1\) (\(2^{31}\) is a bit over 2 billion)).
Similarly, the type String is the set of all potential String objects, plus null, because in Java, null is a value of every reference type.
The set of values of a subtype, then, is a subset of the values of any supertypes.
Every reference type is (at least transitively) a subtype of the special class type Object, even the interface types (otherwise, interface types can generally not be subtypes of class types, because interfaces cannot extend classes).
From Java's perspective, subtyping relationships are created through (potentially implicit) extends and implements declarations and transitivity.
Java checks them for well-formedness in the sense that it checks that any overridding methods are compatible with the overridden ones in their superclass, and any unimplemented methods inherited from interfaces are actually implemented in classes declaring to implement those interfaces.
Where Java considers a subtyping relationship to exist (based on the declarations), it allows using a value of the subtype as one of the supertype.
Definition 12.7. Liskov Substitution Principle
[comp1110-5U] - Liam O'Connor, Fabian Muehlboeck
Definition 12.7. Liskov Substitution Principle
[comp1110-5U]- Liam O'Connor, Fabian Muehlboeck
Inheritance is not merely a mechanism for code reuse, but also a semantic claim. If B extends A,
then a B ought to be usable anywhere an A is expected. This is only safe if B behaves consistently with
A's contract, not just its signatures. This requirement is called behavioural subtyping.
The Liskov substitution principle (LSP) makes this precise. If B is a subtype of A, then for any method overridden in B:
- No stronger preconditions:
B's method must accept at least everythingA's method accepts. - No weaker postconditions:
B's method must guarantee at least everythingA's method guarantees. - Invariants preserved: any invariant of
Amust also hold forB. - History constraint:
Bmust not allow state changes thatAwould not permit.
Violating LSP means that code written against A may break when given a B, even though the compiler accepts it.
12.8. Subtyping vs Inheritance
[comp1110-8E] - Fabian Muehlboeck
12.8. Subtyping vs Inheritance
[comp1110-8E]- Fabian Muehlboeck
It is common for many Java courses to put code inheritance front and center. This is wrong!
Yes, it is important to not repeat yourself, especially for maintainable code, and so inheritance is a useful feature. However, it is better to repeat yourself than to violate the assumptions of programmers using your code.
As such, always think of behavioural subtyping first when deciding on your inheritance relationships. Design your system in such a way that you can maximize code inheritance while always respecting behavioural subtyping!
Lecture 13. Testing, Abstraction
[comp1110-2026s2-lec07a] - September 22, 2026
- Fabian Muehlboeck
Lecture 13. Testing, Abstraction
[comp1110-2026s2-lec07a]- September 22, 2026
- Fabian Muehlboeck
Exercise 13.1. Box inheritance
[comp1110-5X] - Liam O'Connor, Fabian Muehlboeck
Exercise 13.1. Box inheritance
[comp1110-5X]- Liam O'Connor, Fabian Muehlboeck
Consider the following interface, implemented by four classes that each draw a box to the terminal (here "colour" just means the character to draw to the terminal):
The four classes are:
WhiteBox(width, height, frame)— fill colour is always' '.BlackBox(width, height, frame)— fill colour is always the same as the frame colour.GreyBox(width, height, frame, fill)— fill colour is the given fill character.ColouredBox(width, height, frame, fill)— likeGreyBoxbut with an additionalsetFillColour(char c)method.
Which subtyping relationships are LSP compliant?
Solution.
- Liam O'Connor, Fabian Muehlboeck
Solution.
- Liam O'Connor, Fabian Muehlboeck
WhiteBoxextendsGreyBox— compliant.WhiteBoxis aGreyBoxwith fill fixed to' '. Since constructors are not inherited, a caller ofGreyBox's interface methods has no expectation about how the fill colour was initially chosen.WhiteBoxsatisfies all ofGreyBox's method contracts, with a stronger invariant on fill.BlackBoxextendsGreyBox— not compliant.BlackBoxties the fill colour to the frame colour, so callingsetFrameColourhas a side effect ongetFillColour. A caller ofGreyBoxwould not expectsetFrameColourto change the fill — this violates the history constraint.ColouredBoxextendsGreyBox— not compliant. AlthoughColouredBoxdoes not change the behaviour of any existing method, the additionalsetFillColourmethod allows the fill colour to be mutated after construction.GreyBoxhas an implicit history constraint — once constructed with a fill colour, that fill does not change.ColouredBoxviolates this.GreyBoxextendsWhiteBox— not compliant.WhiteBoxhas an invariant that fill is always' '.GreyBoxallows any fill colour, breaking this invariant.ColouredBoxextendsWhiteBox— not compliant. Violates both the fill invariant ofWhiteBoxand the history constraint, sincesetFillColourallows the fill to be changed after construction.BlackBoxextendsWhiteBox— not compliant. The fill colour is the frame character, which is not necessarily' ', breakingWhiteBox's fill invariant.
13.2. Common modifier keywords
[comp1110-8H] - Fabian Muehlboeck
13.2. Common modifier keywords
[comp1110-8H]- Fabian Muehlboeck
Construction 13.2.1. Static members
[comp1110-8I] - Fabian Muehlboeck
Construction 13.2.1. Static members
[comp1110-8I]- Fabian Muehlboeck
Construction 13.2.1.1. Static methods
[comp1110-4F] - Liam O'Connor, Fabian Muehlboeck
Construction 13.2.1.1. Static methods
[comp1110-4F]- Liam O'Connor, Fabian Muehlboeck
A static method belongs to the class itself rather than to any particular instance.
It is declared with the static keyword and called on the class name rather than on an object:
Because a static method has no associated instance, it cannot access instance fields or call instance (non-static) methods. It can only work with its arguments and other static members of the class — it is essentially a function.
Static methods are useful for utility functions that are logically associated with a class but do not depend on any particular object's state. You have already seen examples of this in the standard library, such as the utility functions for characters or array comparison.
Construction 13.2.1.2. Static fields
[comp1110-8J] - Fabian Muehlboeck
Construction 13.2.1.2. Static fields
[comp1110-8J]- Fabian Muehlboeck
A static field belongs to the class itself rather than to any particular instance.
It is declared with the static keyword and accessed via the class name rather than on an object:
Static fields can be useful to share information across the instances of a class - they are essentially global variables. Global variables - especially the ones that change their values - can be very dangerous, so use them sparingly!
Construction 13.2.1.3. Static nested classes
[comp1110-8K] - Fabian Muehlboeck
Construction 13.2.1.3. Static nested classes
[comp1110-8K]- Fabian Muehlboeck
Like inner classes, static nested classes are class definitions nested within another class definition:
The key difference is that the instances of a static nested class do not have to be associated with an instance of the enclosing class. In exchange, they also do not have access to the instance fields of the enclosing class.
Essentially, static nested classes are treating their enclosing class as a package. The main point of having them is for encapsulation.
Construction 13.2.2. Final classes and methods
[comp1110-5S] - Liam O'Connor
Construction 13.2.2. Final classes and methods
[comp1110-5S]- Liam O'Connor
A class declared final cannot be subclassed. A method declared final cannot be overridden. These are guarantees to the compiler and to readers of the code that no subclass will change the behaviour of the class or method in question.
This final keyword is useful when a class has been carefully designed with a specific contract and allowing subclasses to override behaviour could silently violate it.
Construction 13.2.3. Final fields and variables
[comp1110-8L] - Fabian Muehlboeck
Construction 13.2.3. Final fields and variables
[comp1110-8L]- Fabian Muehlboeck
Fields can be declared final, which means that they have to be set once when an object is constructed, and cannot be changed afterwards.
Local variables can also be declared final, which again means that they cannot be reassigned.
static final fields are also called constants, and the naming convention is for them to be in all caps:
Construction 13.2.4. Abstract classes and methods
[comp1110-5N] - Liam O'Connor, Fabian Muehlboeck
Construction 13.2.4. Abstract classes and methods
[comp1110-5N]- Liam O'Connor, Fabian Muehlboeck
An abstract class is a class that cannot be instantiated directly: it exists only to be extended.
It may define fields and concrete methods that subclasses inherit, as well as abstract methods that subclasses must implement.
Abstract classes occupy a middle ground between interfaces (no implementation) and concrete classes (full implementation).
In Java, abstract classes and methods are declared with the abstract keyword.
13.3. Designing and Testing an Interface
[comp1110-8G] - Fabian Muehlboeck
13.3. Designing and Testing an Interface
[comp1110-8G]- Fabian Muehlboeck
Example 13.3.1. The GuestList interface (String version)
[comp1110-3X] - Liam O'Connor, Fabian Muehlboeck
Example 13.3.1. The GuestList interface (String version)
[comp1110-3X]- Liam O'Connor, Fabian Muehlboeck
Our GuestList interface will represent a set of names. The interface exposes three operations:
The semantics are set-like: a guest is either coming or not; adding the same guest twice has the same effect as adding them once. The interface says nothing about how this is implemented — that is left to implementing classes.
Definition 13.3.2. Unit testing with JUnit
[comp1110-40]
Definition 13.3.2. Unit testing with JUnit
[comp1110-40]A unit test checks a single, small piece of a program in isolation from the rest of the system. Unit tests contrast with integration tests, which test how multiple components work together.
JUnit is the standard unit-testing framework for Java. A JUnit test is an ordinary Java method annotated with @Test. Inside the method, special JUnit methods are used similarly to the assertions were used previously in this course:
assertTrue(expr)— fails ifexpris falseassertFalse(expr)— fails ifexpris trueassertEquals(expected, actual)— fails if the two values differ- Many more, see the JUnit documentation for a full list.
If any assertion fails, JUnit reports the test as failed and shows which assertion was violated.
Definition 13.3.3. Test-Driven Development
[comp1110-41] - Liam O'Connor
Definition 13.3.3. Test-Driven Development
[comp1110-41]- Liam O'Connor
Test-driven development (TDD) is a practice where tests are written before the implementation. The workflow is:
- Write a test for the behaviour you want
- Watch it fail (since there is no implementation yet)
- Write the minimum implementation to make it pass
- Repeat
This encourages thinking carefully about what a piece of code should do before thinking about how it should work. It also ensures that every piece of behaviour has a test from the start.
Definition 13.3.4. Black-box and white-box testing
[comp1110-3Z]
Definition 13.3.4. Black-box and white-box testing
[comp1110-3Z]When writing tests we can take two perspectives on the code under test:
- In black-box testing, tests are written against the interface (or specification) only, without looking at the implementation. This is the preferred approach for unit-testing interfaces, because the tests remain valid across all implementations.
- In white-box testing, tests are written with knowledge of the implementation internals, targeting specific branches, edge cases, or internal states.
Example 13.3.5. JUnit tests for GuestList
[comp1110-3Y] - Liam O'Connor
Example 13.3.5. JUnit tests for GuestList
[comp1110-3Y]- Liam O'Connor
The following unit tests exercise the GuestList interface in a black-box fashion. To use JUnit in IntelliJ, create a new Java file and include the following imports at the top. You can run all tests in the file by pressing the green play button next to the class name, or run a single test by pressing the play button next to that test method.
Notice that the variable gl is declared as GuestList, not as a specific implementation class. This ensures the test targets the interface contract rather than any implementation detail, respecting abstraction boundaries. Each @Test method is independent: JUnit creates a fresh instance of the test class for each one, so the two tests do not interfere with each other.
Example 13.3.6. Lazy array implementation of GuestList
[comp1110-42] - Liam O'Connor, Fabian Muehlboeck
Example 13.3.6. Lazy array implementation of GuestList
[comp1110-42]- Liam O'Connor, Fabian Muehlboeck
The first implementation of the GuestList interface stores names in a plain String[] array. It is called lazy because add does not check for duplicates before inserting. Duplicates are only resolved later, at remove time, by nulling out all occurrences.
The lazy approach is simple but wastes space: the array may accumulate many nulls over time. When the array is filled up, we double its size (see Definition 13.3.7).
Definition 13.3.7. Amortised complexity and array doubling
[comp1110-43] - Liam O'Connor, Fabian Muehlboeck
Definition 13.3.7. Amortised complexity and array doubling
[comp1110-43]- Liam O'Connor, Fabian Muehlboeck
When an array-backed collection is full, we cannot simply extend it, because arrays have a fixed length. Instead, we create a new array of double the current size and copy all elements across. This makes any individual add that triggers a resize cost \(\mathrm {O}(n)\). However, such expensive calls are rare: after doubling, the next \(\frac {n}{2}\) insertions are cheap (\(\mathrm {O}(1)\) each).
Amortised analysis spreads the cost of an expensive operation across the many cheap operations that preceded it. If we charge each element a small constant at the time it is first inserted to "pre-pay" for its eventual copy, we find that each insertion costs \(\mathrm {O}(1)\) on average over any sequence of \(n\) insertions, even though individual insertions may cost \(\mathrm {O}(n)\).
To see that this is the case here, consider the overall cost of what we have done so far after \(n + 1\) insertions. In the worst case, we just had to double the size of the array for the last insertion, meaning that the array now has size \(2n\). That last doubling involved \(n\) copies. The doubling before that involved \(\frac {n}{2}\) copies. The one before that involved \(\frac {n}{4}\) copies. And so on. All of those copy operations sum up to less than \(2n\) copies. This means that per insertion (of the \(n + 1\) insertions we did so far), we made less than 2 copies (a constant), plus the constant amount of work for the insertion. The combination of those costs is still constant.
Example 13.3.8. Strict array implementation of GuestList
[comp1110-45] - Liam O'Connor, Fabian Muehlboeck
Example 13.3.8. Strict array implementation of GuestList
[comp1110-45]- Liam O'Connor, Fabian Muehlboeck
This implementation of GuestList is called strict (as opposed to the lazy implementation) because it enforces uniqueness eagerly: add calls isComing first and returns immediately if the guest is already present.
This implementation maintains the following data invariant: all non-null entries in data from 0 to count-1 are distinct.
Because we are in the same package as GuestArrayList, we have access to its package-private fields and know about how its methods work.
We also know that enforcing uniqueness changes nothing about the externally expected behaviour of the class.
As such, we can make our new class a subtype of GuestArrayList, and use inheritance to reuse existing code, changing only what needs to be changed:
The add method is overridden to the isComing check first. However, after this check, the code of the method is the same as before, and we can just call the superclass' implementation of that method instead of copying it into here.
Because the invariant guarantees at most one copy exists, remove can stop after finding the first match. Contrast with the lazy implementation, where remove must scan the entire array to null out all copies.
However, because this change to the code of remove is deep within the code of that method, we have to rewrite it fully, rather than calling the superclass' implementation.
Warning 13.3.9. Encapsulation and defensive copying
[comp1110-47] - Liam O'Connor
Warning 13.3.9. Encapsulation and defensive copying
[comp1110-47]- Liam O'Connor
Encapsulation means that the internal state of an object can only be changed through its defined methods. This is broken if a method returns a direct reference to an internal array or other mutable object, because the caller can then modify it from outside.
The fix is to return instead a defensive copy: a fresh object with the same contents. The caller is then free to do whatever they like with the copy without affecting the internal state of the object.
Example 13.3.10. Encapsulation danger when returning internal arrays
[comp1110-46] - Liam O'Connor, Fabian Muehlboeck
Example 13.3.10. Encapsulation danger when returning internal arrays
[comp1110-46]- Liam O'Connor, Fabian Muehlboeck
Suppose we extend the GuestList interface with a method coming() that returns the current list of guests. This seems straightforward, but a naive implementation illustrates a common encapsulation hazard: returning a direct reference to the internal array.
A caller who receives this reference can write list.coming()[0] = "Intruder" and corrupt the guest list without going through add or remove, potentially breaking the data invariant. The correct fix is to return a defensive copy:
Lecture 14. Generics, Collections, Hashing and Hash Tables
[comp1110-2026s2-lec07b] - September 24, 2026
- Fabian Muehlboeck
Lecture 14. Generics, Collections, Hashing and Hash Tables
[comp1110-2026s2-lec07b]- September 24, 2026
- Fabian Muehlboeck
Warning.
- September 24, 2026
- Fabian Muehlboeck
We started with hashing and hash tables at the end of this lecture, but the relevant notes will be in Lecture 08A.Warning.
- September 24, 2026
- Fabian Muehlboeck
14.1. Making Our Types Generic
[comp1110-8M] - Fabian Muehlboeck
14.1. Making Our Types Generic
[comp1110-8M]- Fabian Muehlboeck
Construction 14.1.1. The Object type
[comp1110-4A] - Liam O'Connor, Fabian Muehlboeck
Construction 14.1.1. The Object type
[comp1110-4A]- Liam O'Connor, Fabian Muehlboeck
In Java, Object is the root of the class hierarchy: every class is a subtype of Object. This means a variable of type Object can hold a value of any class: a String, an Integer, a Person, and so on.
The Object type, among other things, defines a .equals() method, so values of type Object can be compared for equality, but very few other operations are available. Many classes provide override .equals() to perform a deep comparison, as we have seen for Strings.
The downside is that the compiler knows very little about the value stored in an Object variable: to use it as its original type, an explicit cast is needed.
Example 14.1.2. The GuestList interface (Object version)
[comp1110-49] - Liam O'Connor, Fabian Muehlboeck
Example 14.1.2. The GuestList interface (Object version)
[comp1110-49]- Liam O'Connor, Fabian Muehlboeck
The original GuestList interface used String throughout. Replacing String with Object allows any type of value to be stored, since every Java class is a subtype of Object:
Our implementations (both strict and lazy) can be easily adapted by
replacing the type String with Object throughout.
Because Object already specifies the equals method, we've gained a lot of flexibility for essentially no cost, which is great!
The tricky part comes when we are also supposed to return values stored in our list.
Consider our previous extension of the above interface where we also return a list of everyone who is coming, this time with Object:
This method now returns an array of Objects, when our testing code would presumably expect an array of Strings.
We can adjust our testing code to expect Object arrays, and we can explicitly cast the elements of the array to our expected type.
However, this approach sacrifices type safety. Nothing prevents a caller from adding a mix of unrelated types to the same list, and retrieving a value requires an explicit cast (e.g. (String) list.coming()[0]) that can fail at runtime with a ClassCastException. The compiler cannot catch such errors.
Construction 14.1.3. Casts
[comp1110-4B] - Liam O'Connor
Construction 14.1.3. Casts
[comp1110-4B]- Liam O'Connor
A cast tells the compiler to treat a value as a more specific type than it currently knows about. The syntax is to write the target type in parentheses before the expression:
Casts are necessary when retrieving values from a collection that stores Object, since the compiler only knows the value is an Object, not what type it originally was when created.
However, if the cast is wrong (for example, casting a String to an Integer), Java throws an exception called ClassCastException at runtime. The compiler cannot catch this mistake. This is one of the key motivations for type parameters, which avoid the need for casts entirely.
Construction 14.1.4. Type parameters
[comp1110-48] - Liam O'Connor, Fabian Muehlboeck
Construction 14.1.4. Type parameters
[comp1110-48]- Liam O'Connor, Fabian Muehlboeck
A type parameter allows a class or interface to be written once and used with many different types, while retaining type safety (unlike Object). We write the type parameter in angle brackets after the class or interface name, and use it as a placeholder for a concrete type throughout. Referring to our GuestList example:
When using the interface, the caller fills in the placeholder with a concrete type:
The compiler then treats every occurrence of T as String for that particular instance of the class. This means list.add(42) is a compile-time error, since 42 is not a String. Whereas using Object, such mistakes are only caught at run time.
This feature is also known as generics in Java, or more generally as parametric polymorphism.
Example 14.1.5. Lazy Array Implementation of GuestList (generic version)
[comp1110-4C] - Liam O'Connor, Fabian Muehlboeck
Example 14.1.5. Lazy Array Implementation of GuestList (generic version)
[comp1110-4C]- Liam O'Connor, Fabian Muehlboeck
Updating ArrayGuestList to match the generic interface requires adding the type parameter <T> to the class declaration and replacing Object with T in the method signatures. The internal storage remains Object[], however, due to type erasure:
Type erasure is also the reason why the coming method needs an additional argument.
Because we are expected to return an array of T, but still cannot create one, we need to be given an object that can create such an array for us, given a size.
Function is a generic interface with one method (called apply) that accepts two type arguments - the first one is the argument type of apply, the second is the return type.
In this case, any object satisfying Function<Integer, T[] has an apply method that takes an integer (meant to be the intended size of the array) and returns an array (hopefully of the given size).
A user of coming can of course pass any object satisfying the interface, but for any concrete array type, say, String[], we can simply write:
This special notation refers to the constructors of a given type, which for arrays always take an integer for the size of the array. We may talk about why this works at the end of the semester.
Warning 14.1.6. Type erasure
[comp1110-4D] - Liam O'Connor, Fabian Muehlboeck
Warning 14.1.6. Type erasure
[comp1110-4D]- Liam O'Connor, Fabian Muehlboeck
Java generics are implemented via type erasure: at runtime, the type parameter T is erased to Object, so the expression new T[n] is illegal: the Java runtime has no idea what T is.
Any generic class that needs an array internally must therefore either allocate an Object[] and cast values out of it appropriately, or somehow get an object that knows what to create:
The casts inserted here work in practice as long as only values of type T are inserted — and this should be maintained through encapsulation — but it is a genuine limitation of how Java implements generics.
14.1.7. Subtyping with Generics
[comp1110-8O] - Fabian Muehlboeck
14.1.7. Subtyping with Generics
[comp1110-8O]- Fabian Muehlboeck
Generics introduced a new kind of type: instantiated generic types.
These are class types (which we have seen before), but now with type arguments, e.g. List<String>.
In Java, subtyping is largely based on the inheritance hierarchy.
With generics, that is still true, and in most cases, the additional requirement is that the type arguments between two types are the same.
For example, in the Java Collections API, there are concrete classes ArrayList<T> and LinkedList<T>, which both
implement the generic list interface List<T>, which in turn extends the generic interface Iterable<T>, which is a supertype of almost all collection interfaces and classes.
Without type arguments, it therefore holds that ArrayList and LinkedList are subtypes of List, and List is a subtype of Iterable (and so are, transitively, ArrayList and LinkedList).
This gets preserved with type arguments if the type arguments are the same.
Therefore:
LinkedList<String>is a subtype ofList<String>is a subtype ofIterable<String>ArrayList<String>is a subtype ofList<String>is a subtype ofIterable<String>LinkedList<Object>is a subtype ofList<Object>is a subtype ofIterable<Object>ArrayList<Object>is a subtype ofList<Object>is a subtype ofIterable<Object>
LinkedList<String>is not a subtype ofList<Object>List<String>is not a subtype ofIterable<Object>ArrayList<Object>is not a subtype ofList<String>List<Object>is not a subtype ofIterable<String>- ...
The reason for this in principle is Behavioural Subtyping.
Take, for example, LinkedList<String> and List<Object>.
From the outer types, we know that if at all, LinkedList<String> would be a subtype of List<Object>.
This works fine for reading from the list:
Since a LinkedList<String> stores String values, and objs.get is supposed to return an Object,
and every String is an Object, this code is not problematic (though illegal).
The problem here comes in when we write to the list:
A List<Object> should be able to contain arbitrary objects, so the call to add with a Person value is fine.
But that would be a problem if the underlying list object is a List<String>, which is not supposed to contain Person values.
The same argument works the other way around (that LinkedList<Object> is not a subtype of List<String>) - this time,
the problem is reading from the list, as one would expect that a List<String> returns a String, but a
LinkedList<Object> cannot guarantee that.
Warning.
- Fabian Muehlboeck
This argument would technically also apply to arrays.
However, Java (and other languages) violates type safety here by treating Warning.
- Fabian Muehlboeck
String[] as a subtype of Object[].
You can write code that relies on this so long as you only read from those arrays - trying to write a value of a wrong type to them will throw an exception at run time.
You still cannot treat an Object[] as an array of some other type (e.g. String[]).
This will fail at the point where you cast the array to that other array type, even if you never read from the array.
This is why we could not simply cast the array in implementing the coming function in the GuestList example.
14.2. Java Collections
[comp1110-8N] - Fabian Muehlboeck
14.2. Java Collections
[comp1110-8N]- Fabian Muehlboeck
14.2.1. The Java collections API
[comp1110-4E] - Liam O'Connor, Fabian Muehlboeck
14.2.1. The Java collections API
[comp1110-4E]- Liam O'Connor, Fabian Muehlboeck
Java provides a rich set of built-in data structures in the java.util package, collectively known as the collections API. Now that we have built GuestList from scratch, we can see how the same ideas appear in the standard library.
The GuestList interface is essentially a set, and Java provides a Set<T> interface for exactly this purpose. The TreeSet<T> implementation of Set<T> stores elements in sorted order using a binary tree structure:
The collections API also provides:
List<T>— an ordered sequence, withArrayList<T>(array-based with amortised expansion, like ourArrayGuestList) andLinkedList<T>(linked list-based) as implementations. See the List documentation.Map<K,V>— a mapping from keys to values, with e.g.TreeMap<K,V>as a tree-based implementation. See the Map documentation.
In practice, you would use these standard library types rather than writing your own. The exercise of building GuestList was to understand what is happening under the hood.
14.2.2. Factory methods in the collections API
[comp1110-4H] - Liam O'Connor, Fabian Muehlboeck
14.2.2. Factory methods in the collections API
[comp1110-4H]- Liam O'Connor, Fabian Muehlboeck
The Java collections API provides convenient factory methods for creating collections with a fixed set of elements. These are static methods named of() on the collection interfaces:
These are particularly handy in tests where you need a quick collection to work with without constructing it element by element.
Warning.
- Liam O'Connor, Fabian Muehlboeck
Warning.
- Liam O'Connor, Fabian Muehlboeck
Note that collections created with of() are immutable: calling add() or remove() on them will throw an UnsupportedOperationException at runtime. If you need a mutable collection, construct one explicitly using e.g. new TreeSet<>(Set.of(...)).
Warning.
- September 24, 2026
- Fabian Muehlboeck
We started with hashing and hash tables at the end of this lecture, but the relevant notes will be in Lecture 08A.Warning.
- September 24, 2026
- Fabian Muehlboeck
Lecture 15. Hashing and Hash Tables, Part II
[comp1110-2026s2-lec08a] - September 29, 2026
- Fabian Muehlboeck
Lecture 15. Hashing and Hash Tables, Part II
[comp1110-2026s2-lec08a]- September 29, 2026
- Fabian Muehlboeck
15.1. Boxing
[comp1110-8P] - Fabian Muehlboeck
15.1. Boxing
[comp1110-8P]- Fabian Muehlboeck
When using generic types, a key restriction on type arguments is that they can only be reference types.
That is, one cannot have, say, a List<int>.
To cope with this, Java has a reference type called a wrapper class corresponding to each value type:
Integercorresponds tointCharactercorresponds tocharBooleancorresponds tobooleanBytecorresponds tobyteLongcorresponds tolongShortcorresponds toshortFloatcorresponds tofloatDoublecorresponds todouble
Any such wrapper class is treated specially, so you can assign to and from the wrapped type as if they were the same:
This is called autoboxing; putting an int value into an Integer is called boxing, and the reverse operation is called unboxing.
To have a list of integers, then, you'd use the type List<Integer>.
The reason we have the primitive value types at all is that they are much better for performance.
Creating an instance of Integer, on the other hand, requires us to allocate space on the heap, which is expensive.
Since these are objects, it is also important to note that == does not express equality on the wrapper classes.
You may different Integer objects representing the same integer value.
Warning.
- Fabian Muehlboeck
Java specifies that all integers (and Warning.
- Fabian Muehlboeck
short values) between -128 and 127 (inclusive) must have a unique corresponding Integer (and Short) value.
Similarly, this is true for true and false, and all byte and char values between 0 and 255.
This is good for performance, especially because those values are very common, but very insidious for your test cases:
if you happen to use == in your code for wrapper classes, tests with smaller numbers will not fail, even if you really needed to use .equals .
15.2. Hashing and Hash Tables
[comp1110-4I] - September 29, 2026
- Liam O'Connor, Fabian Muehlboeck
15.2. Hashing and Hash Tables
[comp1110-4I]- September 29, 2026
- Liam O'Connor, Fabian Muehlboeck
15.2.1. Can we do better than O(log n) for lookups?
[comp1110-4J] - Liam O'Connor
15.2.1. Can we do better than O(log n) for lookups?
[comp1110-4J]- Liam O'Connor
Binary Search Trees and sorted arrays give us \(\mathrm {O}(\log n)\) lookup, but this requires the data to be sorted, and sorting requires a total order on keys. Even when a total order exists, \(\mathrm {O}(\log n)\) starts to hurt for very large \(n\). Furthermore, for some key types, such as composite objects with no natural ordering, defining a total order may be awkward or arbitrary.
The goal is \(\mathrm {O}(1)\) (average-case) lookup, insertion, and deletion, regardless of whether the keys have a natural ordering. This is what hash tables deliver.
Example 15.2.2. Most common number in an array of bounded range
[comp1110-4K] - Liam O'Connor, Fabian Muehlboeck
Example 15.2.2. Most common number in an array of bounded range
[comp1110-4K]- Liam O'Connor, Fabian Muehlboeck
Suppose we want to find the most common number in an array of integers known to lie in the range 0 to 99 (inclusive). We can use the integer itself as a direct index into a counts array:
This is \(\mathrm {O}(n)\) time and \(\mathrm {O}(1)\) space (the array size is fixed). The key insight is that the integer is its own index. The challenge for hash tables is generalising this idea to arbitrary key types like strings.
Definition 15.2.3. Hash functions
[comp1110-4L] - Liam O'Connor
Definition 15.2.3. Hash functions
[comp1110-4L]- Liam O'Connor
A hash function is a computable function H mapping values of some type to integers. The only correctness condition is that H must respect equality: if a.equals(b) then H(a) == H(b). Note the converse need not hold: two unequal values may hash to the same integer.
Definition 15.2.4. Hash tables
[comp1110-4M] - Liam O'Connor
Definition 15.2.4. Hash tables
[comp1110-4M]- Liam O'Connor
A hash table is a data structure that uses a hash function to map keys to positions in a backing array of "buckets". If the hash function meets certain conditions, this provides \(\mathrm {O}(1)\) average-case lookup, insertion, and deletion. Given a hash function H and an array of capacity buckets, a key is stored at index: Math.abs(H(key)) % capacity.
Since there are infinitely many possible keys but only capacity buckets, collisions are unavoidable: two distinct keys will sometimes map to the same bucket. A collision resolution strategy determines what the table does when this happens.
Definition 15.2.5. Desiderata for hash functions used in hash tables
[comp1110-4N]
Definition 15.2.5. Desiderata for hash functions used in hash tables
[comp1110-4N]Beyond basic correctness conditions, a hash function intended for use in a hash table should also be:
- Deterministic: every call to \(H(a)\) returns the same value for the same input, across all executions. This rules out functions that depend on memory addresses, random numbers, or wall-clock time.
- Fast: \(H\) should run in \(\mathrm {O}(1)\) time with respect to the number of keys in the table. The cost of hashing a single key may grow with the size of that key (e.g., hashing a longer string takes more steps), but it should not grow as more keys are inserted.
- Well-distributed: hash codes should be spread roughly uniformly across the integer range, to minimise collisions. A function that always returns 0 is technically correct but useless in practice, as every key lands in the same bucket, and lookup degrades to \(\mathrm {O}(n)\).
Example 15.2.6. Hash functions for strings
[comp1110-4O] - Liam O'Connor, Fabian Muehlboeck
Example 15.2.6. Hash functions for strings
[comp1110-4O]- Liam O'Connor, Fabian Muehlboeck
This is what's called a polynomial rolling hash for strings:
At each step, the accumulated sum is multiplied by \(31\) before adding the next byte. This means the contribution of each byte is weighted by its position: the first byte is multiplied by \(31^{n-1}\), the last by \(31^0\). Using a prime multiplier (\(31\) is a small, odd prime) empirically produces good distribution and avoids systematic collisions on common string patterns. This is essentially the algorithm used by java.lang.String.hashCode(). Contrast with a bad hash function:
This satisfies the equality and determinism desiderata but violates distribution: every key collides.
Definition 15.2.7. Cryptographic hash functions
[comp1110-4P] - Liam O'Connor
Definition 15.2.7. Cryptographic hash functions
[comp1110-4P]- Liam O'Connor
A cryptographic hash function is a hash function with an additional defining property: preimage resistance. This means that computing \(H(x)\) is easy, but recovering \(x\) from \(H(x)\) is computationally infeasible. Well-known examples include SHA-1 and MD5.
Cryptographic hash functions are also designed to produce avalanche effects: a tiny change to the input scrambles the output completely, so that \(H(x)\) and \(H(x')\) look unrelated even if \(x\) and \(x'\) differ by a single bit. This makes them useful for verifying integrity, storing passwords, and similar security applications.
Warning 15.2.8. Cryptographic hash functions are not appropriate for hash tables
[comp1110-4Q] - Liam O'Connor
Warning 15.2.8. Cryptographic hash functions are not appropriate for hash tables
[comp1110-4Q]- Liam O'Connor
Cryptographic hash functions are correct hash functions, and they do satisfy our desiderata. However, they are a poor choice for a hash table for two reasons:
- They are much slower to compute than a simple purpose-built hash, for no benefit in this setting. The resistance to inversion that makes them cryptographically useful is irrelevant when you are building a lookup table.
- Avalanche and one-wayness do not guarantee good distribution for real-world key distributions. A purpose-built hash function can be tuned for the structure of likely inputs.
Construction 15.2.9.
Object.hashCode()
[comp1110-4R] - Liam O'Connor
Construction 15.2.9. Object.hashCode()
[comp1110-4R]- Liam O'Connor
Every Java object inherits hashCode() from java.lang.Object.
The default Object.hashCode() is typically derived from the object's memory address, which means two distinct objects with the same field values will hash differently. This makes it wrong for value-typed keys unless overridden. Any class you intend to use as a hash table key must override both equals and hashCode consistently.
Definition 15.2.10. Hash table collisions
[comp1110-4S] - Liam O'Connor
Definition 15.2.10. Hash table collisions
[comp1110-4S]- Liam O'Connor
A collision occurs when two distinct keys hash to the same position in a hash table's backing array. Since there are (usually) infinitely many possible keys but only finitely many positions, by the pidgeonhole principle collisions are unavoidable for any hash function. A good hash function minimises their frequency; a collision resolution strategy determines what the table does when they occur.
Definition 15.2.11. Collision resolution: chaining vs linear probing
[comp1110-4T] - Liam O'Connor
Definition 15.2.11. Collision resolution: chaining vs linear probing
[comp1110-4T]- Liam O'Connor
When two keys map to the same bucket in a hash table — a collision — a collision resolution strategy is needed. There are many approaches, but here are two:
- Chaining: each bucket holds a linked list (or other collection) of all entries that hash to that index. Lookup scans the list linearly. With a good hash function and a sufficiently large array, lists stay short and lookup is \(\mathrm {O}(1)\) amortised.
- Linear probing: the backing array stores entries directly (no linked lists). On a collision, the table scans forward (wrapping around) until it finds an empty slot. Lookup similarly scans forward until it finds the key or an empty slot. Linear probing has better cache performance than chaining but is sensitive to clustering: long runs of occupied slots that slow down both insertion and lookup. Once the hash table becomes too full, the array size is typically doubled and all elements re-hashed and copied to the new array, similarly to the trick used for ArrayList.
Java's HashMap uses chaining (with a tree fallback for very long chains). Linear probing is used in some high-performance open-addressing maps.
Example 15.2.12. Worked example: most common word with chaining
[comp1110-4U] - Liam O'Connor, Fabian Muehlboeck
Example 15.2.12. Worked example: most common word with chaining
[comp1110-4U]- Liam O'Connor, Fabian Muehlboeck
Building on Example 15.2.2, here is a hand-rolled hash table using chaining to find the most common word in a string array. Each bucket is a LinkedList<Entry>, where Entry holds a word and its current count.
Definition 15.2.13. HashMap and HashSet in Java
[comp1110-4V] - Liam O'Connor
Definition 15.2.13. HashMap and HashSet in Java
[comp1110-4V]- Liam O'Connor
java.util.HashMap<K, V> is the standard library's general-purpose hash table. It implements the Map<K, V> interface from Java collections with \(\mathrm {O}(1)\) amortised put, get, and containsKey, handles resizing automatically, and uses chaining internally.
java.util.HashSet<E> is a hash table storing only keys (no associated values), implementing Set<E>. It is backed by a HashMap internally.
Both require that keys correctly implement equals and hashCode.
Example 15.2.14. Worked example: most common word with built-in HashMap
[comp1110-8Q] - Fabian Muehlboeck
Example 15.2.14. Worked example: most common word with built-in HashMap
[comp1110-8Q]- Fabian Muehlboeck
Usually, instead of creating a hash table implementation yourself as we did in Worked example: most common word with chaining, it is better to rely on standard library classes for such functionality:
Warning 15.2.15. Mutable keys are an extremely bad idea
[comp1110-4W] - Liam O'Connor
Warning 15.2.15. Mutable keys are an extremely bad idea
[comp1110-4W]- Liam O'Connor
In a hash table, a key's bucket index is computed from its hash code at insertion time. If you mutate a key after inserting it into a hash table, its hash code may change, placing it in a different bucket — but the table still stores it in the original bucket! The entry becomes permanently lost: containsKey returns false, get returns null, and no error is raised.
Use only immutable objects as hash table keys. Strings, boxed primitives, and classes that only have final fields are safe. Mutable objects are not.
Lecture 16. Week 10 Test Preparation
[comp1110-2026s2-lec08b] - October 1, 2026
- Fabian Muehlboeck
Lecture 16. Week 10 Test Preparation
[comp1110-2026s2-lec08b]- October 1, 2026
- Fabian Muehlboeck
Warning.
- October 1, 2026
- Fabian Muehlboeck
This was a revision lecture in preparation for the week 10 test. No new content for the course will appear here.Warning.
- October 1, 2026
- Fabian Muehlboeck