Abstract Classes, Interfaces, Inner Classes, Encapsulation, Callbacks, and 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.
It is a Partial Implementation of class.
Syntax:
abstract class ClassName {
// the body
}
What are abstract methods:
Example Code:
abstract class Class1 {
abstract int func1();
abstract void func2(int arg1, String arg2);
int func3() {
System.out.println("This is concrete method in abstract class");
return 0;
}
}
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.
Let's say Payment is an abstract class with two instance variables and one abstract method process() and one concrete method print. And two classes CreditCard and UPI extending the abstract class.
abstract class Payment {
double amount;
static final int MAX = 10;
Payment(double amount) {
this.amount = amount;
}
abstract void process();
void print() {
System.out.println("Amount paid: " + amount);
}
}
class CreditCard extends Payment {
CreditCard(double amt) {
super(amt);
}
void process() {
System.out.println("Processing Payment through a Credit Card");
}
}
class UPI extends Payment {
UPI(double amt) {
super(amt);
}
void process() {
System.out.println("Processing Payment through UPI");
}
}
public class Test {
public static void main(String[] args) {
Payment p1 = new CreditCard(5000);
Payment p2 = new UPI(4000);
p1.process(); // Through Credit Card
p1.print();
p2.process(); // UPI
p2.print();
}
}
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:
public interface Comparable {
public abstract int cmp(Comparable s);
}
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.
public class Circle extends Shape implements Comparable {
// Assuming radius is defined somewhere in this class or Shape
private double radius;
public double perimeter() {
return 2 * Math.PI * radius;
}
public int cmp(Comparable s) {
if (this.radius > ((Circle) s).radius) return 1;
if (this.radius < ((Circle) s).radius) return -1;
return 0;
}
}
An interface in Java is like a blueprint where all the methods are abstract and have no implementation. Unlike classes, a class can implement multiple interfaces, allowing it to inherit different functionalities without any conflicts. Other classes only need to know what the interface can do, not how it works, which helps keep the code simple and flexible.
The Comparable interface defines the comparison
behavior. The quicksort algorithm relies solely on
this capability, ignoring all other details of the objects.
interface Comparable {
public abstract int cmp(Comparable s);
}
class Myclass implements Comparable {
private double size;
public Myclass(double size) {
this.size = size;
}
public int cmp(Comparable s) {
if (s instanceof Myclass) {
Myclass other = (Myclass) s;
if (this.size < other.size) return -1;
else if (this.size == other.size) return 0;
else return 1;
}
return 0;
}
public double getSize() {
return size;
}
}
class SortFunctions {
public static void quicksort(Comparable[] a) {
quicksort(a, 0, a.length - 1);
}
private static void quicksort(Comparable[] a, int low, int high) {
if (low < high) {
int pivotIndex = partition(a, low, high);
quicksort(a, low, pivotIndex - 1);
quicksort(a, pivotIndex + 1, high);
}
}
private static int partition(Comparable[] a, int low, int high) {
Comparable pivot = a[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (a[j].cmp(pivot) < 0) {
i++;
Comparable temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
Comparable temp = a[i + 1];
a[i + 1] = a[high];
a[high] = temp;
return i + 1;
}
}
public class Main {
public static void main(String[] args) {
Myclass[] arr = {
new Myclass(3.5), new Myclass(1.2), new Myclass(4.7),
new Myclass(2.6), new Myclass(0.9)
};
System.out.println("Before sorting:");
for (Myclass m : arr) System.out.println(m.getSize());
SortFunctions.quicksort(arr);
System.out.println("\nAfter sorting:");
for (Myclass m : arr) System.out.println(m.getSize());
}
}
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:
public interface Comparable {
public static String cmpdoc() {
String s = "Return -1 if this < s, ";
s += "0 if this == s, ";
s += "+1 if this > s.";
return s;
}
}
// Can be called using: System.out.println(Comparable.cmpdoc());
Default methods enable interfaces to provide a basic implementation of certain methods. This allows developers to:
public interface Comparable {
public default int cmp(Comparable s) {
return 0;
}
}
public class Circle implements Comparable {
@Override
public int cmp(Comparable s) {
return -1; // Example: Circle is "less than" other objects
}
}
// Called via instance: c1.cmp(c2);
Java 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.
// Class defines a method
public class Person {
public String getName() {
return "No name";
}
}
// Interface defines a default method
public interface Designation {
public default String getName() {
return "No designation";
}
}
// Class extends Person and implements Designation
public class Employee extends Person implements Designation {
// Person's getName() method wins! No need to override.
}
// Output of new Employee().getName() will be "No name"
If the Employee class wants to define its own
behavior, it can override the method explicitly to resolve the
conflict with a fresh implementation.
When designing a LinkedList, its fundamental building
block is the Node. A Node represents an
individual element in the list and typically contains Data, a Next
Reference, and optionally a Previous Reference.
Node Be Private?LinkedList methods, not
the internal structure.
Node (e.g., adding prev for doubly
linked) don't impact the public API.
An inner class, like Node, can access all private
components of its enclosing class. Here is a full implementation:
class LinkedList {
private int size;
private Node first;
public Object head() {
return (first != null) ? first.data : null;
}
public void insert(Object newData) {
Node newNode = new Node(newData);
newNode.next = first;
if (first != null) {
first.prev = newNode;
}
first = newNode;
size++;
}
// Private inner class
private class Node {
public Object data;
public Node next;
public Node prev;
public Node(Object data) {
this.data = data;
this.next = null;
this.prev = null;
}
}
public int getSize() {
return size;
}
}
Encapsulation is one of the fundamental principles of OOP. It involves hiding internal data and providing controlled access via getters and setters to maintain object integrity.
Date Class ExampleBy encapsulating fields, we ensure invalid dates cannot be set.
class Date {
private int day;
private int month;
private int year;
public int getDay() { return day; }
public int getMonth() { return month; }
public int getYear() { return year; }
public void setDate(int day, int month, int year) {
if (isValidDate(day, month, year)) {
this.day = day;
this.month = month;
this.year = year;
} else {
System.out.println("Invalid date");
}
}
private boolean isValidDate(int day, int month, int year) {
if (year < 1 || month < 1 || month > 12) return false;
int maxDays;
switch (month) {
case 4: case 6: case 9: case 11: maxDays = 30; break;
case 2: maxDays = (isLeapYear(year)) ? 29 : 28; break;
default: maxDays = 31;
}
return day >= 1 && day <= maxDays;
}
private boolean isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
}
Imagine a system where people check train seat availability. To prevent spamming, we only allow logged-in users to query, and we limit how many queries they can make (e.g., 3 queries per login).
QueryObject if successful.
QIF interface
guarantees the object has a getStatus method.
import java.util.Date;
interface QIF {
int getStatus(int trainNo, Date date);
}
class BookingDB {
public static int getAvailableSeats(int trainNo, Date date) {
if (trainNo == 101) return 50;
if (trainNo == 102) return 30;
return 0;
}
}
class RailwayBooking {
private BookingDB railwayDB = new BookingDB();
private boolean validLogin(String username, String password) {
return "user".equals(username) && "pass".equals(password);
}
public QIF login(String username, String password) {
if (validLogin(username, password)) {
return new QueryObject();
}
return null;
}
private class QueryObject implements QIF {
private int numQueries = 0;
private static final int QLIM = 3;
public int getStatus(int trainNo, Date date) {
if (numQueries >= QLIM) {
System.out.println("Query limit reached.");
return -1;
}
numQueries++;
int seatsAvailable = BookingDB.getAvailableSeats(trainNo, date);
System.out.println("Seats for Train " + trainNo + ": " + seatsAvailable);
return seatsAvailable;
}
}
}
A callback is a way for one object (e.g., a
Timer) to notify another object that something has
happened.
The timer is strictly tied to Myclass. It cannot be
used by other classes without modifying the Timer.
class Timer implements Runnable {
private Myclass owner;
public Timer(Myclass o) { owner = o; }
public void start() { owner.timerdone(); }
public void run() { start(); }
}
We use Object but this requires risky casting (ClassCastException
risk).
class Timer implements Runnable {
private Object owner;
public Timer(Object o) { owner = o; }
public void start() { ((Myclass) owner).timerdone(); }
public void run() { start(); }
}
The best approach is an interface. The
Timerowner interface ensures any object passed in has
the timerdone() callback method.
interface Timerowner {
void timerdone();
}
class Myclass implements Timerowner {
public void f() {
Timer t = new Timer(this);
t.start();
}
public void timerdone() {
System.out.println("Timer is done!");
}
}
class Timer implements Runnable {
private Timerowner owner;
public Timer(Timerowner o) { owner = o; }
public void start() { owner.timerdone(); }
public void run() { start(); }
}
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.
LinearList
The Iter class implements Iterator,
starting at the list's head and moving through the nodes.
public interface Iterator {
boolean has_next();
Object get_next();
}
class Linearlist {
private Node head;
private class Node {
Object data;
Node next;
Node(Object data) { this.data = data; this.next = null; }
}
public void append(Object data) {
Node newNode = new Node(data);
if (head == null) { head = newNode; }
else {
Node temp = head;
while (temp.next != null) temp = temp.next;
temp.next = newNode;
}
}
private class Iter implements Iterator {
private Node position = head;
public boolean has_next() { return position != null; }
public Object get_next() {
if (has_next()) {
Object data = position.data;
position = position.next;
return data;
}
return null;
}
}
public Iterator get_iterator() { return new Iter(); }
}
class Main {
public static void main(String[] args) {
Linearlist l = new Linearlist();
l.append("Hello");
l.append("World");
Iterator i = l.get_iterator();
while (i.has_next()) {
System.out.println(i.get_next()); // Hello, World
}
}
}
If needed, you can create multiple iterators to traverse the list at different points simultaneously without them interfering with each other.