
Java - Week 4Week 4Grouping together classes in JavaAbstract ClassesGeneric functionsExampleMultiple Inheritance in Java Sorting Circle Objects Interfaces in Java Implementing the Comparable Interface Interface in more detailUsing Comparable for Quicksort Comparable Interface Example Adding Methods to Interfaces in Java Static Methods in Interfaces Example Default Methods in Interfaces Example Dealing with Conflicts in Java Interfaces Types of Conflicts 1. Conflict Between Static and Default Methods 2. Conflict Between a Class and an Interface Example Explanation Resolving Conflicts with Fresh Implementation Overridden Example Output Example Understanding Nested Objects and Inner ClassesWhy Should Node Be Private?Full Implementation of LinkedList with a Private Node ClassInteraction with stateAdvantages of EncapsulationThe Date Class ExampleControlled interaction with ObjectHow do we achieve this?1. Logging In2. Using the Query Object3. Why Use an Interface?4. Limiting QueriesHow Does It Work Step-by-Step?Why Is This a Good Design?Code ExampleImagine It Like ThisCallback in JavaStep 1: A Timer Specific to MyclassStep 2: Making the Timer Generic with ObjectStep 3: Using Interfaces for Safety1. Define an Interface for the Callback2. Modify Myclass to Implement the Interface3. Modify Timer to Use the InterfaceJava IteratorLinear List ExampleLinearList ClassIterator InterfaceImplementing Iterator in LinearListUsing Multiple Iterators
In object-oriented programming, related classes like Circle, Square, and Rectangle can extend a base class Shape to group shared behavior. To ensure consistency, the Shape class can define a public double perimeter() method. A default implementation, such as public double perimeter() { return -1.0; }, relies on subclasses to override it but risks incorrect behavior if they don't, making this approach dependent on programmer discipline.
A better solution is to make the perimeter() method abstract in the Shape class by defining it as public abstract double perimeter();. This ensures every subclass, such as Circle, Square, and Rectangle, provides a concrete implementation. Since abstract classes cannot be instantiated, the Shape class must also be declared abstract using public abstract class Shape. This design enforces consistency and eliminates reliance on programmer discipline.
Although abstract classes cannot be instantiated directly, variables of their type can be declared. For example, you can create an array Shape shapearr[] = new Shape[3]; and populate it with subclass objects: shapearr[0] = new Circle(...);, shapearr[1] = new Square(...);, and shapearr[2] = new Rectangle(...);. Iterating through the array and calling shapearr[i].perimeter() invokes the appropriate implementation for each subclass, demonstrating polymorphism and ensuring flexible, reusable code.
Abstract classes define shared properties and behaviors for related classes, enabling reusable and flexible code. When an abstract class implements the Comparable interface, it enforces comparison logic in its subclasses, allowing sorting of objects that extend the abstract class. This ensures consistency and promotes code reuse across different object types.
Note: Quicksort is a sorting algorithm. Don't worry about the algorithm, focus on the concept here.
x1abstract class Comparable {2 public abstract int cmp(Comparable s);3}4class Myclass extends Comparable {5 private double size; 6 public Myclass(double size) {7 this.size = size;8 }9 // Implementing the cmp method for comparison10 public int cmp(Comparable s) {11 if (s instanceof Myclass) {12 Myclass other = (Myclass) s;13 if (this.size < other.size) {14 return -1; 15 } else if (this.size == other.size) {16 return 0; 17 } else {18 return 1;19 }20 }21 return 0;22 }23
24 public double getSize() {25 return size;26 }27}28// SortFunctions class with quicksort method29class SortFunctions {30 public static void quicksort(Comparable[] a) {31 quicksort(a, 0, a.length - 1);32 }33 private static void quicksort(Comparable[] a, int low, int high) {34 if (low < high) {35 int pivotIndex = partition(a, low, high);36 quicksort(a, low, pivotIndex - 1);37 quicksort(a, pivotIndex + 1, high);38 }39 }40 private static int partition(Comparable[] a, int low, int high) {41 Comparable pivot = a[high]; // pivot element42 int i = low - 1; // Index of smaller element43 for (int j = low; j < high; j++) {44 if (a[j].cmp(pivot) < 0) {45 i++;46 // Swap a[i] and a[j]47 Comparable temp = a[i];48 a[i] = a[j];49 a[j] = temp;50 }51 }52
53 // Swap a[i+1] and pivot (a[high])54 Comparable temp = a[i + 1];55 a[i + 1] = a[high];56 a[high] = temp;57
58 return i + 1;59 }60}61public class Main {62 public static void main(String[] args) {63 Myclass[] arr = {64 new Myclass(3.5),65 new Myclass(1.2),66 new Myclass(4.7),67 new Myclass(2.6),68 new Myclass(0.9)69 };70 System.out.println("Before sorting:");71 for (Myclass m : arr) {72 System.out.println(m.getSize());73 }74 SortFunctions.quicksort(arr);75 System.out.println("\nAfter sorting:");76 for (Myclass m : arr) {77 System.out.println(m.getSize());78 }79 }80}Java does not support multiple inheritance with classes to avoid ambiguity and complexity. However, it allows classes to extend a single class and implement multiple interfaces, enabling a form of multiple inheritance for flexibility in design.
Consider the need to sort Circle objects using generic functions in SortFunctions. Since Circle already extends the Shape class, it cannot extend another class like Comparable. This limitation arises because Java does not support multiple inheritance. However, by using interfaces, we can achieve the desired functionality.
An interface in Java is similar to an abstract class but contains no concrete methods. It specifies a set of methods that a class must implement. For example, the Comparable interface can be defined as follows:
xxxxxxxxxx31public interface Comparable {2 public abstract int cmp(Comparable s);3}A class that implements an interface is required to define all the methods declared in that interface.
To make the Circle class sortable, we can implement the Comparable interface alongside extending the Shape class. Here’s how it is done:
xxxxxxxxxx131public class Circle extends Shape implements Comparable {2 public double perimeter() {3 // Implementation for calculating perimeter4 return 2 * Math.PI * radius;5 }6
7 public int cmp(Comparable s) {8 // Comparison logic for sorting, e.g., based on radius9 if (this.radius > ((Circle) s).radius) return 1;10 if (this.radius < ((Circle) s).radius) return -1;11 return 0;12 }13}An interface in Java is like a blueprint where all the methods are abstract and have no implementation. A class that implements an interface must provide code for all its methods. Unlike classes, a class can implement multiple interfaces, allowing it to inherit different functionalities without any conflicts. Interfaces describe specific abilities or roles of a class, making them easy to use and understand. Other classes only need to know what the interface can do, not how it works, which helps keep the code simple and flexible.
In programming, there are cases where a function or algorithm needs limited information about the objects it operates on. For example, a generic quicksort function works for any datatype that supports comparisons. Instead of dealing with all properties of a datatype, it only needs the ability to compare two objects. This focused capability can be expressed using an interface like Comparable.
The Comparable interface is used to define the comparison behavior for objects. The quicksort algorithm, which can sort any array of Comparable objects, relies solely on this capability. By declaring the input type as Comparable[], we ensure that the algorithm only interacts with objects that implement the cmp method, ignoring all other details of the objects.
Here is how quicksort can be implemented using the Comparable interface:
xxxxxxxxxx781interface Comparable {2 // Compares this object with another Comparable object3 public abstract int cmp(Comparable s);4}5class Myclass implements Comparable {6 private double size; 7 public Myclass(double size) {8 this.size = size;9 }10 // Implementing the cmp method for comparison11 public int cmp(Comparable s) {12 if (s instanceof Myclass) {13 Myclass other = (Myclass) s;14 if (this.size < other.size) {15 return -1; 16 } else if (this.size == other.size) {17 return 0; 18 } else {19 return 1; 20 }21 }22 return 0;23 }24 public double getSize() {25 return size;26 }27}28
29class SortFunctions {30 public static void quicksort(Comparable[] a) {31 quicksort(a, 0, a.length - 1);32 }33 private static void quicksort(Comparable[] a, int low, int high) {34 if (low < high) {35 int pivotIndex = partition(a, low, high);36 quicksort(a, low, pivotIndex - 1); // Sort the left part37 quicksort(a, pivotIndex + 1, high); // Sort the right part38 }39 }40 private static int partition(Comparable[] a, int low, int high) {41 Comparable pivot = a[high]; 42 int i = low - 1; 43 for (int j = low; j < high; j++) {44 if (a[j].cmp(pivot) < 0) {45 i++;46 // Swap a[i] and a[j]47 Comparable temp = a[i];48 a[i] = a[j];49 a[j] = temp;50 }51 }52 Comparable temp = a[i + 1];53 a[i + 1] = a[high];54 a[high] = temp;55
56 return i + 1;57 }58}59public class Main {60 public static void main(String[] args) {61 Myclass[] arr = {62 new Myclass(3.5),63 new Myclass(1.2),64 new Myclass(4.7),65 new Myclass(2.6),66 new Myclass(0.9)67 };68 System.out.println("Before sorting:");69 for (Myclass m : arr) {70 System.out.println(m.getSize());71 }72 SortFunctions.quicksort(arr);73 System.out.println("\nAfter sorting:");74 for (Myclass m : arr) {75 System.out.println(m.getSize());76 }77 }78}Java interfaces have evolved to include new features, making them more versatile and functional. Earlier, interfaces could only have abstract methods, but now they can include static methods and default methods, which enhance their usability without breaking existing code.
Static methods in interfaces allow defining utility or helper methods directly within the interface. These methods:
Cannot access instance variables or methods of implementing classes.
Are invoked directly using the interface name, rather than an instance of the implementing class.
xxxxxxxxxx91public interface Comparable {2 public static String cmpdoc() {3 String s;4 s = "Return -1 if this < s, ";5 s = s + "0 if this == s, ";6 s = s + "+1 if this > s.";7 return s;8 }9}The static method cmpdoc provides documentation for the comparison logic.
It can be called directly using the interface name, like this:
xxxxxxxxxx11System.out.println(Comparable.cmpdoc());Default methods enable interfaces to provide a basic implementation of certain methods. This allows developers to:
Add new methods to interfaces without forcing all implementing classes to define them.
Provide a default behavior that implementing classes can optionally override.
xxxxxxxxxx51public interface Comparable {2 public default int cmp(Comparable s) {3 return 0;4 }5}The cmp method here has a default implementation that always returns 0.
If an implementing class does not override this method, it will inherit the default behavior.
If needed, an implementing class can override the method with its own logic:
xxxxxxxxxx61public class Circle implements Comparable {2 3 public int cmp(Comparable s) {4 return -1; // Example: Circle is "less than" other objects5 }6}Default methods are invoked like regular instance methods using the object name:
xxxxxxxxxx31Circle c1 = new Circle();2Circle c2 = new Circle();3System.out.println(c1.cmp(c2)); // Calls the overridden method in Circle classJava provides mechanisms to handle conflicts that can arise from the old problem of multiple inheritance, which is more prominent with the introduction of default methods in interfaces. These conflicts occur when a class inherits methods with the same name and signature from multiple sources. Java resolves these conflicts with specific rules, ensuring clarity and backward compatibility.
When a static method in a class or interface conflicts with a default method in an interface, the subclass must provide a fresh implementation to resolve the conflict.
If a method is inherited from both a class and an interface, the method in the class "wins." This is motivated by the principle of backward compatibility, as Java assumes that class methods are already well-defined and should take precedence over interface methods.
Consider the following scenario where a class (Person) and an interface (Designation) define methods with the same name and signature.
xxxxxxxxxx191// Class defines a method2public class Person {3 public String getName() {4 return "No name";5 }6}7
8// Interface defines a default method9public interface Designation {10 public default String getName() {11 return "No designation";12 }13}14
15// Class extends Person and implements Designation16public class Employee extends Person implements Designation {17 // No need to override getName()18 // Person's getName() method will be used19}Class Method Precedence
In the Employee class, both Person (class) and Designation (interface) define a getName() method.
Java resolves this conflict by giving precedence to the class method (Person.getName()), as classes are considered the primary source of behavior in Java's object-oriented model.
Default Method Ignored
The default method getName() from Designation is ignored unless the subclass (Employee) explicitly overrides it.
Output Example
When the getName() method is called on an Employee object, it will invoke the method from the Person class:
xxxxxxxxxx21Employee emp = new Employee();2System.out.println(emp.getName()); // Output: "No name"If the Employee class wants to define its own behavior for getName(), it can override the method explicitly.
xxxxxxxxxx61public class Employee extends Person implements Designation {2 3 public String getName() {4 return "Employee name";5 }6}xxxxxxxxxx21Employee emp = new Employee();2System.out.println(emp.getName()); // Output: "Employee name"When designing a LinkedList, its fundamental building block is the Node. A Node represents an individual element in the list and typically contains:
Data: The value stored in the node.
Next Reference: A pointer to the next node in the list.
(Optional) Previous Reference: In the case of a doubly linked list, a reference to the previous node.
Node Be Private?Encapsulation:
Encapsulation is a core principle of object-oriented programming. Keeping Node as a private class ensures that the internal workings of the LinkedList are hidden from the outside world. Users of the LinkedList class interact only with its public methods (head, insert, etc.), not with its internal structure (Node).
Ease of Maintenance:
If the Node structure needs to change (e.g., adding a prev field to convert the list into a doubly linked list), this change will not impact the interface of the LinkedList. The users of the LinkedList class will remain unaffected because they do not directly access Node.
Improved Access Control:
By making Node private, we ensure that only the LinkedList class has access to it. This prevents accidental misuse or modification of Node from outside the LinkedList class.
An inner class, like Node, can access all private components of its enclosing class (LinkedList). This relationship is beneficial when implementing methods like insert, which may require modifying private fields such as size or first. Suppose we want to enhance our Node to support a doubly linked list by adding a prev field. This change is straightforward and does not affect the public API of LinkedList.
LinkedList with a Private Node ClassHere’s a detailed implementation based on the above discussion:
xxxxxxxxxx391class LinkedList {2 private int size; // Tracks the number of elements in the list3 private Node first; // Reference to the first node in the list4
5 // Public method to retrieve the head of the list6 public Object head() {7 return (first != null) ? first.data : null; // Return data of the first node or null if empty8 }9
10 // Public method to insert a new element at the beginning of the list11 public void insert(Object newData) {12 Node newNode = new Node(newData); // Create a new node with the given data13 newNode.next = first; // Point the new node's next to the current first node14 if (first != null) {15 first.prev = newNode; // Update the previous reference of the current first node16 }17 first = newNode; // Update the first node reference to the new node18 size++; // Increment the size of the list19 }20
21 // Private inner class representing a Node22 private class Node {23 public Object data; // Data stored in the node24 public Node next; // Reference to the next node25 public Node prev; // Reference to the previous node (for doubly linked list)26
27 // Constructor to initialize a Node with data28 public Node(Object data) {29 this.data = data;30 this.next = null;31 this.prev = null;32 }33 }34
35 // Optional: Method to get the size of the list36 public int getSize() {37 return size;38 }39}Encapsulation is one of the fundamental principles of object-oriented programming (OOP). It involves:
Hiding Internal Data: The internal state of an object (its data fields) is kept private to prevent unauthorized or unintended access from external code.
Providing Controlled Access: Public methods, such as accessors (getters) and mutators (setters), regulate how the data can be accessed or modified. This control helps maintain the integrity of the object's state.
Data Integrity:
By making the fields private and using methods to control access, you can enforce rules (e.g., validation) that ensure the object's data remains consistent and valid.
Flexibility:
You can modify the internal implementation without affecting external code that depends on the class, as long as the public interface remains the same.
Simplified Maintenance:
Encapsulation helps isolate changes. If you decide to change how data is stored or processed internally, you only need to update the class implementation, not the code that uses it.
Improved Debugging:
Encapsulation ensures that all changes to the data go through a single point (e.g., setter methods), making it easier to debug and track issues.
Date Class ExampleThe Date class represents a date with day, month, and year. By encapsulating its fields and using public methods to access and modify them, we ensure that invalid dates cannot be set.
Here’s the detailed implementation:
xxxxxxxxxx781class Date {2 // Private fields to store the day, month, and year3 private int day;4 private int month;5 private int year;6
7 // Getter methods to retrieve individual components of the date8 public int getDay() {9 return day;10 }11
12 public int getMonth() {13 return month;14 }15
16 public int getYear() {17 return year;18 }19
20 // Setter method to update the entire date at once21 public void setDate(int day, int month, int year) {22 if (isValidDate(day, month, year)) { // Validate the date combination23 this.day = day;24 this.month = month;25 this.year = year;26 } else {27 System.out.println("Invalid date: " + day + "/" + month + "/" + year);28 }29 }30
31 // Private method to validate the date32 private boolean isValidDate(int day, int month, int year) {33 // Check year validity34 if (year < 1) {35 return false;36 }37
38 // Check month validity39 if (month < 1 || month > 12) {40 return false;41 }42
43 // Determine the maximum number of days in the given month44 int maxDays;45 switch (month) {46 case 4: case 6: case 9: case 11:47 maxDays = 30; // April, June, September, November have 30 days48 break;49 case 2:50 // February: Check for leap year51 maxDays = (isLeapYear(year)) ? 29 : 28;52 break;53 default:54 maxDays = 31; // All other months have 31 days55 }56
57 // Check day validity58 return day >= 1 && day <= maxDays;59 }60
61 // Helper method to determine if a year is a leap year62 private boolean isLeapYear(int year) {63 return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);64 }65}66public class Main {67 public static void main(String[] args) {68 Date date = new Date();69
70 // Set a valid date71 date.setDate(15, 8, 2023);72 System.out.println("Date: " + date.getDay() + "/" + date.getMonth() + "/" + date.getYear());73
74 // Set an invalid date75 date.setDate(31, 4, 2023); // April has only 30 days76 // The program will print an error message for the invalid date77 }78}Imagine you have a system where people can check how many seats are available on a train for a specific date. But we don’t want bots (automated programs) to spam the system by making a lot of requests and slowing it down for everyone else. So, we need to:
Make sure only logged-in users can check train availability.
Limit how many queries a user can make after logging in (e.g., 3 queries per login).
We use objects to manage this process. Here's how it works:
When a user logs in, we check if their username and password are correct:
If correct, the system gives them an object called a QueryObject.
This object allows them to check train seat availability.
If the login fails (e.g., wrong password), the user doesn’t get the object and can’t make any queries.
Once logged in, the user uses the QueryObject to check seat availability. This object:
Talks to the database (called BookingDB) to get the number of available seats for a specific train on a specific date.
Keeps track of how many times the user has queried the system.
Stops working once the user reaches the query limit (e.g., 3 queries).
An interface is like a contract that describes what a class (like QueryObject) can do. In our case:
The interface (QIF) says, "Any object that implements me must have a getStatus method to check train availability."
This makes it easy for the main program to interact with the QueryObject because it only needs to know about the QIF interface, not the details of how QueryObject works.
To stop users from overusing the system:
The QueryObject keeps a counter (numqueries) to track how many queries the user has made.
If the user tries to make more queries than allowed (QLIM), the system tells them they’ve reached their limit.
A user tries to log in.
If the login is successful, they get a QueryObject.
If the login fails, they don’t get anything.
The user uses the QueryObject to check train seat availability.
Each time they query, the system checks if they’ve reached their limit.
If not, it shows the number of available seats.
If they’ve reached the limit, it tells them to log in again.
The system tracks everything to ensure fair usage.
Encapsulation:
The RailwayBooking class handles login and controls access to the database.
The QueryObject class only focuses on querying the database.
Security:
Users can’t access the database directly. They must log in first.
The query limit ensures that no one can overload the system.
Reusability:
By using an interface (QIF), we can easily add new types of query objects in the future without changing the main program.
xxxxxxxxxx661import java.util.Date;2interface QIF {3 int getStatus(int trainNo, Date date);4}5// BookingDB: Simulates the train database6class BookingDB {7 public static int getAvailableSeats(int trainNo, Date date) {8 // Hardcoded seat availability for simplicity9 if (trainNo == 101) return 50;10 if (trainNo == 102) return 30;11 if (trainNo == 103) return 10;12 return 0; // No seats available for other train numbers13 }14}15// RailwayBooking: Manages login and QueryObject creation16class RailwayBooking {17 private BookingDB railwayDB = new BookingDB();18 // Simulates user login validation19 private boolean validLogin(String username, String password) {20 // Replace with actual login validation logic if needed21 return "user".equals(username) && "pass".equals(password);22 }23 public QIF login(String username, String password) {24 if (validLogin(username, password)) {25 return new QueryObject(); // Create a new QueryObject upon successful login26 }27 return null; // Login failed28 }29 // Private inner class implementing QIF30 private class QueryObject implements QIF {31 private int numQueries = 0; // Track the number of queries32 private static final int QLIM = 3; // Query limit per login33 public int getStatus(int trainNo, Date date) {34 if (numQueries >= QLIM) {35 System.out.println("Query limit reached. Please log in again.");36 return -1; // Indicates the query limit has been reached37 }38 numQueries++;39 int seatsAvailable = BookingDB.getAvailableSeats(trainNo, date);40 System.out.println("Available seats for Train " + trainNo + " on " + date + ": " + seatsAvailable);41 return seatsAvailable;42 }43 }44}45public class Main {46 public static void main(String[] args) {47 RailwayBooking system = new RailwayBooking();48
49 // Attempt login50 QIF queryObject = system.login("user", "pass");51
52 if (queryObject == null) {53 System.out.println("Login failed. Incorrect username or password.");54 return;55 }56
57 // Successful login: Perform train queries58 System.out.println("Login successful. Querying train seat availability...");59
60 // Query train seat availability61 queryObject.getStatus(101, new Date());62 queryObject.getStatus(102, new Date());63 queryObject.getStatus(103, new Date());64 queryObject.getStatus(104, new Date()); // This should exceed the query limit65 }66}Think of a theme park:
You need a ticket (login) to enter the park.
After entering, you’re given a wristband (QueryObject) to ride the attractions.
The wristband has a limited number of rides (query limit). Once you use up all your rides, you need to buy a new ticket (log in again).
A callback is a way for one object (e.g., a Timer) to notify another object (e.g., Myclass) that something has happened. In this case:
The Timer runs in parallel (using Runnable) and, when finished, it "calls back" the Myclass object to notify it.
The Myclass decides what to do when the timer is done.
MyclassHere’s how a simple timer tied specifically to Myclass works:
How it works:
The Timer class has a reference to its creator (Myclass), which is passed to it during construction.
When the timer finishes, it directly calls the timerdone() method on the Myclass object.
Code:
xxxxxxxxxx411class Myclass {2 // Method to start the timer3 public void f() {4 Timer t = new Timer(this); // Pass `this` (current object) to Timer5 t.start(); // Start the timer6 }7
8 // Method to be called when the timer is done9 public void timerdone() {10 System.out.println("Timer is done!");11 }12}13
14class Timer implements Runnable {15 private Myclass owner; // Reference to the owner (Myclass object)16
17 // Constructor to initialize the Timer with its owner (Myclass object)18 public Timer(Myclass o) {19 owner = o; // Remember who created this timer20 }21
22 // Method to start the timer and notify the owner when done23 public void start() {24 // Simulate some timer logic25 System.out.println("Timer started...");26 owner.timerdone(); // Notify the owner that the timer is done27 }28
29 30 public void run() {31 // This method is required to implement Runnable but is not used in this case32 start();33 }34}35
36public class Main {37 public static void main(String[] args) {38 Myclass myObject = new Myclass(); // Create an instance of Myclass39 myObject.f(); // Start the process, which will start the timer40 }41}Problem:
This approach only works for Myclass. If you want another class (e.g., AnotherClass) to use the Timer, you’d have to modify the Timer to know about AnotherClass.
ObjectTo make the timer more flexible:
We change the owner to be a general Object instead of Myclass.
When notifying, we cast the owner back to the expected type (Myclass) to call timerdone().
Code:
xxxxxxxxxx431class Myclass {2 // Method to start the timer3 public void f() {4 Timer t = new Timer(this); // Pass `this` (current object) to Timer5 t.start(); // Start the timer6 }7
8 // Method to be called when the timer is done9 public void timerdone() {10 System.out.println("Timer is done!");11 }12}13
14class Timer implements Runnable {15 private Object owner; // A generic owner16
17 // Constructor to initialize the Timer with its owner (Myclass object)18 public Timer(Object o) {19 owner = o; // Remember who created this timer20 }21
22 // Method to start the timer and notify the owner when done23 public void start() {24 // Simulate some timer logic25 System.out.println("Timer started...");26 // Cast the owner back to Myclass and call the timerdone method27 ((Myclass) owner).timerdone();28 }29
30 31 public void run() {32 // This method is required to implement Runnable but is not used in this case33 start();34 }35}36
37public class Main {38 // Main method to run the program39 public static void main(String[] args) {40 Myclass myObject = new Myclass(); // Create an instance of Myclass41 myObject.f(); // Start the process, which will start the timer42 }43}Problem:
Using Object makes the timer generic, but it requires casting, which can be risky. If the owner isn’t actually a Myclass, the program will throw a ClassCastException.
To solve the casting problem and ensure type safety, we use an interface:
An interface acts as a contract that says, "Any class using the timer must have a timerdone() method."
This way, the Timer knows that its owner will always have the timerdone() method, no matter what class it is.
The interface (Timerowner) specifies what the callback should look like:
xxxxxxxxxx31interface Timerowner {2 void timerdone(); // A method to be implemented by the owner3}Myclass to Implement the InterfaceNow, Myclass implements the Timerowner interface, meaning it promises to provide the timerdone() method:
xxxxxxxxxx101class Myclass implements Timerowner {2 public void f() {3 Timer t = new Timer(this); // Pass `this` to Timer4 t.start(); // Start the timer5 }6
7 public void timerdone() {8 System.out.println("Timer is done!");9 }10}Timer to Use the InterfaceThe Timer now expects its owner to be a Timerowner. This ensures that the owner has a timerdone() method, removing the need for risky casting:
xxxxxxxxxx271// Timer class that accepts a Timerowner and notifies the owner when done2class Timer implements Runnable {3 private Timerowner owner; // The owner must implement Timerowner4 public Timer(Timerowner o) {5 owner = o; // Remember the owner6 }7
8 // Method to start the timer and notify the owner when done9 public void start() {10 // Simulate some timer logic11 System.out.println("Timer started...");12 owner.timerdone(); // Call the owner's `timerdone()` method13 }14
15 16 public void run() {17 // This method is required to implement Runnable but is not used in this case18 start();19 }20}21public class Main {22 public static void main(String[] args) {23 Myclass myObject = new Myclass(); // Create an instance of Myclass24 myObject.f(); // Start the process, which will start the timer25 }26}27
An Iterator is a design pattern used to traverse a collection (like a list) without exposing its underlying structure. It simplifies accessing and iterating over elements, ensuring uniformity in how we access them.
We have a LinearList class, which can be implemented using an array or linked list internally. To interact with it, we need an iterator because the list's internal structure isn't exposed.
xxxxxxxxxx151public class Linearlist {2 private Node head; // The start of the list3
4 private class Node {5 Object data;6 Node next;7 }8
9 public void append(Object o) {10 Node m;11 for (m = head; m != null && m.next != null; m = m.next) {}12 Node n = new Node(o);13 if (m != null) m.next = n;14 }15}The Iterator interface defines two key methods:
has_next(): Checks if there are more elements.
get_next(): Returns the next element.
xxxxxxxxxx41public interface Iterator {2 boolean has_next();3 Object get_next();4}LinearListThe Iter class implements Iterator, starting at the list's head and moving through the nodes.
xxxxxxxxxx681// Define the Linearlist class2class Linearlist {3 private Node head; // Head of the linked list4
5 // Inner class to represent a node in the linked list6 private class Node {7 Object data; // Data in the node8 Node next; // Reference to the next node9
10 Node(Object data) {11 this.data = data;12 this.next = null;13 }14 }15
16 // Method to append data to the linked list17 public void append(Object data) {18 Node newNode = new Node(data);19 if (head == null) {20 head = newNode; // If the list is empty, the new node becomes the head21 } else {22 Node temp = head;23 while (temp.next != null) {24 temp = temp.next; // Traverse to the last node25 }26 temp.next = newNode; // Append the new node at the end27 }28 }29
30 // Inner class to implement the Iterator interface for traversing the linked list31 private class Iter implements Iterator {32 private Node position = head;33
34 public boolean has_next() {35 return position != null; // Check if there is a next node36 }37
38 public Object get_next() {39 if (has_next()) {40 Object data = position.data;41 position = position.next; // Move to the next node42 return data;43 }44 return null; // Return null if no next node45 }46 }47
48 // Method to get a fresh iterator49 public Iterator get_iterator() {50 return new Iter();51 }52}53
54// Main class to test the Linearlist55class Main {56 public static void main(String[] args) {57 Linearlist l = new Linearlist(); // Create a new Linearlist instance58 l.append("Hello"); // Append "Hello" to the list59 l.append("World"); // Append "World" to the list60
61 // Get an iterator and iterate through the list62 Iterator i = l.get_iterator();63 while (i.has_next()) {64 System.out.println(i.get_next()); // Output: Hello, World65 }66 }67}68
If needed, you can create multiple iterators to traverse the list at different points.
xxxxxxxxxx91Iterator i = l.get_iterator();2while (i.has_next()) {3 Object oi = i.get_next();4 Iterator j = l.get_iterator();5 while (j.has_next()) {6 Object oj = j.get_next();7 System.out.println(oi + " " + oj); // Print combinations8 }9}