Marc Hauschildt
Web Technologies and Computer Software Development Instructor at Kirkwood Community College in Cedar Rapids, IA.
Week 9
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);
}
}
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);
}
}
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);
}
}
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);
}
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);
}
Create Comparator objects to compare by other data.
-> (a dash and greater than sign), followed by a set of 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());
};
}
When you declare Comparator<Movie>, Java knows that the two input parameters are Movies, so you can just type (m1, m2).
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);
}
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;
}
}
}
}
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();
}
}
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();
}
}
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);
});
}
}
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() {
}
}
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) {
}
}
}
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;
}
}
}
}
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;
}
}
}
}
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");
}
}
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
}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
}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();
}
}During the month of October, there is a global open source contribution initiative called Hacktoberfest.
To participate, you need to register your GitHub account on their website and make at least 4 meaningful pull requests in the month of October.
Here are some examples of open source projects
git remote -v
git remote add upstream https://github.com/mlhaus/xxxx.git
git remote -v
git checkout -b yourname-contribution1
git add .
git commit -m 'A meaningful message'git push origin yourname-contribution1
git pull origin main
git checkout main
Note: If you make any changes to the main branch, you must remove or commit the code before continuing.
Use the Git > Fetch command to obtain the change history (branches and commits) from the upstream repo.
Or you can enter this command: git fetch upstream
When you fetch changes from upstream, all new commits are downloaded and stored as a remote branch.
Use the Git > Merge command to merge changes from the upstream main branch into your local main branch.
git merge upstream/main
git push origin main
Original
Repo
Your
Copy
Local Computer
Fork it
Clone to
set Origin
Set Upstream
Fetch
Changes
Push Changes
By Marc Hauschildt
Web Technologies and Computer Software Development Instructor at Kirkwood Community College in Cedar Rapids, IA.