SFU CMPT 213: Object-Oriented Programming with Java

Software Design Process

Software Development Activities: specification, design & implementation, verification, evolution.

Object-Oriented Design (OOD) Process: an iterative process of discovery and refinement.

OOD works: classes represent stable problem domain concepts.

Heuristic Process: trial of errors, analysis, and refinement.

Skeleton Code: implement minimal parts or features of full system first.

Component Wise: implement one class or component at a time.


Class Design

Object: an entity with state, behaviours to operate on the state with unique identity.

Class: an object’s blueprint.

Dependency: a class uses another class.

Aggregation: a class has-a object of another class (multiplicities).

Inheritance: a class is-a subcategory of another class. (prefer composition over inheritance)

Coupling: two classes are coupled if one depends on the other. The less coupling, the better for better flexibility.

Wicked Problem: need good design to implement the system but need the system to know if you have a good design. (Contradiction)

Sloppy Design: make mistakes and adapt.

Heuristic Process: uses rules of thumb, trial and errors, analysis and refinement.

Continual Integration: a gradual growth of the system by continually integration changes.

Big Bang Integration: assemble the software at once.

Primitive Data Type: pass by value (copy)

Object Data Type: pass by reference (pointer)

Static Factory Method: a static method that creates and return a new object.

Immutable: when an object is created, it cannot be changed.

Anonymous Class: an unnamed class, uses for custom sorting, filtering files, and event callbacks.

Anonymous Object: an unnamed object.

Strategy Pattern: encapsulate an algorithm into a class by using anonymous classes & objects.

Comparable: natural sorting order, class needs to implement Comparable<T> and override compareTo(T other).

Comparator: customer sorting order, need to instantiate an anonymous class and override compare(T objA, T objB).


Interface Quality

Interface / Code Quality (4Cs)

1.) Cohesion (Single Responsibility per Class)

2.) Completeness or Convenience

3.) Clarity or Conciseness (a function should return an appropriate data type)

4.) Consistency (parameters naming)

Other Code Quality

5.) Constructor creates a fully formed object.

6.) Command-Query separation.

7.) Implement Iterable or Comparable.

8.) Breaking encapsulation.

Precondition: assumption at the start of the function.

Ex: when calculating a square root, the input must be non-negative integer.

Postcondition: conditions at the end of the function.

Ex: when calling sort function, the order of the items must be either ascending or descending order.

Invariants: conditions or porperties remain unchanged throughout the execution of the function.

Ex: in Queue data structure, the size of queue must not be negative.


Contract Programming vs Defensive Programming

Defensive Programming:

  • A class is responsible for maintaining a correct state.
  • All inputs values and actions are checked for correctness.
  • Pro: invalid or errors are caught early.

Programming by Contract:

  • Each method and class has a contract that the client agrees to meet and perform an action.
  • Pro: removes redundant validity checks.

Using Design by Contract and Defensive Programming:

Use Design by Contract to communicate the expectations, and use Defensive Programming to verify these expectations using asserts and exceptions .

Assertions

  • Triggers a runtime error, if the condition is false.
  • Use assertion to catch unanticipated cases.
  • Don’t use assertion that will already cause runtime errors.
  • Enabling assertions in Java by passing: -ea to Java Virtual Machine (JVM).


Interface Polymorphism

Polymorphism: late binding (runtime compilation) and provides loose coupling.

Ad-hoc Polymorphism: static binding by method overloading of specific type.

Parametric Polymorphism: static binding by using generic method that works for many data type.

Subtype Polymorphism: late binding (runtime) using inheritance or interface via method overriding.

Design Heuristic: code to an interface, not a concrete type.

@Override: tells the compiler to check if the overriding methods exist in the superclass.

Promotion: casting lower-size data type to higher data type (int to double).

Demotion: casting higher-size data type to lower data type (double to int).

Java Garbage Collection: primitive data type stored in stack, objects are stored in heap.

Parsing String to Int: Integer.parseInt(userInput)

Operator Overloading: Java does not support operator overloading.

String: immutable, always create a new object when concatenating.


