0% found this document useful (0 votes)
1 views

week11

The document covers exceptions, wrapper classes, and generics in Java, explaining how to handle runtime errors through structured exception handling using try-catch blocks and custom exceptions. It also discusses the importance of generics for creating flexible data structures like ArrayLists and the use of wrapper classes for primitive types. Additionally, it introduces the concept of defining generic classes and interfaces, along with a custom implementation of a LinkedList data structure.

Uploaded by

fatihuzuntas51
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1 views

week11

The document covers exceptions, wrapper classes, and generics in Java, explaining how to handle runtime errors through structured exception handling using try-catch blocks and custom exceptions. It also discusses the importance of generics for creating flexible data structures like ArrayLists and the use of wrapper classes for primitive types. Additionally, it introduces the concept of defining generic classes and interfaces, along with a custom implementation of a LinkedList data structure.

Uploaded by

fatihuzuntas51
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 18

Week 11 - Exceptions, wrapper classes, and generics

EXCEPTIONS
Exceptions are runtime errors that interrupt the regular operation of a program while it’s running.
Some examples of exceptions are division by zero, invalid user input, file not found. When an
exception occurs in a program, the way we handle it can significantly impact the program’s
reliability, maintainability, and usability. If we merely return or display strings to indicate an error,
it can lead to several potential issues such as debugging difficulty, handling exceptions correctly.

Exception handling involves using structured approaches to address and manage exceptions
when they occur. It simplifies error handling, reduces the risk of unhandled errors and improves
program robustness and user experience.

Consider the following case. What happens if the variable b has a value of 0? A runtime error
will be triggered because integer division by 0 is not allowed.
public int aMethod(int a, int b){
return a / b;
}

To fix this, you can try to use an if structure:


public int getQuotient(int a, int b){
if ( b == 0){
System.out.println(“division by zero”);
System.exit(1); → terminates the whole execution
}
return a / b;
}
When you see an exception, we are terminating the execution. The user of the code might not
comprehend what the problem is. Instead, the user of the code might be informed about the
exception. S/he should decide what to do in case of an exception. So, we can throw an
exception to let the user of the code about the exception.
public int getQuotient(int a, int b){
if ( b == 0){
throw new ArithmeticException(“Division by zero”);
}
return a / b;
}
When a throw statement is called it creates an instance of ArithmeticExpcetion that can be
caught by the user of the code. ArithmeticExpcetion is a subtype of the Exception class in Java
(in java.lang) and one of its constructors requires a String object defining the problem. When we
throw an exception, the normal execution of the code is interrupted. The user of the code
should plan what to do in case of a thrown exception. But, how?

try and catch blocks

1/18
The try block contains the code that might cause an exception while the catch block includes
the code that will handle the exception. The code within the try block is the only code that can
be monitored for exceptions. If an exception occurs outside of the try block, it will not be
caught and handled by the associated catch block. It is important to place the code that might
throw exceptions within the try block to ensure proper exception handling. Below code is a
demonstration including try and catch blocks for the above method throwing an exception:

// some code before


int c;
try{
// code to run here to monitor for an exception
c = getQuotient(10, 0);
}
catch (ArithmeticExpcetion ex){
// code to handle the exception
System.out.println("Exception: an integer cannot be divided by zero ");
// set c to 1 for example to illustrate we can do more than informing the user
c = 1;
}
In the catch block, you define the type of exception you want to handle. If Exception becomes
the type, you would catch all types of exceptions. However, it is usually known in advance what
type of exceptions might occur so restricting the type of the exception could be useful. Now, our
code does not stop its execution due to a runtime error. It has been properly handled in the
catch block.

Exception class extends Throwable class and here, spends some time to explore different types
of exceptions in here (I verbally discussed some of them in the class):
https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html

Declaring, throwing and catching exceptions


