Home Week-2 Week-5

Java - Week 4

Week 4

Grouping together classes in Java

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.

Abstract Classes

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.

Generic functions

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.

Example

Note: Quicksort is a sorting algorithm. Don't worry about the algorithm, focus on the concept here.

Multiple Inheritance in Java

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.

Sorting Circle Objects

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.

Interfaces in Java

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:

A class that implements an interface is required to define all the methods declared in that interface.

Implementing the Comparable Interface

To make the Circle class sortable, we can implement the Comparable interface alongside extending the Shape class. Here’s how it is done:

Interface in more detail

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.

Using Comparable for Quicksort

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.

Comparable Interface

Example

Here is how quicksort can be implemented using the Comparable interface:

Adding Methods to Interfaces in Java

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

Static methods in interfaces allow defining utility or helper methods directly within the interface. These methods:

  1. Cannot access instance variables or methods of implementing classes.

  2. Are invoked directly using the interface name, rather than an instance of the implementing class.

Example

  1. The static method cmpdoc provides documentation for the comparison logic.

  2. It can be called directly using the interface name, like this:


Default Methods in Interfaces

Default methods enable interfaces to provide a basic implementation of certain methods. This allows developers to:

  1. Add new methods to interfaces without forcing all implementing classes to define them.

  2. Provide a default behavior that implementing classes can optionally override.

Example

  1. The cmp method here has a default implementation that always returns 0.

  2. If an implementing class does not override this method, it will inherit the default behavior.

  3. If needed, an implementing class can override the method with its own logic:

Default methods are invoked like regular instance methods using the object name:

Dealing with Conflicts in Java Interfaces

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 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.


Types of Conflicts

1. Conflict Between Static and Default Methods

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.

2. Conflict Between a Class and an Interface

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.

Example


Explanation

  1. 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.

  2. Default Method Ignored

    • The default method getName() from Designation is ignored unless the subclass (Employee) explicitly overrides it.

  3. Output Example

    • When the getName() method is called on an Employee object, it will invoke the method from the Person class:


Resolving Conflicts with Fresh Implementation

If the Employee class wants to define its own behavior for getName(), it can override the method explicitly.

Overridden Example

Output Example


Understanding Nested Objects and Inner Classes

When designing a LinkedList, its fundamental building block is the Node. A Node represents an individual element in the list and typically contains:

  1. Data: The value stored in the node.

  2. Next Reference: A pointer to the next node in the list.

  3. (Optional) Previous Reference: In the case of a doubly linked list, a reference to the previous node.

 

Why Should Node Be Private?

  1. 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).

  2. 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.

  3. 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.

 

Full Implementation of LinkedList with a Private Node Class

Here’s a detailed implementation based on the above discussion:


Interaction with state

Encapsulation is one of the fundamental principles of object-oriented programming (OOP). It involves:

  1. Hiding Internal Data: The internal state of an object (its data fields) is kept private to prevent unauthorized or unintended access from external code.

  2. 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.


Advantages of Encapsulation

  1. 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.

  2. 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.

  3. 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.

  4. 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.


The Date Class Example

The 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:


Controlled interaction with Object

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:

  1. Make sure only logged-in users can check train availability.

  2. Limit how many queries a user can make after logging in (e.g., 3 queries per login).


How do we achieve this?

We use objects to manage this process. Here's how it works:

1. Logging In

When a user logs in, we check if their username and password are correct:

If the login fails (e.g., wrong password), the user doesn’t get the object and can’t make any queries.


2. Using the Query Object

Once logged in, the user uses the QueryObject to check seat availability. This object:


3. Why Use an Interface?

An interface is like a contract that describes what a class (like QueryObject) can do. In our case:


4. Limiting Queries

To stop users from overusing the system:


How Does It Work Step-by-Step?

  1. A user tries to log in.

    • If the login is successful, they get a QueryObject.

    • If the login fails, they don’t get anything.

  2. 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.

  3. The system tracks everything to ensure fair usage.


Why Is This a Good Design?

  1. Encapsulation:

    • The RailwayBooking class handles login and controls access to the database.

    • The QueryObject class only focuses on querying the database.

  2. Security:

    • Users can’t access the database directly. They must log in first.

    • The query limit ensures that no one can overload the system.

  3. Reusability:

    • By using an interface (QIF), we can easily add new types of query objects in the future without changing the main program.


Code Example

Imagine It Like This

Think of a theme park:

  1. You need a ticket (login) to enter the park.

  2. After entering, you’re given a wristband (QueryObject) to ride the attractions.

  3. 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).


Callback in Java

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:


Step 1: A Timer Specific to Myclass

Here’s how a simple timer tied specifically to Myclass works:

  1. 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.

  2. Code:

  1. 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.


Step 2: Making the Timer Generic with Object

To make the timer more flexible:

Code:

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.


Step 3: Using Interfaces for Safety

To solve the casting problem and ensure type safety, we use an interface:


1. Define an Interface for the Callback

The interface (Timerowner) specifies what the callback should look like:


2. Modify Myclass to Implement the Interface

Now, Myclass implements the Timerowner interface, meaning it promises to provide the timerdone() method:


3. Modify Timer to Use the Interface

The Timer now expects its owner to be a Timerowner. This ensures that the owner has a timerdone() method, removing the need for risky casting:

Java Iterator

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.


Linear List Example

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.

LinearList Class


Iterator Interface

The Iterator interface defines two key methods:


Implementing Iterator in LinearList

The Iter class implements Iterator, starting at the list's head and moving through the nodes.

Using Multiple Iterators

If needed, you can create multiple iterators to traverse the list at different points.