Comparable Example

public class Student implements Comparable<Student> {
    private int id;
    private String name;
    
    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public int getID() {
        return this.id;
    }
    
    public String getName() {
        return this.name;
    }
    
    /**
     * Natural ordering
     *
     * @param otherStudent
     * @return
     */
    public int compareTo(Student otherStudent) {
        // Ascending order
        // this.getName().compareTo(otherStudent.getName());
             
        // Descending order
        return otherStudent.getName().compareTo(this.name);
    }
     
    @Override
    public String toString() {
        return "ID: " + this.id + " NAME: " + this.name;
    }
}

public static void main(String[] args) {
    Student studentA = new Student(1, "Student A");
    Student studentB = new Student(2, "Student B");
    Student studentC = new Student(3, "Student C");
    
    List<Student> students = new ArrayList<Student>();
    students.add(studentA);
    students.add(studentB);
    students.add(studentC);
    
    Collections.sort(students);
}

Comparator Example

public class Robot {
    private int id;
    private String name;
    
    public Robot(int id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public int getID() {
        return this.id;
    }
    
    public String getName() {
        return this.name;
    }
     
    @Override
    public String toString() {
        return "ID: " + this.id + " NAME: " + this.name;
    }
}

public static void main(String[] args) {
    Robot robotA = new Student(1, "Robot A");
    Robot robotB = new Student(2, "Robot B");
    Robot robotC = new Student(3, "Robot C");
    
    List<Robot> robots = new ArrayList<Robot>();
    robots.add(robotA);
    robots.add(robotB);
    robots.add(robotC);
    
    Collections.sort(robots, (Robot robotA, Robot robotB) -> { 
        return robotA.getID() - robotB.getID();
    });
}

Iterable and Iterator

public class Student {
    private String name;

    public Student (String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    @Override
    public String toString() {
        return "Student: " + this.name;
    }
}

public class StudentsList implements Iterable<Student> {
    private List<Student> students;

    public StudentList() {
        this.students = new ArrayList<>();
    }
    
    public void addStudent(Student newStudent) {
        this.students.add(newStudent);
    }
    