You should use the throws keyword to declare that a method could throw an exception.
public int getQuotient(int a, int b) throws ArithmeticException{
….

You should use the throw keyword to throw an exception.


public int getQuotient(int a, int b) throws ArithmeticException{
if ( b == 0){
throw new ArithmeticException(“Division by zero”);
}
return a / b;
}

2/18
You should use the try-catch block to catch exceptions. The prototype is as follows. As you can
see you define many catch blocks for a single try block because there might be more than one
exception occurring in try and each or some of them could be treated differently.

try{
}
catch(Exceptiontype ex1){
}
catch(Exceptiontype2 ex2){
}
catch(Exceptiontype3 ex3){
}
… // some more catch blocks

In Java, the Throwable class is the superclass of all errors and exceptions. It provides several
fields and methods that can be used in case of an exception.
Fields:

● getMessage(): Returns a string describing the exception.


● getStackTrace(): Returns an array of StackTraceElement objects, each representing a
stack frame on the call stack at the time the exception was thrown.
● getCause(): Returns the cause of this throwable or null if the cause is nonexistent or
unknown.

Methods:

● printStackTrace(): Prints the throwable and its backtrace to the standard error stream.
● fillInStackTrace(): Fills the stack trace of this throwable with the current stack trace, as
determined by the current thread.
● initCause(Throwable): Initializes the cause of this throwable to the specified value.
● toString(): Returns a string representation of this throwable.

These fields and methods can be used to obtain information about an exception, such as its
message, stack trace, and cause. They can also be used to handle exceptions in a more
informative and robust manner.

The code outputs a detailed message to the user by retrieving the message from the
exception’s getMessage method.
try {
File file = new File("data.txt");
Scanner scanner = new Scanner(file);
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {

3/18
System.out.println("File not found: " + e.getMessage());
}

The finally block is used to specify code that will always execute after a try block, regardless of
whether an exception was thrown or caught. This guarantees the proper management and
cleanup of resources, even when errors occur. You should ideally clean up resources here. Its
execution is guaranteed.
try {
// Code that may throw an exception
} catch (ExceptionType e) {
// Code to handle the exception
} finally {
// Code that will always execute
}

An example of a finally block to close the opened file. We should close the file no matter what.
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
System.out.println(reader.readLine());
} catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("Failed to close reader: " + e.getMessage());
}
}
System.out.println("Cleanup complete.");
}

Extending Exceptions
You can even create your own exceptions by extending an Exception. For example,
IllegalArgumentException is used to indicate that a method has been passed an argument that
is not valid. We can create a custom exception named AgeNotInRangeException by extending
IllegalArgumentException. The new exception provides a more descriptive, specific error for age
range violations.
public class AgeNotInRangeException extends IllegalArgumentException {
private int age;

// Constructor with a custom message

4/18
public AgeNotInRangeException(String message) {
super(message);
}

// Constructor with a custom message and the invalid age


public AgeNotInRangeException(String message, int age) {
super(message);
this.age = age;
}

// Getter for the invalid age


public int getAge() {
return age;
}

// Override toString() for a custom exception description


@Override
public String toString() {
return "AgeNotInRangeException: " + getMessage() + " (Age: " + age + ")";
}
}

In a method where we need to set an age, we can throw this exception as shown below:
public void setAge(int age) {
if (age < 0 || age > 100) {
throw new AgeNotInRangeException("Age must be between 0 and 100.", age);
}
System.out.println("Age set to: " + age);
}

GENERICS
When students learn programming, they typically start with arrays, which are fixed-size and
non-generic (do not worry what this means, we will now study generics) data structures. These
limitations naturally lead to the introduction of more flexible collections like ArrayList, HashMaps,
HashSet in Java, which are both generic and dynamic.

Arrays are fixed sized and type-safe. Once created, you cannot change the size unless you
create a copy, which is indeed another array. You cannot add more elements and mix types.

int [ ] elements = new int[3]; // cannot shrink or grow. The type is int
String [ ] names = = {“Ali”, “Mehmet”, “Can”}; // cannot shrink or grow. The type is String
elements [0] = 1;
elements [1] = 2;
elements [2] = 3; // cannot add any further. You must manually copy the contents
// it is type-safe. You cannot work with mixed types

