Inheritance and polymorphism

Inheritance 

Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows one class to acquire the properties and behaviors (methods) of another class. It promotes code reusability, modularity, and hierarchy in Java programming

Types of Inheritance in Java

1. Single Inheritance
A subclass (child class) inherits from a single superclass (parent class).

Example:

class Animal {  
    void sound() {  
        System.out.println("Animals make sounds");  
    }  
}  

class Dog extends Animal {  
    void bark() {  
        System.out.println("Dog barks");  
    }  
}  

public class Main {  
    public static void main(String[] args) {  
        Dog d = new Dog();  
        d.sound(); // Inherited method  
        d.bark();  
    }  
}

2. Multilevel Inheritance

A class inherits from another class, which in turn inherits from another class.

Example:

class Animal {  
    void sound() { System.out.println("Animals make sounds"); }  
}  

class Mammal extends Animal {  
    void walk() { System.out.println("Mammals walk"); }  
}  

class Dog extends Mammal {  
    void bark() { System.out.println("Dog barks"); }  
}  

public class Main {  
    public static void main(String[] args) {  
        Dog d = new Dog();  
        d.sound();  
        d.walk();  
        d.bark();  
    }  
}


3. Hierarchical Inheritance

A single superclass has multiple child classes.

Example:

class Animal {  
    void sound() { System.out.println("Animals make sounds"); }  
}  

class Dog extends Animal {  
    void bark() { System.out.println("Dog barks"); }  
}  

class Cat extends Animal {  
    void meow() { System.out.println("Cat meows"); }  
}  

public class Main {  
    public static void main(String[] args) {  
        Dog d = new Dog();  
        d.sound();  
        d.bark();  

        Cat c = new Cat();  
        c.sound();  
        c.meow();  
    }  
}

Java does not support multiple inheritance (i.e., inheriting from multiple classes) to avoid ambiguity problems caused by the Diamond Problem.
Instead, Java provides interfaces to achieve multiple inheritance behavior.

Using super Keyword in Inheritance

👉The super keyword is used to refer to the immediate parent class object.

👉It can be used to call parent class methods or constructors.


Example:

class Animal {  
    Animal() {  
        System.out.println("Animal Constructor");  
    }  

    void sound() {  
        System.out.println("Animals make sounds");  
    }  
}  

class Dog extends Animal {  
    Dog() {  
        super(); // Calls parent constructor  
        System.out.println("Dog Constructor");  
    }  

    void sound() {  
        super.sound(); // Calls parent method  
        System.out.println("Dog barks");  
    }  
}  

public class Main {  
    public static void main(String[] args) {  
        Dog d = new Dog();  
        d.sound();  
    }  
}

Advantages of Inheritance

✔ Code reusability – Avoids code duplication.
✔ Enhances modularity – Simplifies program structure.
✔ Extensibility – Easy to add new features.
✔ Reduces maintenance effort – Common code is maintained in a single class.

Super and Subclass

✨In Java Inheritance, the concepts of superclass and subclass are fundamental.

✨Superclass (Parent Class): The class whose properties and methods are inherited by another class.

✨Subclass (Child Class): The class that inherits from another class. It can have additional properties and methods.

1. Defining a Superclass and Subclass

class Animal { // Superclass
    void eat() {
        System.out.println("This animal eats food");
    }
}

class Dog extends Animal { // Subclass
    void bark() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.eat(); // Inherited from Animal class
        d.bark(); // Defined in Dog class
    }
}

Output:

This animal eats food  
Dog barks

2. super Keyword in Java

The super keyword is used inside a subclass to refer to its immediate superclass. It is commonly used for:

1. Calling superclass methods

2. Accessing superclass fields

3. Calling superclass constructors

a) Using super to Call Superclass Methods

class Animal {
    void sound() {
        System.out.println("Animals make sounds");
    }
}

class Dog extends Animal {
    void sound() {
        super.sound(); // Calls superclass method
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.sound();
    }
}

Output:

Animals make sounds  
Dog barks


b) Using super to Access Superclass Fields

class Animal {
    String type = "Wild Animal";
}

class Dog extends Animal {
    String type = "Domestic Animal";

    void displayType() {
        System.out.println("Dog type: " + type); // Child class field
        System.out.println("Animal type: " + super.type); // Superclass field
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.displayType();
    }
}

Output:

Dog type: Domestic Animal  
Animal type: Wild Animal


c) Using super to Call Superclass Constructor

class Animal {
    Animal() {
        System.out.println("Animal Constructor");
    }
}

class Dog extends Animal {
    Dog() {
        super(); // Calls superclass constructor
        System.out.println("Dog Constructor");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
    }
}

Output:

Animal Constructor  
Dog Constructor


Advantages of Using Superclass and Subclass

✔ Code reusability – Reduces duplication.
✔ Extensibility – Easily add new functionalities.
✔ Polymorphism – Allows overriding methods for dynamic behavior.
✔ Better organization – Helps in creating a structured hierarchical


Method Overriding 