    @Override
    public Iterator<Student> iterator() {
        return this.students.iterator();
    }
}

public static void main(String[] args) {
    StudentsList studentsList = new StudentsList();
    studentsList.addStudent(new Student("Student A");
    studentsList.addStudent(new Student("Student B");
    studentsList.addStudent(new Student("Student C");
    
    // Using iterable
    for (Student student : studentsList) {
        System.out.println(student);
    }
}



Inheritance

super refers to the superclass (automatically added if missing in the constructor).

this refers to the current object.

A method cannot be both abstract and final .

abstract methods are to be overridden while final methods cannot be overridden.

A method cannot be both abstract and static.

abstract methods are to be overridden by subclasses while static methods belong to the class and cannot be overridden by the subclasses.

Cannot override private, static, or final methods.

Shadow Variables: a subclass declares variables with the same name as the superclass. (do not do this).

Design Principle

Program to an interface, not implementation: allowing flexibility to reference a different concrete class later.

Prefer composition over inheritance: allowing referencing a new object, reduce rigid coupling from static inheritance hierarchy.


REST API

HTTP: hypertext Transfer Protocol.

HTTP Methods: GET, POST, PUT, DELETE.

API: application programming interface.

REST: representational state transfer.

TLA: three letter acronym.

CRUD: create, read, update, delete.


Springboot

Springboot Framework: an dependency injection framework.

POJO: plain old java object.

Dependency Injection: allow loosely coupled classes, easy to mock object using POJO, pass objects as reference, no need to instantiate the object.


Path Endpoint Example

@GetMapping("/quotes/{id}")
public Quote getQuote(@PathVariable("id") long id) {
    return QuoteRepository.find(id);
}

Query Endpoint (?key={someValue}) Example

@GetMapping("/quotes/")
public Quote search(@RequestParam(value="search", defaultValue="") String searach) {
    return QuoteRepository.search(search);
}

Body Endpoint (POST/PUT) Example

@PostMapping("/name")
public ResponseEntity<ProductDTO> updateProduct(@RequestBody ProductDTO product) {
    ProductDTO updatedProduct = this.productService.updateProduct(productDTO);
    return new ResponseEntity<>(updatedProduct, HttpStatus.OK);
}



Inheritance Design

  • Each class manages its own state
  • Use super in constructor and any overridden methods.
  • Avoid protected, and use private except for protected interface for the derived classes.
  • Code reuse does not justify inheritance.
  • Inheritance represents a subset relationship with superclass.

Liskov Substitution Principle (LSP)

  • Subtypes must be substitutable for their base types.
  • B can inherit A iff for each method in A, B's method accepts all parameters, and does everything with those parameters.

IS-A Limitation

  • A subclass cannot extend or refine the behaviour of its superclass that breaks LSP.
  • For example, if Car is a subtype of Vehicle, then any code that expects Vehicle should be able to work seamlessly with an instance of Car.
  • This ensures subclasses do not change the contract of their superclass.

5 OOD Principles

SOLID:

S: Single Responsibility Principle (A class has one responsibility)

O: Open Closed Principle (Be open for extension, closed for modification)

L: Liskov Substitution Principle (Subtype objects interchangeable with base objects)

I: Interface Segregation Principle (Prefer many client-specific interfaces)

D: Dependency Inversion Principle (Depend on abstractions, not concrete classes)

Self Use: base class calls a method which can be overridden by derived class.

Problem: when the base class calls its own methods which can be overridden.

Solution: use a private helper function that cannot be overridden.

Better Inheritance: use Polymorphism

Implementation Inheritance: inheriting from a concrete class to reuse its code.

Interface Inheritance: implementing an interface to support polymorphism.

Inheritance Guideline/Plan:

  • Use composition to depend on interfaces.
  • Use small classes to implement interfaces.
  • Compose objects at runtime.
  • Use/build small objects.


Design Pattern

Iterator: no change, does not expose implementation details.

Software Design Pattern: allows discussion, implementation, and reuse of proven software designs.

MVC: separates model from view.

Facade Pattern: introduces a new class model to decouple UI from model complexity, and decouple UI from model complexity.

Observer Pattern

  • allows objects to register for updates with another object at runtime.
  • produces one-to-many relationship.
  • subject is the object being observed.
  • observers many object observing the subject.

Observer Example

// Subject: WeatherStation class
public interface WeatherStation {
    void addObserver(WeatherObserver observer);
    void removeObserver(WeatherObserver observer);
    void notifyObservers();
}

public class WeatherStationImplementation implements WeatherStation {
    private List<WeatherObserver> observers = new ArrayList<>();
    private int temperature;
    private int humidity;
    
    public void addObserver(WeatherObserver observer) {
        observers.add(observer);
    }
    
    public void removeObserver(WeatherObserver observer) {
        observers.remove(observer);
    }
    
    public void notifyObservers() {
        for (WeatherObserver observer : observers) {
            observer.update(this.temperature, this.humidity);
        }
    }
    
    public void setMeasurements(int temperature, int humidity) {
        this.temperature = temperature;
        this.humidity = humidity;
        
        notifyObservers();
    }
}

// Observer: WeatherObserver interface
public interface WeatherObserver {
    void update(int temperature, int humidity);
}

// Concrete observer: LCDDisplay
public class LCDDisplay implements WeatherObserver {
    @Override
    public void update(int temperature, int humidity) {
        System.out.println("LCD Display: Temperature " + temperature + ", Humidity " + humidity);
    }
}

// Concrete observer: SMSAlert
public class SMSAlert implements WeatherObserver {
    private String phoneNumber;
    
    public SMSAlert(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }
    
    public void update(int temperature, int humidity) {
        if (temperature > 30 || humidity > 80) {
            sendSMS(phoneNumber);
        }
    }
    
    private void sendSMS(String phoneNumber) {
        // Send SMS message.
    }
}

public static void (String[] args) {
    WeatherStation weatherStation = new WeatherStationImplementation();
    
    LCDDisplay display = new LCDDisplay();
    SMSAlert smsAlert = new SMSAlert("123456789");
    
    weatherStation.addObserver(display);
    weatherStation.addObserver(smsAlert);
}

Facade Design Pattern Example

// Concrete classes.
class Crust {
    void makeCheeseCrust() { ... }
}

class Sauce {
    void addTomatoSauce() { ... }
}

class Cheese {
    void addMozarella() { ... }
}

class Toppings {
    void addPepperoni() { ... }
    void addMushrooms() { ... }
}

// Facade
public class PizzaOrderFacade {
    private Crust crust;
    private Sauce sauce;
    private Cheese cheese;
    private Toppings toppings;
        
    public PizzaOrderFacade(Crust crust, Sauce sauce, Cheese cheese, Toppings toppings) {
        this.crust = crust;
        this.sauce = sauce;
        this.cheese = cheese;
        this.toppings = toppings;
    }
        
    public void orderPizza(String pizzaType) {
        System.out.println("Ordering: " + pizzaType + " pizza.");
        crust.makeCheeseCrust();
        sauce.addTomatoSauce();
        cheese.addMozzarella();
            
        if (pizzaType.equals("Pepperoni")) {
            toppings.addPepperoni();
        } else if (pizzaType.equals("Veggie")) {
            toppings.addMushrooms();
        }
    }
}

public static void main(String[] args) {
    Crust crust = new Crust();
    Sauce sauce = new Sauce();
    Cheese cheese = new Cheese();
    Toppings toppings = new Toppings();
        
    PizzaOrderFacade pizzaOrder = new PizzaOrderFacade(crust, sauce, cheese, toppings);
    pizzaOrder.orderPizza("Pepperoni");
}


Exception

private void methodA() {
    try {
        ...
    }
    catch (TypeA exception) {
        ...
    }
    catch (TypeB exception) {
        ...
    }
    finally {
        ...
    }
}

If no exception is thrown, finally is called after try.

If an exception is caught, finally is called after the catch.

If an exception is uncaught

  • finally is called immediately
  • the exception is propagated up until a method catches the exception.
  • If main() does not handle the exception, the program is terminated.

Exception handling is a design decision.

Example: throw an exception in database to propagate up to UI to catch and display the error.

All exceptions inherit from Exception class.

Possible methods: getMessage(), printStackTrace()


2 Type of Exceptions


Checked Exception: must be either caught by the method or listed in methods throws exception.

// Checked exception: FileNotFoundException
private void actionA() throws FileNotFoundException {
    ...
}
// Checked exception: IOException
try {
    FileReader fileReader = new FileReader("nonexistent.txt");
} catch (IOException e) {
    // Handle exception.
}

Unchecked Exception: does not need to be caught or listed, RuntimeException is unchecked.

// Unchecked exception: NullPointerException
String name = null;
System.out.println(name.length()); // throw NullPointerException

// Unchecked exception: ArithmeticException
int result = 10 / 0; // throw ArithmeticException, cannot divide a number by 0.
public class DemoCheckedExceptions {
    public top() throws FileNotFoundException {
        foo1();
    }

    void foo1() throws FileNotFoundException {
        foo2();
    }

    void foo2() throws FileNotFoundException {
        throw new FileNotFoundException()
    }
}
  • Prefer Unchecked Exception as Checked Exception is required to change for all methods throwing and catching.

Custom Exceptions

public class NoFileSelected extends RuntimeException {
    public NoFileSelected() {
        super();
    }
    
    public NoFileSelected(String message) {
        super(message);
    }
}

try-with-resources to automatically close files/scanners.

try (Scanner scanner = new Scanner(file)) {
    if (scanner.hasNextInt()) {
        int number = scanner.nextInt();
        ...
    }
}



Code Smell

DRY: don’t repeat yourself, extract method, extract constant and pull-up to base class.

Template Method Design Pattern

  • Use inheritance (at compile time) to behave like template.
  • The base class has algorithm and calls abstract methods where derived classes override the abstract methods.
  • Good design choice if inheritance hierarchy already exists.

Strategy Pattern

  • Composition (at runtime), flexible for different algorithms during execution.
  • Advocate simpler client code, no inheritance, and allow the use of lambda functions.

Template Method Example

abstract class IntFileProcessor {
    private int processFile(File file) {
        try (FileReader reader = new FileReader(file)) {
            Scanner scanner = new Scanner(reader);
            
            int result = this.getStartValue();
            while (scanner.hasNextInt()) {
                    result = this.processInt(result, scanner.nextInt());
            }
            
            return result;
        } catch (IOException exception) {
            execption.printStackTrace();
        }
        
        return 0;
    }
    
    abstract protected int getStartValue();
    abstract protected int processInt(int current, int next);
}

class IntFileProcessorSum extends IntFileProcessor {
    @Override
    protected int getStartValue() {
        return 0;
    }
    
    @Override
    protected int processInt(int current, int next) {
        return current + next;
    }
}

class IntFileProcessorProduct extends IntFileProcessor {
    @Override
    protected int getStartValue() {
        return 1;
    }
    
    @Override
    protected int processInt(int current, int next) {
        return current * next;
    }
}



Object & Hash Code

Object Equality: == compares two objects’ addresses.


Overriding equals(Object o)

class Car {
    private String make;
    private Date year;
    private int seating;
    private double weight;
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Car) return false;
        
        Car that = (Car) o;
        
        return Objects.equals(this.make, that.make)
            && Objects.equal(this.year, that.year)
            && this.seating == that.seating
            && Double.compare(this.weight, that.weight) == 0;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(this.make, this.year, this.seating, this.weight);
    }
}

equals() Contract

  • For non-null x and y equals must be:
  • Reflexive: x.equals(x) must be true.
  • Symmetric: x.equals(y) iff y.equals(x).
  • Transitive: x.equals(y) && y.equals(z) -> x.equals(z) .
  • Consistent: value of x.equals(y) remains unchanged for any invocations.
  • Not-Equal Null: x.equals(null) must be false.

HashCode: Objects.hash() must pass all fields that stores object’s states.

When overriding equals() you must override hashCode() whenever objectA.equal(objectB) then objectA.hashCode() == objectB.hashCode(); otherwise, collections may not work.


Threads

public interface Runnable {
    void run();
}

class SomeTask implements Runnable {
    @Override
    public void run() {
        ... // do something.
    }
}

public void main(String[] args) {
    Runnable taskA = new SomeTask();
    Thread threadA = new Thread(taskA);
    threadA.start();
}

1.) Create a Task that implements Runnable.

2.) Create a Thread that passes the Task to it.


Timing

Time Slice: a block of time during which a Thread can execute, OS/JVM allocates time-slices to threads.

Not always equals:

  • Starvation: a task is given but no time to execute.
  • Fairness: often use round-robin (RR) scheduling.
    • round-robin: assigning tasks/jobs in a cyclical manner.
  • Priority: some threads have higher priority than others.

Suspending a Thread: Thread.sleep(delay)


Thread Synchronization

Race Condition: happens when multiple threads depends on shared resources and the order of threads are scheduled.

Cause/Issue:

  • The execution of a thread is interrupted by another thread.
  • The second thread corrupts operation of the first thread.

Solution:

  • Use thread safe to prevent race condition.
  • Use locks.

Using Locks

1.) Create a lock to access some resources.

2.) Lock the lock before accessing resources.

3.) Use resource.

4.) Unlock the lock when is done.

Deadlock: each thread is waiting for another one to release its lock, dining philosophers problem.

Dining philosophers problem: 2 chopsticks to eat but each philosopher sits side by side with each other.

Critical Section: a portion of thread’s execution where it can suffer a race condition.

Threads Example

Heisenbug: a bug whose behaviour is changed by looking for it, debugging can change thread timing resulting in changing behaviour.


Stopping a Thread : normally threads end once run() finished.

Runnable taskA = new TaskA();
Thread threadA = new Thread(taskA);
threadA.start()

// Do this after thread is no longer needed to safely stop a thread.
threadA.interrupt();