5/18
elements [0] = “ali”; → illegal

Since every type is Object, you can create an Object array to mix the types. However, you need
an explicit cast to perform type-specific operations. But it is unsafe because typecast may lead
to a runtime error caused by type mismatches (ClassCastException)..

Object [ ] elements = new Object [3]; // cannot shrink or grow. The type is Object
elements [0] = “murat”;
elements [1] = 2;
elements [2] = “Can”;
// type casting is necessary. But it is unsafe
String myName = (String)elements [0]; // not a runtime error
String lastName = (String)elements [1]; → runtime error, ClassCastException

The moral is we detect errors in runtime. Generics let us detect errors in compile time. The
concept of generics refers to the use of parameterized types. Parameterized types are
important because they allow the creation of classes, interfaces, and methods that can
operate on various data types specified as parameters. Such classes, interfaces, or
methods that work with type parameters are referred to as generic.

With this capability (parameterized types), the compiler can substitute parameterized types
with concrete types.

Before defining our own generic classes or methods. Let’s see some built-in generics. We have
just discussed the arrays, which suffers from being fixed size and non-generic. Java has already
some built-in collections, which do not suffer from these. A popular example is ArrayList. It can
be dynamically sized, type safe, generic, and has many built-in functionality for collection
operations. You can define a generic ArrayList with the following syntax:
ArrayList<String> list = new ArrayList<>();

Here, the parameterized type is String. We are actually declaring that we want to work with the
String type. The parameterized type is placed between the angle brackets, < >. <String>
defines the type the ArrayList will work with.
// Using ArrayList
ArrayList<String> names = new ArrayList<>();
names.add("Ali");
names.add("Ahmet");
names.add("Bilal");
.
.
.
names.add("Zehra"); // you can create as many as. Dynamic sizing.

6/18
You can create another ArrayList object to work with a different type:
// Using ArrayList
ArrayList<Integer> numbers = new ArrayList<>(); // notice type is Integer not int
numbers .add(1);
numbers .add(2);
numbers .add(3);
.
.
.
numbers .add(1024); // you can create as many as. Dynamic sizing.
Integer i = numbers.get(0); // no casting is needed.

A short pause for WrapperClasses


However, there is one thing that you must be very careful of. Notice the parameterized type
associated with the ArrayList is Integer, it is not int. Generics require objects/instances, not
primitives, because they are designed to work with reference types. A primitive type cannot
have instances. Instead, Java uses wrapper classes to represent primitives as objects (e.g.,
Integer for int). Java provides wrapper classes for all primitive data types: int → Integer,
double → Double, char → Character, etc.

You can create an object of a Wrapper class with the new operator or by directly assigning a
value. However, creating an instance with the new operator is deprecated after Java 9. So, it is
a better idea to avoid it.
Double d = new Double(13.2); → deprecated since version Java 9
Double e = 13.2;

Automatic conversion of a primitive type to its corresponding wrapper class is called


autoboxing. Automatic conversion of a wrapper class back to its primitive type is called
unboxing.
Integer myInt = 12; // this is autoboxing
int x = myInt; // this is unboxing

An important question should pop in your mind: “if wrappers are reference types, should i use
equals method instead of == operator to compare two instances of it for the value equality?”
→ The answer is YES. Avoid using the == operator to compare values for equality. For instance,
with Integer objects, the == operator may yield accurate results for small values, but this
behavior does not hold for large values. The JVM caches objects representing small integer
values (usually within the range of -128 to 127). Consequently, the == operator might return true
in such cases, even though the objects being compared are distinct.

Integer i1 = 5;
Integer i2 = 5;
i1 == i2; → returns true in my jShell
i1 = 10293;

7/18
i2 = 10293;
i1 == i2; → returns false in my jShell

So, use the equals method.


Integer i1 = 5;
Integer i2 = 5;
i1.equals(i2); → always returns true, consistent
i1 = 10293;
i2 = 10293;
i1.equals(i2); → always returns true, consistent

Self study:
ArrayList (collections in general) has many built-in functionalities as Wrapper classes do. Our
main focus is generics now. So, please explore them.