Method Overriding is an OOP feature in Java where a subclass provides a specific implementation of a method that is already defined in its superclass.

Key Rules for Method Overriding

1. Same Method Signature – The method in the subclass must have the same name, return type, and parameters as in the superclass.

2. Inheritance is Required – The subclass must extend the superclass.

3. Cannot Reduce Visibility – The overridden method cannot have a more restrictive access modifier (e.g., public method in the superclass cannot be overridden as private).

4. @Override Annotation (Optional but Recommended) – Helps prevent mistakes and improves code readability.

5. Final Methods Cannot Be Overridden – A method declared with final in the superclass cannot be overridden.

6. Static Methods Cannot Be Overridden – They are class-level methods, not instance-level.

7. Constructors Cannot Be Overridden – Because constructors are not inherited.

Example of Method Overriding

class Animal {
    void sound() {
        System.out.println("Animals make sounds");
    }
}

class Dog extends Animal {
    @Override
    void sound() { // Overriding the superclass method
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting
        a.sound(); // Calls the overridden method in Dog
    }
}

Output:

Dog barks

Using super to Call Superclass Method

If you need to access the superclass method inside the subclass, use super.methodName().

class Animal {
    void sound() {
        System.out.println("Animals make sounds");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        super.sound(); // Calls the superclass method
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.sound();
    }
}

Output:

Animals make sounds  
Dog barks

Overriding vs Overloading

Method Overriding with final, static, and private Methods

1. Final Methods Cannot Be Overridden

class Animal {
    final void sound() {
        System.out.println("Animals make sounds");
    }
}

class Dog extends Animal {
    // Compilation Error: Cannot override final method
    // void sound() { System.out.println("Dog barks"); }
}

2. Static Methods Are Not Overridden, They Are Hidden

class Animal {
    static void sound() {
        System.out.println("Animal makes sound");
    }
}

class Dog extends Animal {
    static void sound() { // Hides the method, does not override
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.sound(); // Calls Animal's method due to static binding
    }
}

Output:

Animal makes sound

3. Private Methods Are Not Inherited

class Animal {
    private void sound() {
        System.out.println("Animal makes sound");
    }
}

class Dog extends Animal {
    // This is NOT overriding; it's a new method
    void sound() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.sound(); // Calls Dog's method, not Animal's
    }
}

Output:

Dog barks

Using @Override Annotation

Although not required, it's recommended to use @Override to avoid mistakes when overriding.

class Animal {
    void sound() {
        System.out.println("Animal makes sound");
    }
}

class Dog extends Animal {
    @Override // Ensures method is correctly overridden
    void sound() {
        System.out.println("Dog barks");
    }
}


Polymorphism and Method Overriding

Overriding enables runtime polymorphism, allowing Java to determine which method to call at runtime.

class Animal {
    void sound() {
        System.out.println("Animal makes sound");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        a1.sound(); // Calls Dog's sound() method
        a2.sound(); // Calls Cat's sound() method
    }
}

Output:

Dog barks  
Cat meows

Advantages of Method Overriding

✔ Supports Runtime Polymorphism – Helps achieve dynamic method dispatch.
✔ Increases Code Reusability – Enhances maintainability.
✔ Provides Specific Implementations – Allows customizing inherited methods.



Object Class 

In Java, the Object class is the superclass of all classes. Every class in Java implicitly inherits from java.lang.Object, either directly or indirectly.

Key Features of the Object Class

It is part of java.lang package.

✨It provides common methods that all Java objects can use.

✨If a class does not explicitly extend another class, it automatically extends Object.

Methods in the Object Class

The Object class provides several useful methods, which can be overridden in subclasses:

1. toString() Method

The toString() method returns a string representation of the object.
By default, it returns:
ClassName@hashcode

Example

class Car {
    String model;
    
    Car(String model) {
        this.model = model;
    }

    @Override
    public String toString() {
        return "Car model: " + model;
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car("Tesla Model S");
        System.out.println(car); // Implicitly calls car.toString()
    }
}

Output:

Car model: Tesla Model S

2. equals(Object obj) Method

👉Used to compare two objects.

👉Default implementation compares memory addresses (reference comparison).

✨Should be overridden to compare object properties.


Example (Default vs. Overridden equals)

class Car {
    String model;
    
    Car(String model) {
        this.model = model;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true; // Same reference
        if (obj == null || getClass() != obj.getClass()) return false;
        
        Car car = (Car) obj;
        return model.equals(car.model);
    }
}

public class Main {
    public static void main(String[] args) {
        Car c1 = new Car("Tesla Model S");
        Car c2 = new Car("Tesla Model S");
        
        System.out.println(c1.equals(c2)); // true (after overriding)
    }
}

Output:

true


3. hashCode() Method

👉Returns an integer hash code for the object.

👉Used in hash-based collections like HashMap, HashSet.

👉Should be overridden along with equals().


Example

class Car {
    String model;
    
    Car(String model) {
        this.model = model;
    }

