COMP2511
Tutorial 4
Today
- Assignment tips
- Design by Contract
- Design Principles
- Streams and lambdas
Assignment Tips
Material from Alvin Cherk
Assignment Tips
Use .equal() instead of == for comparison between objects
public class EqualsExample {
public static void main(String[] args) {
String s1 = "Hello"; // string literal
String s2 = new String("Hello"); // string class
System.out.println(s1 == s2); // false, as its comparing the addresses of s1 and s2
System.out.println(s1.equals(s2)); // true, compares the values at the addresses
// ^ do this
}
}Correctness + style issue
Assignment Tips
Avoid magic numbers
Assign them to static final variables (readability - style issue)
public class MagicNumbers {
public void doSomething() {
if (files.size() == 5) {} // what does 5 mean here?
}
}
public class MagicNumbers {
private final static int MAX_FILES = 5;
// declare constant with good name ^
public void doSomething() {
if (files.size() == MagicNumbers.MAX_FILES) {};
// ^ use constant here
}
}
Assignment Tips
Use instanceof for type comparison (open/close - design issue)
public abstract class Shape {
public abstract String getType();
}
public class Rectangle extends Shape {
public void doSomething(List<Shape> shapes) {
for (Shape s : shapes) {
if (s.getClass().equals(Rectangle.class)) { // v1: kinda ok
// do something only on exactly the Rectangle class
}
if (s.getType().equals("Rectangle")) { // v2: very bad
// do something on all rectangles
}
if (s instanceof Rectangle) { // v3: best choice here
// do something on all rectangles
}
}
}
}
public class Square extends Rectangle { ... }Assignment Tips
Polymorphism is preferred over type-checking when performing a specific action (open closed principle - design issue)
// Example: What to do
public abstract class Shape {
public abstract String getType();
public abstract double area();
// ^ declare method in superclass
}
public class Rectangle extends Shape {
public double area() {
// calculate the area this way
}
}
public class Square extends Rectangle {
public double area() {
// calculate the area a different way
}
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle());
shapes.add(new Square());
for (Shape s : shapes) {
s.area(); // no more type checking
}
}
}Try to achieve this, but we know it's not always possible
Design By Contract
Design By Contract
What is it?
A contract must specifiy the following 3 conditions:
- Pre-condition - what inputs does it expect?
- Post-condition - what outputs does it guarantee?
- Invariant - what does not change?
Software design approach that specifies how a software component should interact with other software components
Design By Contract
What does it really mean?
If someone attempts to use my system without following my contracts, then the system doesn't need to ensure correctness.
i.e. system is allowed to crash
- Unlike COMP1511, invalid input no longer needs to be checked if specified in the contracts
- You can safely assume labs and assignments will not test for invalid input (even if you don't see any contracts in the code)
Implications for this course
Design By Contract
public class Calculator {
public static Double add(Double a, Double b) {
return a + b;
}
public static Double divide(Double a, Double b) {
return a / b;
}
public static Double sin(Double angle) {
return Math.sin(angle);
}
public static Double tan(Double angle) {
return Math.tan(angle);
}
}Exercise: specify a contract for every method in the Calculator class
Design By Contract
/**
* @preconditions: angle != null
* @postconditions: sin(angle)
* @invariant: a, b will remain unchanged
*/
public static Double sin(Double angle) {
return Math.sin(angle);
}
/**
* @preconditions: angle != null, angle != Math.PI / 2
* @postconditions: tan(angle)
* @invariant: a, b will remain unchanged
*/
public static Double tan(Double angle) {
return Math.tan(angle);
}/**
* @preconditions: a, b != null
* @postconditions: a + b
* @invariant: a, b will remain unchanged
*/
public static Double add(Double a, Double b) {
return a + b;
}/**
* @preconditions: a, b != null, b != 0
* @postconditions: a / b
* @invariant: a, b will remain unchanged
*/
public static Double divide(Double a, Double b) {
return a / b;
}add
divide
sin
tan
Law of Demeter
"Principle of least knowledge"
Law of Demeter
What is it?
Design principle that states an object should have minimal knowledge about other objects

It aims to achieve loose coupling
Purpose
- In a loosely coupled system, objects are less dependent on the internals of other objects (fewer contact points)
- Implementation of an object can change without needing to modify the caller
Law of Demeter
How to uphold the Law of Demeter?
A method m of an object o should only call methods from:
- The object
oitself - Objects that are member variables of
o - Objects passed in as parameters to
m - Objects instantiated within m
a.getB().getC()*Sometimes it is unavoidable
Avoid situations like below*

Code Review
Law of Demeter
Code Review
In the unsw.training package there is some code for a training system
- Every employee must attend a whole day training seminar run by a qualified trainer
- Each trainer is running multiple seminars with no more than 10 attendees per seminar
The bookTraining method in the TrainingSystem class violates the Law of Demeter. How?
Code Review
-
How does the
bookTrainingmethod violate the Law
of Demeter?-
TheSeminarclass is a "stranger" of theTrainingSystemclass -
getStart()is a method of theSeminarclass and it is invoked
within theTrainingSystemclass
-
-
What are some other undesirable properties about the
overall system?- The
TrainingSystemhas low cohesion as it depends on
mostly external classes - The
Seminarclass is poorly encapsulated. It relies on theTrainingSystemclass to enforce its contract (i.e. capacity)
- The
/**
* An in person all day seminar with a maximum of 10 attendees.
*/
public class Seminar {
private LocalDate start;
private List<String> attendees;
public LocalDate getStart() {
return start;
}
/**
* Try to book this seminar if it occurs on one of the available days and
* isn't already full
* @param employee
* @param availability
* @return The date of the seminar if booking was successful, null otherwise
*/
public LocalDate book(String employee, List<LocalDate> availability) {
for (LocalDate available : availability) {
if (start.equals(available) &&
attendees.size() < 10) {
attendees.add(employee);
return available;
}
}
return null;
}
}
public class TrainingSystem {
public List<Trainer> trainers;
/**
* Try to booking training for an employee, given their availability.
*
* @param employee
* @param availability
* @return The date of their seminar if booking was successful, null there
* are no empty slots in seminars on the day they are available.
*/
public LocalDate bookTraining(String employee, List<LocalDate> availability) {
for (Trainer trainer : trainers) {
LocalDate booked = trainer.book(employee, availability);
if (booked != null)
return booked;
}
return null;
}
}
/**
* A trainer that runs in person seminars.
*/
public class Trainer {
private String name;
private String room;
private List<Seminar> seminars;
public List<Seminar> getSeminars() {
return seminars;
}
/**
* Try to book one of this trainer's seminars.
* @param employee
* @param availability
* @return The date of the seminar if booking was successful, null if the
* trainer has no free slots in seminars on the available days.
*/
public LocalDate book(String employee, List<LocalDate> availability) {
for (Seminar seminar : seminars) {
LocalDate booked = seminar.book(employee, availability);
if (booked != null)
return booked;
}
return null;
}
}Liskov Substitution Principle
Liskov Substitution Principle
LSP is violated when either
- Preconditions are strengthened in the subclass
- Postconditions are weakened in the subclass
- Invariants are not held in the subclass
What is it?
Design principle that states objects of a superclass should be replaceable with objects of its subclasses
Formal definition
Let Φ(x) be a provable property of objects x of class T. Then, Φ(y) should be true for objects y of class S where S is a subclass of T
Liskov Substitution Principle
Common signs of potential LSP violations
- Subclass throwing an exception in methods inherited from the superclass
- Subclass leaving methods inherited from the superclass blank (unimplemented)
- When the inheritance hierarchy doesn't appear to make sense

Liskov Substitution Principle
Solve the problem by minimising / removing inheritance
- Delegation - delegate the functionality to another class
- Composition - reuse behaviour using one or more classes with composition
Design suggestion: favour composition over inheritance
Violations of LSP are typically caused by inheritance
- Inheritance achieves the goal of code re-use
- But potential subclasses are now locked in even if some properties and functions of the superclass don't make sense
Liskov Substitution Principle
/**
* An online seminar is a video that can be viewed at any time by employees. A
* record is kept of which employees have watched the seminar.
*/
public class OnlineSeminar extends Seminar {
private String videoURL;
private List<String> watched;
}
/**
* Precondition: each seminar can have a maximum of 10 attendees.
* Postcondition: an attendee is added to the seminar.
*/
public class Seminar {
private LocalDate start;
private List<String> attendees;
public LocalDate getStart() {
return start;
}
public List<String> getAttendees() {
return attendees;
}
}
How does OnlineSeminar violate LSP?
Suppose the capacity precondition is currently true, adding a new attendee to an OnlineSeminar will not satisfy the postcondition because presumably the attendee is added to a watched list as opposed to the attendees list from the superclass
Streams and Lambdas
Streams and Lambdas
What it is?
Java's way of enabling functional programming
Common stream methods:
forEach, filter, map, reduce
Why bother?
- Eliminate common bugs (i.e. using
forEachmakes index out of bounds impossible) - Often lead to more compact and readable code
Aside: Optional<type>
If a variable can be null, use Optional<>.
Try to never set variables to be null because dealing with NullPointerExceptions is never fun :(
public class OptionalExample {
public static void main(String[] args) {
List<String> strings = new ArrayList<String>(Arrays.asList(new String[] { "1", "2", "3", "4", "5" }));
Optional<String> res = strings.stream().filter(x -> x.equals("1")).findAny();
if (res.isPresent()) {
System.out.println(res.get());
} else {
// It doesn't exist
// Handle error?
}
}
}
.findAny() actually returns Optional<type>
Material from Alvin Cherk
Code Demo
Streams
Code Demo
Convert the following to use streams
package stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class App {
public static void main(String[] args) {
// Exercise 1
List<String> strings = new ArrayList<String>(Arrays.asList(new String[] { "1", "2", "3", "4", "5" }));
for (String string : strings) {
System.out.println(string);
}
// Exercise 2
List<Integer> ints = new ArrayList<Integer>();
for (String string : strings) {
ints.add(Integer.parseInt(string));
}
System.out.println(ints);
}
}package stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class App {
public static void main(String[] args) {
// Exercise 1 Solution
List<String> strings = new ArrayList<String>(Arrays.asList(new String[] { "1", "2", "3", "4", "5" }));
strings.stream().forEach(x -> System.out.println(x));
strings.stream().forEach(x -> {
System.out.println(x);
});
// Exercise 2 Solution
List<Integer> parsedStrings = strings.stream().map(x -> Integer.parseInt(x)).collect(Collectors.toList());
strings2.stream().map(x -> Integer.parseInt(x)).forEach(x -> System.out.println(x));
}
}Code Demo
package stream;
public abstract class GenericSatellite {
private double linearSpeed; // in KM/minute
private String name; // name of satellite
public GenericSatellite(double linearSpeed, String name) {
this.linearSpeed = linearSpeed;
this.name = name;
}
public String getName() {
return this.name;
}
public double getLinearSpeed() {
return this.linearSpeed;
}
}
package stream;
public class FastSatellite extends GenericSatellite {
private static final double speed = 100.0;
public FastSatellite(String name) {
super(speed, name);
}
}
package stream;
public class SlowSatellite extends GenericSatellite {
private static final double speed = 50.0;
public SlowSatellite(String name) {
super(SlowSatellite.speed, name);
}
}
package stream;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Controller {
private List<GenericSatellite> satellites = new ArrayList<>();
public void addSatellite(GenericSatellite satelliteToAdd) {
this.satellites.add(satelliteToAdd);
}
public List<GenericSatellite> getSatelliteList() {
return this.satellites;
}
public static void main(String[] args) { ... }
}Further Streams Example
public static void main(String[] args) {
Controller c = new Controller();
c.addSatellite(new FastSatellite("Fast Satellite 1"));
c.addSatellite(new FastSatellite("Fast Satellite 2"));
c.addSatellite(new SlowSatellite("Slow Satellite 1"));
List<GenericSatellite> allSatellites = c.getSatelliteList();
// What if I just want FastSatellites only?
// Method 1, normal for-in/for-each
List<GenericSatellite> fast1 = new ArrayList<>();
for (GenericSatellite x : allSatellites) {
if (x instanceof FastSatellite) {
fast1.add(x);
}
}
// Method 2, streams
List<GenericSatellite> fast2 = allSatellites
.stream()
.filter(x -> x instanceof FastSatellite)
.collect(Collectors.toList());
// Same, but I typecast at the same time
List<FastSatellite> fast3 = allSatellites
.stream()
.filter(x -> x instanceof FastSatellite)
.map(x -> (FastSatellite) x)
.collect(Collectors.toList());
}Further Streams Example
public static void main(String[] args) {
Controller c = new Controller();
List<GenericSatellite> allSatellites = c.getSatelliteList();
// What if im trying to look for a satellite with name == "Fast Satellite 2"?
// Method 1, normal for-in/for-each
GenericSatellite g1 = null;
for (GenericSatellite x : allSatellites) {
if (x.getName().equals("Fast Satellite 2")) {
g1 = x;
break;
}
}
// Method 2, streams
Optional<GenericSatellite> g2 = allSatellites
.stream()
.filter(x -> x.getName().equals("Fast Satellite 2"))
.findAny();
}Further Streams Example
The End
Reminder to talk about code smell
COMP2511 Tutorial 4
By Matthew Liu
COMP2511 Tutorial 4
- 172