*Back to our discussion on generics


Defining generic classes and interfaces
We can define a generic type for classes and interfaces. Below is an example for both. Here,
<T> represents the generic type. Generally, people use E or T to signify a generic type.
// GenericClass.java
public class GenericClass<T>{
private T value;
public void set(T value) {
this.value = value;
}

public T get() {
return value;
}
}
// GenericInterface.java
public interface GenericInterface<T> {
void add(T item);
T get(int index);
int size();
}

When creating an instance, you must be giving the concrete type. Below, we specify the
concrete type as Integer.
GenericClass <Integer> myGenericClass = new GenericClass<>();

A generic example: LinkedList

8/18
Before starting: Java provides a built-in LinkedList class as part of the java.util package. We will
name our implementation as CustomLinkedList since this is a handcrafted version designed for
educational purposes. This name distinguishes itself from the standard library.

Let’s revise LinkedList: LinkedList is an ADT (abstract data type) that consists of a sequence of
elements, called nodes, where each node contains data and a reference/pointer to the next
node in the sequence. We will use a single directional list. Operations we will provide are listed
below:
● Add: Inserts elements at the end of the list.
● Remove: Deletes a specific element by value.
● Size: Returns the number of elements.
● IsEmpty: Checks if the list is empty.
● PrintList: Displays the elements in sequence.
● Get: Retrieves an element at a specific index.

Nodes
A single element will be represented as a node. We will create a Node class for it. Obviously,
this class should have a field for the data and a reference/pointer to the next node. We want our
Node class to be generic.
public class Node<T> {
private T data;
private Node<T> next; //reference/pointer to the next Node

// Constructor
public Node(T data) {
this.data = data;
this.next = null;
}

// Getter for data


public T getData() {
return data;
}

// Setter for data


public void setData(T data) {
this.data = data;
}

// Getter for next


public Node<T> getNext() {
return next;
}

// Setter for next


public void setNext(Node<T> next) {
this.next = next;

9/18
}
}
The Node class has a field named data. This field holds the value stored in the node. It is
generic, allowing the Node to store any type of data (T). So, our LinkedList will be versatile
and reusable for different data types. The second field is called next. This field is a
reference/pointer to the next Node in the linked list. If the node is the last in the sequence, this
reference should be null, indicating the end of the list. The Node constructor initializes the node
with a given value (data) and sets the next reference to null. Notice how parameterized types
are utilized throughout the code. For example, the constructor accepts a type that will be
determined at runtime.

CustomLinkedList class
The CustomLinkedList class is a generic implementation of a singly linked list. It provides a
dynamic (by adding and removing elements), linear data structure (singly linked) where
elements are connected through nodes. This class encapsulates the essential operations to
manage a linked list, such as adding, removing, and accessing elements.
import java.util.Iterator;

public class CustomLinkedList<T>{


private Node<T> head;
private int size;

// Constructor
public CustomLinkedList() {
this.head = null;
this.size = 0;
}

public Node<T> getHead() {


return head;
}

// Private setter for head (only for internal control)


private void setHead(Node<T> head) {
this.head = head;
}

// Add an element to the end of the list


public void add(T data) {
Node<T> newNode = new Node<>(data);
if (isEmpty()) { // if head is null, this node is the head
setHead(newNode);
} else {
Node<T> current = getHead();
while (current.getNext() != null) {// we are appending, so traverse all.
current = current.getNext();
}

10/18
current.setNext(newNode); // set this node as the last
}
size++; // increase the size (the number of elements)
}

// Remove an element by value (first occurrence)


public boolean remove(T data) {
if (isEmpty()) {
return false;
}

if (getHead().getData().equals(data)) { // if the one we will remove is the head


setHead(getHead().getNext()); // change the head to the next
size--;
return true;
}
// traverse the linked list to locate the one we want to remove
Node<T> current = getHead();
while (current.getNext() != null && !current.getNext().getData().equals(data)) {
current = current.getNext();
}
// could not find it. It is not in our linked list
if (current.getNext() == null) {
return false;
}
// we got it.
// Set the next of the current to the next of the next
// (this next is the one we want to remove).
current.setNext(current.getNext().getNext());
size--;
return true;
}

// Get the size of the list


public int size() {
return size;
}

// Check if the list is empty


public boolean isEmpty() {
return size == 0; // return getHead() == null;
}

// Print the elements of the list


public void printList() {
Node<T> current = getHead();
while (current != null) {
System.out.print(current.getData() + " -> ");
current = current.getNext();
}

11/18
System.out.println("null");
}

// Get an element by index


public T get(int index) throws IndexOutOfBoundsException{
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
}

Node<T> current = getHead();


for (int i = 0; i < index; i++) {
current = current.getNext();
}
return current.getData();
}
}

The CustomLinkedList has a private reference/pointer to the first node in the list called head. It
is the entry point for traversing the linked list. If the list is empty, head is null. The class also
has a private integer field called size that keeps track of the number of elements in the list. It
ensures efficient size retrieval without requiring traversal of the entire list. The class constructor
initializes an empty linked list. It sets head to null and size to 0, meaning that the list is initially
empty.
The associated methods are summarized as follows:
● add: The add method inserts an element at the end of the list. It creates a new Node
with the provided data and sets head to the new node if the list is empty. Otherwise,
traverses to the last node and updates its next reference to point to the new node.
Finally, it increments the size counter to keep track of the number of elements in the list.
● remove: The remove method deletes a node containing the specified data. It updates
head if the element to be removed is the first node. Otherwise, traverses the list to
locate the node before the target (the one to be removed) and adjusts its next
reference to bypass the target node (by getting the target's next). Only if the element is
found and removed, the size counter is decremented.
● get: The get method retrieves the data of a node at a given index. It ensures the index is
within bounds and traverses the list to the desired position.
● size: The size method returns the number of elements in the list.
● isEmpty: The isEmpty method checks if the list is empty by comparing size to 0.
● printList: The printList method traverses the list from head to the last node, printing the
data of each node followed by an arrow (->) to indicate the connection.

By using generics (<T>), the CustomLinkedList class is highly flexible, allowing it to store and
manage any type of data. This generic design makes the class reusable across different use
cases without being tied to a specific data type.

QA session

12/18
Q: Why are we using the equals() method instead of == in the remove method?
→As you know, the == operator checks reference equality. The equals method checks value
equality. By default, the equals method in the Object class performs the same operation as ==.
However, many classes (e.g., String, Integer or the classes you have created) override it to
compare the actual content or value of objects rather than their memory locations. Remember
that the Node class is generic (<T>), meaning it can store any type of object. For custom
objects or wrapper classes like String or Integer, you typically want to compare values, not
references. Using equals ensures that the comparison is based on the logical equality defined
for the type T.

Q: Then, should we override the equals method in Node?


→ In our implementation, there is no need to override equals in the Node class. The
comparison is performed on the data stored in the node, not the Node object itself (check the
remove method and verify that we are actually comparing data). The data field uses the
generic type T, and the equals method will already work based on how T implements equals.
For example, if T is String, the String class’s overridden equals method will be used. If T is a
custom type you have defined, its equals method (if overridden) will determine equality.

Usage Example
public class App{
public static void main(String[] args) {
//A new CustomLinkedList of type Integer is created
CustomLinkedList<Integer> list = new CustomLinkedList<>();

// Add elements
list.add(10);
list.add(20);
list.add(30);
list.printList(); // Output: 10 -> 20 -> 30 -> null

// Remove an element
list.remove(20);
list.printList(); // Output: 10 -> 30 -> null

// Get size
System.out.println("Size: " + list.size()); // Output: Size: 2

// Get element by index


System.out.println("Element at index 1: " + list.get(1)); // Output: Element at index 1: 30

// Check if empty
System.out.println("Is empty: " + list.isEmpty()); // Output: Is empty: false

// Create a CustomLinkedList of Strings


CustomLinkedList<String> nameList = new CustomLinkedList<>();

// Add elements to the list

13/18
nameList.add("Alice");
nameList.add("Bob");
nameList.add("Charlie");
nameList.printList(); // Output: Alice -> Bob -> Charlie -> null

// Remove an element
nameList.remove("Bob");
nameList.printList(); // Output: Alice -> Charlie -> null

// Add another element


nameList.add("Diana");
nameList.printList(); // Output: Alice -> Charlie -> Diana -> null

// Get an element by index


System.out.println("First name: " + nameList.get(0)); // Output: First name: Alice

// Get the size of the list


System.out.println("Number of names: " + nameList.size()); // Output: Number of names: 3

// Check if the list is empty


System.out.println("Is the list empty? " + nameList.isEmpty()); // Output: Is the list empty?
false
}
}

An advanced example to use our CustomLinkedList in a loop.


By creating CustomLinkedList, we now have a data structure that can dynamically manage its
elements. It can contain many elements, so traversing our CustomLinkedList objects will
eventually become a necessity. We can traverse a CustomLinkedList object through for, while,
and do-while loops as you can imagine. A user of our CustomLinkedList can simply traverse its
elements as demonstrated below.
CustomLinkedList<Integer> numberList = new CustomLinkedList<>();

// Add elements
numberList.add(10);
numberList.add(20);
numberList.add(30);
for (int index = 0; index < numberList.size(); index++) {
Integer number = numberList.get(index);
System.out.println(number);
}

Notice the above code that the user of our code needs to know how to get the length of the
CustomLinkedList, and how to get an element from it. These two functionalities are required to
traverse the CustomLinkedList. At this point, remember that we used to have another loop type
in Java that supports collections, which is foreach. It is designed to simplify code by abstracting
the traversal logic, focusing on the action to be performed (this refers to what are you going to

14/18
do with the elements in the collection) on each element rather than the mechanics of iteration
(this term refers to the requirements to traverse the collection. In our case, it is to get the length
of the collection and get an element out of it).

Let’s try to use foreach with our CustomLinkedList to traverse it. The below code generates an
error.
CustomLinkedList<Integer> numberList = new CustomLinkedList<>();

// Add elements
numberList.add(10);
numberList.add(20);
numberList.add(30);
for (Integer number : numberList) { → ERROR: CANNOT PERFORM THIS RIGHT NOW
System.out.println(number);
}

You might be wondering what the problem is because we were using a very similar notation with
arrays and it used to work with arrays (by the way you can use foreach with ArrayList as well).
The reason is that foreach structure iterates over collections, arrays, or any other objects that
implement the Iterable interface.

What is the Iterable interface?


→ It is a built-in Java interface that allows an object to be the target of for-each loop. The
documentation link is https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html

If you go to the above link, you will see that it is a generic interface with only one abstract
method to be implemented (go to the method summary section and click abstract methods),
which is iterator() whose return type is Iterator<T>, as shown below.
Iterator <T> iterator();

If you have a look at default methods, you will see that there are already two default methods
implemented in this interface, which are given below.
default void forEach(Consumer<? super T> action)
default Spliterator<T> spliterator()

As you all know, we do not need to implement these two methods because their
implementations are already provided. By the way, the Consumer and SplitIterator are other
interface types, and we will soon (next week) discuss what <? super T> means in the foreach
method.

Now, we know that we need to implement the Iterable interface and the method we certainly
need to implement in this interface is iterator. Wait a minute! The return type of the iterator
method is a generic interface, Iterator <T>.

15/18
What is the Iterator interface?
→ It is in java.util (you will need to import it, import java.util.Iterator;). It is used to traverse
elements in a collection sequentially. It defines a standard protocol for accessing elements
one at a time, which is especially useful for collections like lists, sets, and custom data
structures. Documentation: https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html

If you go to the above link, you will see that it is a generic interface with two abstract
methods to be implemented (go to the method summary section and click abstract methods),
which are hasNext() and next() whose return type are boolean and E (generic), respectively,
as shown below (It has two default methods as well called, forEachRemaining and remove).
boolean hasNext()
E next()

We need to implement the Iterator interface just before we implement Iterable because the
iterator method of the Iterable will return an Iterator. Below is our implementation of Iterator
class:
import java.util.Iterator;

public class CustomLinkeListIterator <T> implements Iterator<T>{


// private field controlling the current node in our linked list
private Node<T> currentNode;

// constructor
// assign head
public CustomLinkeListIterator(Node<T> head) {
this.currentNode = head;
}

@Override
public boolean hasNext() {
return currentNode != null;
}

@Override
public T next() {
// calling next before hasNext will cause an exception
// if there is no next
if(!hasNext())
throw new java.util.NoSuchElementException();
T temp = currentNode.getData(); // get the current
currentNode = currentNode.getNext(); // make the current next anymore, move to next
return temp; // return the current
}

}
The CustomLinkeListIterator class is a generic implementation of the Iterator interface. This
iterator class (CustomLinkeListIterator) provides functionality to iterate through the nodes of a

16/18
linked list and access their data sequentially. It is parameterized with a type <T>, which ensures
type safety, allowing it to be used with any data type.
● The class has a single private field, currentNode, which keeps track of the current
position within the linked list. This field is initialized in the constructor with the head
node of the linked list (we will provide CustomLikedList head) and is updated as the
iterator moves through the list (next method).
● The constructor of the CustomLinkeListIterator takes a Node<T> object, representing
the head of the linked list. This head node will be the starting point for iteration.
● The hasNext method determines whether there are more elements to iterate over in
the linked list. It returns true if currentNode is not null, indicating that the iterator has
not yet reached the end of the list. If currentNode is null, it signifies that there are no
more nodes to process. So, the method returns false.
● The next method retrieves the data stored in the current node and advances the
iterator to the next node in the linked list. Before accessing the data, the method checks
whether the next element exists by calling hasNext. If no elements are left, it throws a
NoSuchElementException to signal that the end of the list has been reached. Otherwise,
it fetches the data from the currentNode, updates currentNode to point to the next node,
and returns the retrieved data.

Now that we have completed our implementation of Iterator, let’s go back to our
CustomLinkedList class to implement Iterable. Below is the implementation:
import java.util.Iterator;

public class CustomLinkedList<T> implements Iterable<T>{

// ALL THE PREVIOUS CODE HERE REMAINS SAME.


// I DIDN’T INCLUDE THEM TO SAVE SOME SPACE

@Override
public Iterator<T> iterator() {
return new CustomLinkeListIterator<T>(this.getHead());
}
}

The implementation code for the iterator method returns an Iterator instance,
CustomLinkeListIterator. That is all we need to do here. The method wants us to return an
instance of Iterator, so we returned it. It looks so easy that you might think there is something
missing, but nope, it is really that simple this time!

Let’s now use it in our foreach loop to see it runs (you try it yourself, I will just say it is OK). The
code just now traverses the CustomLinkedList instance without a problem and lists all the
numbers inside.
CustomLinkedList<Integer> numberList = new CustomLinkedList<>();

17/18
// Add elements
numberList.add(10);
numberList.add(20);
numberList.add(30);

for (Integer number : numberList) {


System.out.println(number); → Prints out 10 20 30 on each new line
}

What really happened was this actually:


CustomLinkedList<Integer> numberList = new CustomLinkedList<>();

// Add elements
numberList.add(10);
numberList.add(20);
numberList.add(30);

Iterator <Integer> linkedListInterator = numberList.iterator();

while(linkedListInterator.hasNext())
{
Integer number = linkedListInterator.next();
System.out.println(number);
}

An iterator is created for numberList by calling the iterator() method on it. The type
Iterator<Integer> indicates that the iterator will return Integer objects as it moves through the list.
The hasNext() method checks if there are more elements to be accessed in the list. If hasNext()
returns true, the next() method retrieves the next element from the list, which is stored in the
number variable. We have implemented an interface (Iterator) that defines the behavior within a
loop. Here, you can see that the above is not actually different from our own implementation.

18/18

You might also like