    @Override
    public int hashCode() {
        return model.hashCode();
    }
}

public class Main {
    public static void main(String[] args) {
        Car c1 = new Car("Tesla Model S");
        Car c2 = new Car("Tesla Model S");

        System.out.println(c1.hashCode() == c2.hashCode()); // true
    }
}



4. getClass() Method
Returns the runtime class of an object.


Example

class Car {}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        System.out.println(car.getClass().getName()); // Outputs: Car
    }
}

5. clone() Method

👉Used to create a copy of an object.

👉Requires the Cloneable interface.


Example

class Car implements Cloneable {
    String model;

    Car(String model) {
        this.model = model;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Car c1 = new Car("Tesla");
        Car c2 = (Car) c1.clone();
        
        System.out.println(c1.model); // Tesla
        System.out.println(c2.model); // Tesla
    }
}


6. finalize() Method

👉Called by the Garbage Collector before destroying an object.

👉Not recommended for resource cleanup (use try-with-resources instead).


Example

class Car {
    @Override
    protected void finalize() {
        System.out.println("Car object is being destroyed");
    }
}

public class Main {
    public static void main(String[] args) {
        Car c = new Car();
        c = null;
        System.gc(); // Requests garbage collection
    }
}


7. wait(), notify(), notifyAll() Methods

Used in multithreading to handle inter-thread communication.

Example (Using wait() and notify())

class SharedResource {
    synchronized void produce() throws InterruptedException {
        System.out.println("Producing...");
        wait(); // Releases the lock and waits
        System.out.println("Resumed after notification");
    }

    synchronized void consume() {
        System.out.println("Consuming...");
        notify(); // Notifies the waiting thread
    }
}

public class Main {
    public static void main(String[] args) {
        SharedResource sr = new SharedResource();

        new Thread(() -> {
            try {
                sr.produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> sr.consume()).start();
    }
}


Why is Object Class Important?

✔ Universal Superclass – Every Java class extends Object.
✔ Provides Common Methods – Methods like toString(), equals(), and hashCode() are essential for object manipulation.
✔ Supports Polymorphism – Enables generic programming.


Dynamic Binding 

Dynamic Binding (also known as Late Binding or Runtime Polymorphism) in Java refers to the process where method calls are resolved at runtime instead of compile-time. This allows method overriding to work dynamically.


Key Characteristics of Dynamic Binding

✔ Happens at runtime.
✔ Used in method overriding.
✔ Uses upcasting (superclass reference pointing to subclass object).
✔ Achieved through dynamic method dispatch (method resolution happens dynamically).

Example of Dynamic Binding

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a; // Superclass reference

        a = new Dog(); // Upcasting
        a.sound(); // Calls Dog's sound() method (Dynamic Binding)

        a = new Cat(); // Upcasting
        a.sound(); // Calls Cat's sound() method (Dynamic Binding)
    }
}

Output:

Dog barks  
Cat meows

How It Works?

💫Animal a = new Dog(); → a is of type Animal, but it holds a Dog object.

💫When a.sound() is called, Java resolves the method at runtime and executes Dog's sound() method.

💫Similarly, when a refers to a Cat object, the Cat's sound() method is executed.

Compile-time Binding vs. Runtime Binding


Example of Compile-time vs. Runtime Binding

class CompileTimeBinding {
    void show(int a) {
        System.out.println("Integer: " + a);
    }

    void show(double a) {
        System.out.println("Double: " + a);
    }
}

class RunTimeBinding {
    void display() {
        System.out.println("Superclass method");
    }
}

class SubClass extends RunTimeBinding {
    @Override
    void display() {
        System.out.println("Overridden method in subclass");
    }
}

public class Main {
    public static void main(String[] args) {
        // Compile-time Binding (Overloading)
        CompileTimeBinding obj1 = new CompileTimeBinding();
        obj1.show(5); // Integer version
        obj1.show(5.5); // Double version

        // Runtime Binding (Overriding)
        RunTimeBinding obj2 = new SubClass(); // Upcasting
        obj2.display(); // Calls subclass method dynamically
    }
}

Output:

Integer: 5  
Double: 5.5  
Overridden method in subclass


Why Use Dynamic Binding?

✔ Supports Runtime Polymorphism – Enables flexible code execution.
✔ Reduces Code Coupling – Allows using superclass references for various subclass objects.
✔ Extensible Design – New subclasses can be added without modifying existing code.


Example: Real-world Scenario

Imagine a Payment system where different payment methods (CreditCard, PayPal, UPI) are processed using a common interface.

class Payment {
    void pay() {
        System.out.println("Processing payment...");
    }
}

class CreditCard extends Payment {
    @Override
    void pay() {
        System.out.println("Payment done using Credit Card");
    }
}

class PayPal extends Payment {
    @Override
    void pay() {
        System.out.println("Payment done using PayPal");
    }
}

public class Main {
    public static void main(String[] args) {
        Payment p;

        p = new CreditCard();
        p.pay(); // Calls CreditCard's pay() method

        p = new PayPal();
        p.pay(); // Calls PayPal's pay() method
    }
}

