Lecture. Testing, Abstraction
[comp1110-2026s2-lec07a] - September 22, 2026
- Fabian Muehlboeck
Lecture. Testing, Abstraction
[comp1110-2026s2-lec07a]- September 22, 2026
- Fabian Muehlboeck
Exercise 1. Box inheritance
[comp1110-5X] - Liam O'Connor, Fabian Muehlboeck
Exercise 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.
2. Common modifier keywords
[comp1110-8H] - Fabian Muehlboeck
2. Common modifier keywords
[comp1110-8H]- Fabian Muehlboeck
Construction 2.1. Static members
[comp1110-8I] - Fabian Muehlboeck
Construction 2.1. Static members
[comp1110-8I]- Fabian Muehlboeck
Construction 2.1.1. Static methods
[comp1110-4F] - Liam O'Connor, Fabian Muehlboeck
Construction 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 2.1.2. Static fields
[comp1110-8J] - Fabian Muehlboeck
Construction 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 2.1.3. Static nested classes
[comp1110-8K] - Fabian Muehlboeck
Construction 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 2.2. Final classes and methods
[comp1110-5S] - Liam O'Connor
Construction 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 2.3. Final fields and variables
[comp1110-8L] - Fabian Muehlboeck
Construction 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 2.4. Abstract classes and methods
[comp1110-5N] - Liam O'Connor, Fabian Muehlboeck
Construction 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.
3. Designing and Testing an Interface
[comp1110-8G] - Fabian Muehlboeck
3. Designing and Testing an Interface
[comp1110-8G]- Fabian Muehlboeck
Example 3.1. The GuestList interface (String version)
[comp1110-3X] - Liam O'Connor, Fabian Muehlboeck
Example 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 3.2. Unit testing with JUnit
[comp1110-40]
Definition 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 3.3. Test-Driven Development
[comp1110-41] - Liam O'Connor
Definition 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 3.4. Black-box and white-box testing
[comp1110-3Z]
Definition 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 3.5. JUnit tests for GuestList
[comp1110-3Y] - Liam O'Connor
Example 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 3.6. Lazy array implementation of GuestList
[comp1110-42] - Liam O'Connor, Fabian Muehlboeck
Example 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 3.7).
Definition 3.7. Amortised complexity and array doubling
[comp1110-43] - Liam O'Connor, Fabian Muehlboeck
Definition 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 3.8. Strict array implementation of GuestList
[comp1110-45] - Liam O'Connor, Fabian Muehlboeck
Example 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 3.9. Encapsulation and defensive copying
[comp1110-47] - Liam O'Connor
Warning 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 3.10. Encapsulation danger when returning internal arrays
[comp1110-46] - Liam O'Connor, Fabian Muehlboeck
Example 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: