Java 2

Week 9

  • Sorting Data
  • Functional Interfaces
  • Lambda Expressions, Method References

Comparable Interface

  • Update the Movie class to implement the Comparable interface to create a natural sort order.

  • Because movie IDs are strings we can compare two Strings using the provided compareTo or compareToIgnoreCase method.

  • Generate an equals and hashCode method.

package edu.kirkwood.model;

import java.util.Comparator;
import java.util.Objects;

public class Movie implements Comparable<Movie> {
    // existing code

	/**
     * Compares objects by their id (natural sort order)
     */
	@Override
    public int compareTo(Movie o) {
        // The String class has its own compareTo and compareToIgnoreCase method.
        return this.id.compareTo(o.id);
    }
    
    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Movie movie = (Movie) o;
        return year == movie.year && Objects.equals(id, movie.id) && Objects.equals(title, movie.title) && Objects.equals(plot, movie.plot);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, title, year, plot);
    }

}

Unit Tests

  • Create a MovieTest class.

  • In IntelliJ, click the Fix button to add the JUnit dependency to the pom.xml file.

  • Create a setUp method to instantiate a couple of mock objects.

package edu.kirkwood.model;

import org.junit.jupiter.api.BeforeEach;

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

class MovieTest {
    private Movie m1;
    private Movie m2;
    private Movie m3;
    private List<Movie> movies;

    @BeforeEach
    void setUp() {
        m1 = new Movie("110", "C", 2025, "No Plot");
        m2 = new Movie("11", "a", 2025, "No Plot");
        m3 = new Movie("11", "B", 2025, "No Plot");
        movies = new ArrayList<>();
        movies.add(m1);
        movies.add(m2);
        movies.add(m3);
    }

}

Unit Tests

  • Write unit tests to verify the compareTo method.

  • It will return a negative number if the first object comes before the second object. It will return 0 if the two objects are the same. It will return a positive number if the first object comes after the second object.

  • The tests will fail because "101" comes alphabetically after "11"

package edu.kirkwood.model;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

import static org.junit.jupiter.api.Assertions.*;

class MovieTest {
    private Movie m1;
    private Movie m2;
    private Movie m3;
    private List<Movie> movies;

    @BeforeEach
    void setUp() {
        m1 = new Movie("110", "C", 2025, "No Plot");
        m2 = new Movie("11", "a", 2025, "No Plot");
        m3 = new Movie("11", "B", 2025, "No Plot");
        movies = new ArrayList<>();
        movies.add(m1);
        movies.add(m2);
        movies.add(m3);
    }

    @Test
    void compareToNegative() {
        // Arrange
        int expected = -1; // A negative number means obj1 comes before obj2
        // Act
        int actual = m2.compareTo(m1);
        // Assert
        assertEquals(expected, actual);
    }

    @Test
    void compareToZero() {
        // Arrange
        int expected = 0; // Zero means obj1 and obj2 are the same
        // Act
        int actual = m2.compareTo(m3);
        // Assert
        assertEquals(expected, actual);
    }

    @Test
    void compareToPositve() {
        // Arrange
        int expected = 1; // Positive means obj1 comes after obj2
        // Act
        int actual = m1.compareTo(m3);
        // Assert
        assertEquals(expected, actual);
    }
}

Unit Tests

  • Update the compareTo method to first sort by the length of the id.

  • If the first id length is 2 and the second is 3, this will return -1

  • If the first id length is 2 and the second is 2, this will return 0

  • If the first id length is 3 and the second is 2, this will return 1.

  • If you run the unit tests, they will pass. However, if you increase one movie id length, the test will fail because it is now returning -2 or 2, not -1 or 1.

/**
 * Compares objects by their id (natural sort order)
 */
 @Override
 public int compareTo(Movie o) {
    // First sort by the length of the id
    if(this.id.length() != o.id.length()) {
    	return this.id.length() - o.id.length()
    }
    // If lengths are the same sort them alphabetically
    // The String class has its own compareTo and compareToIgnoreCase method.
    return this.id.compareTo(o.id);
 }

Unit Tests

  • The Integer class has a static compare method.

  • If the first id length is smaller than the second, this will return -1

  • If the first id length is the same as the second, this will return 0

  • If the first id length is larger than the second, this will return 1.

/**
 * Compares objects by their id (natural sort order)
 */
 @Override
 public int compareTo(Movie o) {
    // First sort by the length of the id
    if(this.id.length() != o.id.length()) {
    	return Integer.compare(this.id.length(), o.id.length());
    }
    // If lengths are the same sort them alphabetically
    // The String class has its own compareTo and compareToIgnoreCase method.
    return this.id.compareTo(o.id);
 }

Comparator Functional Interface

  • Create Comparator objects to compare by other data.

  • To create a lambda, start with a pair of parenthesis.
  • Inside the parenthesis you must define the functional interface's abstract method's parameters.
    • To sort things, we need to compare two objects. Those will be our parameters.
  • After the parenthesis type an arrow/lambda operator -> (a dash and greater than sign), followed by a set of curly brackets.
  • Press enter between the curly brackets. The last line will be });
  • Type code in between the curly brackets.
package edu.kirkwood.model;

import java.util.Comparator;
import java.util.Objects;

public class Movie implements Comparable<Movie> {
    // existing code

	@Override
    public int compareTo(Movie o) {
        // Compares objects by their id (natural sort order)
        // The String class has its own compareTo and compareToIgnoreCase method.
        if(this.id.length() != o.id.length()) {
            return this.id.length() - o.id.length();
        }
        return this.id.compareTo(o.id);
    }

    // Compare objects by their title (A to Z)
    public static Comparator<Movie> compareTitleLambdaLong = (Movie m1, Movie m2) -> {
        return m1.getTitle().compareTo(m2.getTitle());
    };


}

Comparator Functional Interface

  • When you declare Comparator<Movie>, Java knows that the two input parameters are Movies, so you can just type (m1, m2).

  • If the code between the curly brackets is a single return statement, the curly brackets and return keyword can be omitted.
  • Written in short form, IntelliJ will suggest that you substitute the code using a method reference.
    • In order to work, the method reference being called must have the same input parameters and return value as defined in the functional interface's abstract method.
  • Create additional Comparator objects.
package edu.kirkwood.model;

import java.util.Comparator;
import java.util.Objects;

public class Movie implements Comparable<Movie> {
    // existing code

	@Override
    public int compareTo(Movie o) {
        // Compares objects by their id (natural sort order)
        // The String class has its own compareTo and compareToIgnoreCase method.
        if(this.id.length() != o.id.length()) {
            return this.id.length() - o.id.length();
        }
        return this.id.compareTo(o.id);
    }

    // Compare objects by their title (A to Z)
    public static Comparator<Movie> compareTitleLambdaLong = (Movie m1, Movie m2) -> {
        return m1.getTitle().compareTo(m2.getTitle());
    };
    public static Comparator<Movie> compareTitleLambdaShort = (m1, m2) -> m1.getTitle().compareTo(m2.getTitle());
    public static Comparator<Movie> compareTitle = Comparator.comparing(Movie::getTitle).thenComparing(Movie::getId);
    // Compares objects by their year (oldest to newest)
    public static Comparator<Movie> compareYear = Comparator.comparing(Movie::getYear).thenComparing(Movie::getId);

}

PresidentDAO Class

  • Write code to get the list of President objects, then sort and display the data.

import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;

public class PresidentDAO {
    private static List<President> presidents = new ArrayList<>();
    
    public static void main(String[] args) {
    	Collections.sort(list);
        printList("Alphabetical A-Z", list);

        Collections.reverse(list);
        printList("Alphabetical Z-A", list);

        // Collections.sort(list,  President.compareHeight);
        // printList("Shortest 5", list, 5);

        // Collections.sort(list,  President.compareHeight.reversed());
        // printList("Tallest 5", list, 5);

        // printOneWithLabel("Shortest", list.stream().min(President.compareHeight).get());

        // printOneWithLabel("Tallest", list.stream().max(President.compareHeight).get());

        // Collections.sort(list, President.compareWeight);
        // printList("Lightest 5", list, 5);

        // Collections.sort(list, President.compareWeight.reversed());
        // printList("Heaviest 5", list, 5);

        // printOneWithLabel("Lightest", list.stream().min(President.compareWeight).get());

        // printOneWithLabel("Heaviest", list.stream().max(President.compareWeight).get());

        // Collections.sort(list, President.compareAge);
        // printList("Oldest 5", list, 5);

        // Collections.sort(list, President.compareAge.reversed());
        // printList("Youngest 5", list, 5);

        // printOneWithLabel("Youngest", list.stream().min(President.compareAge).get());

        // printOneWithLabel("Oldest", list.stream().max(President.compareAge).get());
    }
    
    public static void printList(String title, List<President> presidents) {
        printList(title, presidents, presidents.size());
    }

    public static void printList(String title, List<President> presidents, int count) {
        // The list is printed, formatted as a numbered list, with a title above it.
        System.out.println("---------" + title + "---------");
        for(int i = 0; i < count; i++) {
            System.out.printf("%s) %s, %s\n", (i + 1), presidents.get(i).getLastName(), presidents.get(i).getFirstName());
        }
        System.out.println();
    }

    public static void printOneWithLabel(String label, President president) {
        // Prints a record with a label in front
        System.out.printf("%s: %s, %s\n\n", label, president.getLastName(), president.getFirstName());
    }

    public static List<President> getPresidents() {
        if(presidents.size() == 0) {
             getFromCSV();
        }
        return presidents;
    }

    private static void getFromCSV() {
        List<String> lines = FileInput.readAllLines("presidents.csv");
        for(String line: lines) {
            String[] president = line.split(",");
            try {
                int id = Integer.parseInt(president[0].trim());
                String firstName = president[1].trim();
                String lastName = president[2].trim();
                int height = Integer.parseInt(president[3].trim());
                double weight = Double.parseDouble(president[4].trim());
                LocalDate dateOfBirth = LocalDate.parse(president[5].trim());
                presidents.add(new President(id, firstName, lastName, height, weight, dateOfBirth));
            } catch(IndexOutOfBoundsException | NumberFormatException | DateTimeParseException e) {
                // Skip the line if it is missing a field, or if the String cannot be converted into an int, double, or LocalDate.
                continue;
            }
        }
    }
    
}

Lambdas in Long Form

  • Remove the Comparable implementation in the Book class.
  •  
public class Main {

    public static void usingLambdasLong() {
    	List<Book> books = Books.all();
        Collections.sort(books, (Book b1, Book b2) -> {
            return b1.getTitle().compareTo(b2.getTitle());
        });
        
        for(Book book: books) {
        	System.out.println(book);
        }
    }
    
    public static void main(String[] args) {
        usingLambdasLong();
    }
    
}

The Declarative Way

  • Let's modify the method into a short declarative form.
  • The compiler can figure stuff out well enough that you don't need to declare types. Remove references to Book.
  • The single line shown below is called an expression type body.
    • It creates a function that takes two book objects, and returns the value from the compareTo statement.
public class Main {

    public static void usingLambdasShort() {
    	List<Book> books = Books.all();
        Collections.sort(books, (b1, b2) -> b1.getTitle().compareTo(b2.getTitle()));
        
        for(Book book: books) {
            System.out.println(book);
        }
    }
    
    public static void main(String[] args) {
        usingLambdasShort();
    }
    
}

Method References

  • We use the getTitle method on each instance of Book to get the value that will be used for the comparing command.
  • The lambda in the .forEach() method can use the static method System.out and then the method we want to do, which is println.
    • This returns a reference to the println method and for each applies the entry as the parameter to the function.
  • Another sort example is shown using a List of Strings.
public class Main {

    public static void usingMethodReferences() {
    	List<Book> books = Books.all();
        // Collections.sort(books, Comparator.comparing(b -> b.getTitle()));
        Collections.sort(books, Comparator.comparing(Book::getTitle));
        books.forEach(System.out::println);
    }
    
    public static void main(String[] args) {
        usingMethodReferences();
        
        List<String> names = new ArrayList<>();
        names.add("Marc");
        names.add("amy");
        
        Collections.sort(names, String::compareToIgnoreCase);
        
        names.forEach(name -> {
            name = name.toUpperCase();
            System.out.printf("Hello %s\n", name);
        });
    }
    
}

Animation

  • Create a view class called Animator that will take a message String, like "Loading", and animate an ellipsis to be used when the program loads data.

  • Implement the Runnable interface and override the run method.

package edu.kirkwood.view;

/**
 * A Runnable class that handles the animation logic. This will be run on a
 * separate thread.
 */
public class Animator implements Runnable {
    private final String message;

    /**
     * Constructs the Animator with a message to display.
     * @param message The base text for the loading message.
     */
    public Animator(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        
    }
}

Multithreading

  • Define a boolean flag variable called "running".

  • Use 'volatile' to ensure it is always read from main memory.

    • This is important for thread safety when one thread modifies the flag and another reads it.

  • Define a method to stop the animation manually.

package edu.kirkwood.view;

/**
 * A Runnable class that handles the animation logic. This will be run on a
 * separate thread.
 */
public class Animator implements Runnable {
    private volatile boolean running = true;
    private final String message;

    /**
     * Constructs the Animator with a message to display.
     * @param message The base text for the loading message.
     */
    public Animator(String message) {
        this.message = message;
    }

    /**
     * Signals the animation thread to stop after its current cycle.
     */
    public void stopAnimation() {
        this.running = false;
    }

    @Override
    public void run() {
        while (running) {
            
        }

    }
}

Multithreading

  • Create a loop inside the run method that runs indefinitely.

  • Call Thread.sleep() to pause the thread for a short duration to control the animation speed.

    • 400 means 400 milliseconds, or 0.4 second.

  • When the thread is interrupted, restore the interrupted status and stop running the loop.

package edu.kirkwood.view;

/**
 * A Runnable class that handles the animation logic. This will be run on a
 * separate thread.
 */
public class Animator implements Runnable {
    private volatile boolean running = true;
    private final String message;

    /**
     * Constructs the Animator with a message to display.
     * @param message The base text for the loading message.
     */
    public Animator(String message) {
        this.message = message;
    }

    /**
     * Signals the animation thread to stop after its current cycle.
     */
    public void stopAnimation() {
        this.running = false;
    }

    @Override
    public void run() {
        while (running) {
            

            try {
                // Pause the thread for a short duration to control the animation speed.
                Thread.sleep(400);
            } catch (InterruptedException e) {
                // If the thread is interrupted, restore the interrupted status and stop running.
                Thread.currentThread().interrupt();
                running = false;
            }
        }

    }
}

Animation States

  • Add a states String array containing three animation states

  • Add a currentStateIndex variable to keep track of the current state

  • Inside the loop, print the message followed by the current animation state.

    • The carriage return '\r' moves the cursor to the beginning of the line so we can overwrite it in the next frame of the animation.

package edu.kirkwood.view;

/**
 * A Runnable class that handles the animation logic. This will be run on a
 * separate thread.
 */
public class Animator implements Runnable {
    // Use 'volatile' to ensure the 'running' flag is always read from main memory.
    // This is crucial for thread safety when one thread modifies the flag and another reads it.
    private volatile boolean running = true;
    private final String message;
    private final String[] states = {".  ", ".. ", "..."};
    private int currentStateIndex = 0;

    /**
     * Constructs the Animator with a message to display.
     * @param message The base text for the loading message.
     */
    public Animator(String message) {
        this.message = message;
    }

    /**
     * Signals the animation thread to stop after its current cycle.
     */
    public void stopAnimation() {
        this.running = false;
    }

    @Override
    public void run() {
        while (running) {
            System.out.print("\r" + message + states[currentStateIndex]);

            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                running = false;
            }
        }
        
    }
}

Animation States

  • Cycle to the next state. The modulo operator ensures it wraps around.

  • After the loop finishes, clear the line.

  • We print spaces to overwrite the loading message completely.

package edu.kirkwood.view;

/**
 * A Runnable class that handles the animation logic. This will be run on a
 * separate thread.
 */
public class Animator implements Runnable {
    // Use 'volatile' to ensure the 'running' flag is always read from main memory.
    // This is crucial for thread safety when one thread modifies the flag and another reads it.
    private volatile boolean running = true;
    private final String message;
    private final String[] states = {".  ", ".. ", "..."};
    private int currentStateIndex = 0;

    /**
     * Constructs the Animator with a message to display.
     * @param message The base text for the loading message.
     */
    public Animator(String message) {
        this.message = message;
    }

    /**
     * Signals the animation thread to stop after its current cycle.
     */
    public void stopAnimation() {
        this.running = false;
    }

    @Override
    public void run() {
        while (running) {
            System.out.print("\r" + message + states[currentStateIndex]);

            currentStateIndex = (currentStateIndex + 1) % states.length;

            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                running = false;
            }
        }

        String cleanup = " ".repeat(message.length() + 3);
        System.out.print("\r" + cleanup + "\r");
    }
}

Helpers

  • Inside the Helpers class, create an overloaded method called printList that takes a String title, List<T>, and an optional count. 

  • Type <T> means the input can be any type of List, such as List<Movie>, List<Fraction>, List<String> etc.

  • Whenever a method references Type <T>, you must include <T> before the return type.

  • Create a loop that prints each item with a number in front.

public class Helpers {
    public static <T> void printList(String title, List<T> list) {
        printList(title, list, list.size());
    }

    public static <T> void printList(String title, List<T> list, int count) {
        // The list is printed, formatted as a numbered list, with a title above it.
        System.out.println("---------" + title + "---------");
        for(int i = 0; i < count; i++) {
            System.out.printf("%s) %s\n", (i + 1), list.get(i));
        }
        System.out.println();
    }

	// existing code
}

Helpers

  • Create another generic method called printOneWithLabel that takes a String title, and T obj. 

  • Type T means the input can be any object, such as Movie, Fraction, String, etc.

  • Whenever a method references Type T, you must include <T> before the return type.

public class Helpers {
    public static <T> void printList(String title, List<T> list) {
        printList(title, list, list.size());
    }

    public static <T> void printList(String title, List<T> list, int count) {
        // The list is printed, formatted as a numbered list, with a title above it.
        System.out.println("---------" + title + "---------");
        for(int i = 0; i < count; i++) {
            System.out.printf("%s) %s\n", (i + 1), list.get(i));
        }
        System.out.println();
    }

    public static <T> void printOneWithLabel(String title, T obj) {
        // Prints a record with a label in front
        System.out.println("---------" + title + "---------");
        System.out.println(obj);
    }

	// existing code
}

DataApp

  • Call the UserInput.getString() method to prompt the user for a movie title.

  • Create a getResults method that takes the movie title and returns a List<Movie>.

public class MyDataApp {
    public static void main(String[] args) throws InterruptedException {
        String title = getString("Enter a movie title", true);

        List<Movie> results = getResults(title);
    }

    public static List<Movie> getResults(String title) {
        

        return List.of();
    }
}

Java 2 - Week 9

By Marc Hauschildt

Java 2 - Week 9

  • 426