Output:

Payment done using Credit Card  
Payment done using PayPal

Here, Payment reference can call different payment methods dynamically at runtime.


Conclusion

💫Dynamic Binding enables method overriding in Java.

💫It allows runtime polymorphism, making code more flexible and maintainable.

💫Achieved using superclass references and overridden methods.



Generic Programming 

Generic Programming in Java allows writing code that is flexible, reusable, and type-safe. It enables defining classes, interfaces, and methods with type parameters, which means they can work with any data type without specifying it in advance.

Why Use Generics?

✔ Type Safety – Prevents ClassCastException at runtime.
✔ Code Reusability – One class/method works with multiple data types.
✔ Compile-time Checking – Errors are caught at compile time rather than runtime.
✔ Eliminates Type Casting – No need for explicit type conversions.

1. Generic Classes
A generic class uses a type parameter (T), which stands for "Type".

Example: Generic Box Class

class Box<T> { // Generic class with type parameter T
    private T value;

    void setValue(T value) {
        this.value = value;
    }

    T getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>(); // Box for Integer
        intBox.setValue(10);
        System.out.println(intBox.getValue()); // Output: 10

        Box<String> strBox = new Box<>(); // Box for String
        strBox.setValue("Hello");
        System.out.println(strBox.getValue()); // Output: Hello
    }
}

Here, T is replaced with Integer and String at runtime.

2. Generic Methods
A generic method allows type parameters inside methods instead of whole classes.

Example: Generic Print Method

class GenericMethodExample {
    // Generic method with type <T>
    static <T> void printArray(T[] arr) {
        for (T element : arr) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

public class Main {
    public static void main(String[] args) {
        Integer[] intArr = {1, 2, 3};
        String[] strArr = {"A", "B", "C"};

        GenericMethodExample.printArray(intArr); // Output: 1 2 3
        GenericMethodExample.printArray(strArr); // Output: A B C
    }
}


3. Generic Interfaces
A generic interface allows defining reusable data structures like collections.

Example: Generic Comparable Interface

interface MinMax<T extends Comparable<T>> {
    T min();
    T max();
}

class Numbers<T extends Comparable<T>> implements MinMax<T> {
    private T[] values;

    Numbers(T[] values) {
        this.values = values;
    }

    @Override
    public T min() {
        T min = values[0];
        for (T val : values) {
            if (val.compareTo(min) < 0) min = val;
        }
        return min;
    }

    @Override
    public T max() {
        T max = values[0];
        for (T val : values) {
            if (val.compareTo(max) > 0) max = val;
        }
        return max;
    }
}

public class Main {
    public static void main(String[] args) {
        Integer[] numbers = {3, 5, 1, 9, 2};
        Numbers<Integer> obj = new Numbers<>(numbers);

        System.out.println("Min: " + obj.min()); // Output: Min: 1
        System.out.println("Max: " + obj.max()); // Output: Max: 9
    }
}

Here, the MinMax interface is generic, and the Numbers class implements it for any Comparable type.

4. Bounded Type Parameters (extends)

Sometimes, we want a type parameter to extend a specific class or implement an interface.

Example: Bounded Type (extends Number)

class Calculator<T extends Number> { // T must be a subclass of Number


Casting Objects 

Object Casting in Java refers to converting one type of object reference to another. It is mainly used in inheritance when working with superclasses and subclasses.

Types of Object Casting

1. Upcasting (Implicit Casting)

💫Converting a subclass reference into a superclass reference.

💫Automatic and safe (No explicit cast needed).

2. Downcasting (Explicit Casting)

✨Converting a superclass reference back to a subclass reference.

✨Needs explicit casting and should be done carefully using instanceof.

1. Upcasting (Implicit Casting)

✔ Automatically converts a subclass reference into a superclass reference.
✔ The object still retains its subclass behavior but is limited to superclass methods.

Example of Upcasting

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting (Implicit)
        a.sound(); // Allowed (Inherited method)
        // a.bark(); // Not Allowed (Compile-time error)
    }
}

Output:

Animal makes a sound

Here, Dog is upcasted to Animal. The bark() method is not accessible because a is treated as an Animal.

2. Downcasting (Explicit Casting)

✔ Converting a superclass reference back into a subclass reference.
✔ Requires explicit casting (Subclass) reference.
✔ Should be used only if the object was originally of the subclass type.
✔ Risky – If downcasting is done incorrectly, it throws ClassCastException.

Example of Downcasting

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting (Implicit)
        Dog d = (Dog) a; // Downcasting (Explicit)
        d.bark(); // Allowed now
    }
}

Output:

Dog barks

✔ Downcasting works correctly because a was originally a Dog.

3. Preventing ClassCastException using instanceof

If the object is not actually of the subclass type, downcasting can cause runtime errors (ClassCastException).
✔ Always check with instanceof before downcasting.

Example: Safe Downcasting with instanceof

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    void meow() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting

        if (a instanceof Dog) {
            Dog d = (Dog) a; // Safe Downcasting
            d.bark();
        } else {
            System.out.println("Downcasting not possible");
        }
    }
}

Output:

Dog barks

✔ instanceof ensures safe downcasting and prevents ClassCastException.

4. Casting Objects in Hierarchy

✔ Casting only works within the same inheritance hierarchy.
✔ Unrelated classes cannot be cast.

Incorrect Downcasting (Throws Exception)

public class Main {
    public static void main(String[] args) {
        Animal a = new Cat(); // Upcasting

        Dog d = (Dog) a; // ERROR: Cat cannot be cast to Dog
        d.bark();
    }
}

Error:

Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog

✔ Fix: Always check with instanceof before casting.

5. Object Casting with Interfaces

✔ An interface reference can refer to any class that implements it.
✔ Upcasting works automatically when storing an object in an interface reference.
✔ Downcasting requires explicit casting.

Example: Casting with Interfaces

interface Animal {
    void sound();
}

class Dog implements Animal {
    public void sound() {
        System.out.println("Dog barks");
    }
    
    void guard() {
        System.out.println("Dog guards the house");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting (Implicit)
        a.sound();
        
        if (a instanceof Dog) {
            Dog d = (Dog) a; // Downcasting (Explicit)
            d.guard(); // Now allowed
        }
    }
}

Output:

Dog barks
Dog guards the house

✔ Upcasting works automatically with interfaces.
✔ Downcasting requires instanceof to avoid errors.


instanceof Operator 

The instanceof operator in Java is used to check whether an object is an instance of a specific class or a subclass of that class. It helps prevent ClassCastException when performing downcasting.

Syntax:

objectReference instanceof ClassName

✔ Returns true if objectReference is an instance of ClassName or its subclass.
✔ Returns false if objectReference is null or belongs to an unrelated class.

1. Basic Example of instanceof

class Animal { }

class Dog extends Animal { }

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting

        System.out.println(a instanceof Animal); // true
        System.out.println(a instanceof Dog); // true
    }
}

✔ a instanceof Animal → true because Dog is a subclass of Animal.
✔ a instanceof Dog → true because a refers to a Dog object.

2. Using instanceof to Prevent ClassCastException (Safe Downcasting)

class Animal { }

class Dog extends Animal {
    void bark() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal { }

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting

        if (a instanceof Dog) { // Check before downcasting
            Dog d = (Dog) a; // Safe downcasting
            d.bark();
        }

        Animal b = new Cat();
        if (b instanceof Dog) { // Prevents ClassCastException
            Dog d = (Dog) b; 
            d.bark();
        } else {
            System.out.println("b is not an instance of Dog");
        }
    }
}

✔ The first downcasting (Dog d = (Dog) a) works safely.
✔ The second one (Dog d = (Dog) b) is prevented because b is a Cat, not a Dog.

Output:

Dog barks
b is not an instance of Dog

3. instanceof with Interfaces

✔ instanceof can check if an object implements an interface.

Example: Checking Interface Implementation

interface Animal { }

class Dog implements Animal { }

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        
        System.out.println(d instanceof Animal); // true
        System.out.println(d instanceof Dog); // true
    }
}

✔ d instanceof Animal → true because Dog implements Animal.
✔ d instanceof Dog → true because d is a Dog object.

4. instanceof with Null Objects

✔ If the object is null, instanceof always returns false.

Example: null Check

class Animal { }

public class Main {
    public static void main(String[] args) {
        Animal a = null;

        System.out.println(a instanceof Animal); // false
    }
}

✔ Prevents NullPointerException since null is never an instance of any class.

5. instanceof with Abstract Classes

✔ Works with abstract classes since objects are created from subclasses.

Example: Checking an Abstract Class

abstract class Animal { }

class Dog extends Animal { }

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting

        System.out.println(a instanceof Animal); // true
        System.out.println(a instanceof Dog); // true
    }
}

✔ Even though Animal is abstract, Dog is an instance of Animal.

6. instanceof in Inheritance Hierarchy

✔ Works for parent-child relationships in an inheritance tree.

Example: Checking Multiple Levels of Inheritance

class Animal { }

class Mammal extends Animal { }

class Dog extends Mammal { }

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();

        System.out.println(d instanceof Dog); // true
        System.out.println(d instanceof Mammal); // true
        System.out.println(d instanceof Animal); // true
    }
}

✔ Dog is an instance of Dog, Mammal, and Animal because of inheritance.


7. instanceof with Generic Classes

✔ Works even when using generics in Java.

Example: Generic Box

class Box<T> {
    private T value;

    Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>(10);
        
        System.out.println(intBox instanceof Box); // true
    }
}

✔ intBox instanceof Box → true because intBox is an object of Box<T>.

8. instanceof with Arrays

✔ Arrays can be checked using instanceof.

Example: Checking Array Types

public class Main {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        System.out.println(numbers instanceof int[]); // true
        System.out.println(numbers instanceof Object); // true
    }
}

✔ Arrays are instances of Object in Java.

When to Use instanceof?

✔ Before Downcasting – Prevents ClassCastException.
✔ Checking Interface Implementation – Ensures correct object type.
✔ Handling Null Objects – Avoids NullPointerException.
✔ Working with Collections – Verifies data types in lists/maps.


Abstract Class 

An abstract class in Java is a class that cannot be instantiated and is used as a blueprint for other classes. It can have abstract methods (without a body) and concrete methods (with implementation).

1. Key Features of Abstract Class

✔ Cannot be instantiated directly.
✔ Can have both abstract and concrete methods.
✔ Can have constructors, static methods, and final methods.
✔ Supports inheritance (subclasses must implement abstract methods or be abstract themselves).

2. Defining an Abstract Class

Use the abstract keyword:

abstract class Animal { 
    abstract void sound(); // Abstract method (No body)
    
    void eat() { // Concrete method (Has body)
        System.out.println("Animal is eating");
    }
}


3. Creating a Subclass

✔ A subclass must implement all abstract methods or be declared abstract itself.

Example: Implementing an Abstract Class

abstract class Animal {
    abstract void sound(); // Abstract method

    void eat() { // Concrete method
        System.out.println("Animal is eating");
    }
}

class Dog extends Animal {
    void sound() { // Implementing abstract method
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.sound(); // Dog barks
        d.eat(); // Animal is eating
    }
}

✔ sound() is implemented in Dog.
✔ eat() is inherited from Animal.

Output:

Dog barks
Animal is eating

4. Abstract Class with Constructors

✔ An abstract class can have constructors, which are called when a subclass is instantiated.

Example: Abstract Class Constructor

abstract class Animal {
    Animal() { // Constructor
        System.out.println("Animal constructor called");
    }
    abstract void sound();
}

class Dog extends Animal {
    Dog() {
        System.out.println("Dog constructor called");
    }
    
    void sound() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.sound();
    }
}

Output:

Animal constructor called
Dog constructor called
Dog barks

✔ Superclass constructor runs first before the subclass constructor.

5. Abstract Class vs Interface

6. Abstract Class with Multiple Subclasses

✔ Multiple subclasses can extend the same abstract class.

Example: Multiple Subclasses

abstract class Animal {
    abstract void sound();
}

class Dog extends Animal {
    void sound() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    void sound() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        a1.sound(); // Dog barks
        a2.sound(); // Cat meows
    }
}

✔ Polymorphism: Different implementations of sound().


7. Abstract Class with Static Methods

✔ Static methods in an abstract class can be called without an instance.

Example: Static Method in Abstract Class

abstract class Animal {
    static void show() {
        System.out.println("Static method in abstract class");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal.show(); // Calling static method directly
    }
}

✔ Output: Static method in abstract class

8. Abstract Class with Final Methods

✔ Final methods cannot be overridden in subclasses.

Example: Final Method

abstract class Animal {
    final void sleep() {
        System.out.println("Animal is sleeping");
    }
}

class Dog extends Animal {
    // void sleep() { } // ERROR: Cannot override final method
}

✔ sleep() cannot be overridden because it is final.


Summary

✔ Abstract classes cannot be instantiated but can have constructors.
✔ Abstract methods must be implemented by subclasses.
✔ Can contain both abstract and concrete methods.
✔ Supports inheritance but not multiple inheritance.
✔ Useful for defining a base class with common behavior for subclasses.


Interface in Java

An interface in Java is a blueprint for a class that contains only abstract methods (before Java 8) and static or final variables. It allows multiple classes to implement the same behavior without inheritance limitations.


1. Key Features of an Interface

✔ 100% abstraction (before Java 8, all methods were abstract).
✔ No object creation (cannot be instantiated).
✔ Multiple inheritance supported (a class can implement multiple interfaces).
✔ Only abstract methods (before Java 8).
✔ Only public, static, and final variables.
✔ Allows default and static methods (from Java 8).


2. Declaring and Implementing an Interface

Use the interface keyword. A class implements an interface using implements.

Example: Interface with Abstract Methods

interface Animal {
    void sound(); // Abstract method
}

class Dog implements Animal {
    public void sound() { // Implementing interface method
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.sound(); // Dog barks
    }
}

✔ sound() is implemented in Dog since Animal is an interface.

Output:

Dog barks


3. Interface with Multiple Methods

✔ A class must implement all methods of an interface.

interface Vehicle {
    void start();
    void stop();
}

class Car implements Vehicle {
    public void start() {
        System.out.println("Car is starting");
    }
    public void stop() {
        System.out.println("Car is stopping");
    }
}

public class Main {
    public static void main(String[] args) {
        Car c = new Car();
        c.start();
        c.stop();
    }
}

Output:

Car is starting  
Car is stopping

✔ All methods of Vehicle are implemented in Car.


4. Interface Variables (Final & Static)

✔ Interface variables are public static final by default.

Example: Constant Variables in Interface

interface Game {
    int PLAYERS = 2; // public static final
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Game.PLAYERS); // 2
    }
}

✔ PLAYERS is a constant (final).
✔ Cannot be modified (Game.PLAYERS = 3; gives an error).


5. Multiple Interfaces in One Class

✔ A class can implement multiple interfaces (unlike multiple inheritance with classes).

Example: Multiple Interfaces

interface Animal {
    void eat();
}

interface Bird {
    void fly();
}

class Sparrow implements Animal, Bird { // Multiple interfaces
    public void eat() {
        System.out.println("Sparrow eats grains");
    }
    public void fly() {
        System.out.println("Sparrow flies high");
    }
}

public class Main {
    public static void main(String[] args) {
        Sparrow s = new Sparrow();
        s.eat();
        s.fly();
    }
}

Output:

Sparrow eats grains  
Sparrow flies high

✔ Sparrow implements both Animal and Bird, showing multiple inheritance.

6. Java 8 Features in Interfaces

Java 8 introduced:
✔ Default Methods (can have a body in an interface).
✔ Static Methods (can be called using the interface name).

Example: Default Method

interface Vehicle {
    default void speed() { // Default method
        System.out.println("Vehicle has a speed limit");
    }
}

class Car implements Vehicle { }

public class Main {
    public static void main(String[] args) {
        Car c = new Car();
        c.speed(); // Vehicle has a speed limit
    }
}

✔ speed() is inherited without overriding.

Example: Static Method in Interface

interface MathUtils {
    static int square(int x) {
        return x * x;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(MathUtils.square(4)); // 16
    }
}

✔ Called using the interface name (MathUtils.square(4)).

7. Interface vs Abstract Class

8. Functional Interfaces (Java 8)

✔ A functional interface is an interface with only one abstract method.
✔ Used for lambda expressions.
✔ Example: Runnable, Comparable.

Example: Functional Interface with Lambda

@FunctionalInterface
interface Calculator {
    int add(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Calculator c = (a, b) -> a + b; // Lambda expression
        System.out.println(c.add(5, 3)); // 8
    }
}

✔ Lambda simplifies interface implementation.


9. Marker Interfaces

✔ Interfaces without methods (e.g., Serializable, Cloneable).
✔ Used to mark classes for special behavior.

Summary

✔ Interfaces provide 100% abstraction (before Java 8).
✔ Multiple interfaces can be implemented by a class.
✔ All variables are public static final by default.
✔ Java 8 introduced default and static methods.
✔ Interfaces allow multiple inheritance, unlike classes


Packages in java 

A package in Java is a container for classes, interfaces, and sub-packages that helps organize code and prevent naming conflicts. Packages are similar to folders in a file system.

1. Types of Packages

Java has two types of packages:
✔ Built-in Packages – Provided by Java (e.g., java.util, java.io).
✔ User-defined Packages – Created by developers to organize their code.


2. Creating a User-Defined Package

✔ Use the package keyword at the beginning of the file.
✔ The file should be saved inside a folder matching the package name.

Example: Creating a Package

Step 1: Create a Package (mypackage/Animal.java)

package mypackage; // Define package

public class Animal {
    public void display() {
        System.out.println("This is an animal");
    }
}

✔ The class Animal belongs to package mypackage.
✔ Save the file inside a folder named mypackage.

Step 2: Use the Package (Main.java)

import mypackage.Animal; // Import package

public class Main {
    public static void main(String[] args) {
        Animal a = new Animal(); // Creating an object
        a.display();
    }
}

✔ import mypackage.Animal; allows access to the Animal class.

Output:

This is an animal


3. Using import to Access Packages

✔ import package_name.class_name; → Imports one specific class.
✔ import package_name.*; → Imports all classes in the package.

Example: Importing All Classes

import mypackage.*;

public class Main {
    public static void main(String[] args) {
        Animal a = new Animal();
        a.display();
    }
}

✔ import mypackage.*; imports all classes from mypackage.


4. Access Modifiers and Packages

✔ public → Accessible everywhere.
✔ protected → Accessible in same package & subclasses in other packages.
✔ (default) → Accessible only within the same package.
✔ private → Accessible only within the same class.

5. Sub-Packages in Java

✔ A package can contain sub-packages.
✔ Syntax: package parent_package.subpackage;

Example: Creating a Sub-Package

File: mypackage/animals/Dog.java

package mypackage.animals; // Sub-package

public class Dog {
    public void bark() {
        System.out.println("Dog barks");
    }
}

✔ The class Dog belongs to mypackage.animals.

Using the Sub-Package (Main.java)

import mypackage.animals.Dog; // Import sub-package

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.bark();
    }
}

✔ Output: Dog barks


6. Built-in Java Packages

Java provides many predefined packages, such as:

✔ No need to import java.lang (automatically included).

Example: Using java.util Package

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        
        System.out.println(list); // [Apple, Banana]
    }
}

✔ ArrayList is part of java.util, so it must be imported.


7. Accessing a Package Without import

✔ Use fully qualified name instead of import.

public class Main {
    public static void main(String[] args) {
        mypackage.Animal a = new mypackage.Animal();
        a.display();
    }
}

✔ No need to use import, but the code is longer.


8. Compiling and Running Java Packages

Step 1: Compile the Package

javac -d . Animal.java

✔ -d . creates the package directory automatically.

Step 2: Compile the Main Class

javac -d . Main.java

✔ This ensures the main class recognizes the package.

Step 3: Run the Program

java Main

✔ Executes the program using the compiled package.

Summary

✔ Packages organize Java classes like folders in a file system.
✔ User-defined packages are created using the package keyword.
✔ import is used to access classes from other packages.
✔ Built-in packages include java.util, java.io, java.sql, etc.
✔ Packages help prevent naming conflicts and improve code modularity.



util Package 

The java.util package is one of the most commonly used built-in Java packages. It provides utility classes for data structures, collections, date/time manipulation, random numbers, and more.


1. Commonly Used Classes in java.util

2. Using java.util Classes

✔ You must import java.util classes before using them:

import java.util.ArrayList;
import java.util.Scanner;

✔ Alternatively, you can import all classes in java.util:

import java.util.*;


3. Example: ArrayList (Dynamic Array)

✔ ArrayList allows dynamic resizing and fast access.

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        System.out.println(list); // [Apple, Banana, Cherry]
        list.remove("Banana");
        System.out.println(list); // [Apple, Cherry]
    }
}

✔ Output:

[Apple, Banana, Cherry]  
[Apple, Cherry]

✔ ArrayList allows fast retrieval but slower insertion/deletion compared to LinkedList.

4. Example: HashMap (Key-Value Pairs)

✔ HashMap stores unique keys with associated values.

import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 85);
        scores.put("Bob", 90);
        scores.put("Charlie", 75);

        System.out.println(scores.get("Alice")); // 85
    }
}

✔ Output: 85
✔ Fast retrieval using keys.
✔ Keys must be unique.


5. Example: HashSet (Unique Elements)

✔ HashSet stores unique elements only and does not maintain order.

import java.util.HashSet;

public class Main {
    public static void main(String[] args) {
        HashSet<Integer> set = new HashSet<>();
        set.add(10);
        set.add(20);
        set.add(10); // Duplicate, ignored

        System.out.println(set); // [10, 20]
    }
}

✔ Output: [10, 20]
✔ No duplicate values allowed.


6. Example: Scanner (User Input)

✔ Scanner reads keyboard input from the user.

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Enter your name: ");
        String name = sc.nextLine();

        System.out.println("Hello, " + name + "!");
        sc.close(); // Close scanner
    }
}

✔ Output:

Enter your name: John  
Hello, John!

✔ nextLine() reads full input, while nextInt() reads integers.


7. Example: Random (Generating Random Numbers)

✔ Random generates pseudo-random numbers.

import java.util.Random;

public class Main {
    public static void main(String[] args) {
        Random rand = new Random();
        int randomNumber = rand.nextInt(100); // Random number between 0-99
        System.out.println("Random Number: " + randomNumber);
    }
}

✔ Output:

Random Number: 47

✔ nextInt(100) generates 0 to 99.
✔ nextDouble() generates decimal values between 0.0 and 1.0.

8. Example: Date (Current Date and Time)

✔ Date represents the current date/time.

import java.util.Date;

public class Main {
    public static void main(String[] args) {
        Date now = new Date();
        System.out.println(now);
    }
}

✔ Output (varies by time):

Mon Apr 01 10:30:00 IST 2025


9. Example: Calendar (Date Manipulation)

✔ Calendar allows date/time calculations.

import java.util.Calendar;

public class Main {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        System.out.println("Year: " + cal.get(Calendar.YEAR));
        System.out.println("Month: " + (cal.get(Calendar.MONTH) + 1)); // 0-based
        System.out.println("Day: " + cal.get(Calendar.DAY_OF_MONTH));
    }
}

✔ Output:

Year: 2025  
Month: 4  
Day: 1

✔ MONTH starts from 0 (so add 1).

10. Example: Collections (Sorting & Searching)

✔ Collections provides utility methods for collections.

import java.util.ArrayList;
import java.util.Collections;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(2);
        numbers.add(8);
        numbers.add(1);

        Collections.sort(numbers);
        System.out.println(numbers); // [1, 2, 5, 8]
    }
}

✔ Output: [1, 2, 5, 8]
✔ Collections.sort() sorts numbers in ascending order.

Summary

✔ java.util provides useful data structures, date/time utilities, random number generation, user input handling, and collection utilities.
✔ Common classes include ArrayList, HashMap, HashSet, Scanner, Random, Date, Calendar, and Collections.
✔ Collections framework (ArrayList, HashMap, etc.) is heavily used in Java programming 






I/O programming

Unit 5 -syllabus  Text and binary I/O,Binary I./O classes, Object I/O, Random Access files, multithreading in java: thread life cycle and m...