Object-Oriented Programming Concepts: Abstraction, Encapsulation, Inheritance, and More

Data Abstraction vs. Encapsulation

Definition

Data Abstraction: The concept of hiding complex implementation details and showing only the necessary features of an object.

Encapsulation: The practice of bundling the data (variables) and methods (functions) that operate on the data into a single unit or class.

Purpose

Data Abstraction: Focuses on exposing only the essential features and functionalities.

Encapsulation: Focuses on protecting the data by restricting direct access to it.

Implementation

Data Abstraction: Achieved using abstract classes and interfaces.

Encapsulation: Achieved using access modifiers (private, public, protected) in classes.

Visibility

Data Abstraction: Hides implementation details from the user.

Encapsulation: Hides the data itself from the outside world, allowing access only through methods.

Example

Data Abstraction: Showing the functionality of a car’s steering wheel without explaining the mechanics behind it.

Encapsulation: A class Car with private variables like engine and speed, accessible only through public methods like start() and accelerate().

Goal

Data Abstraction: Simplifies complex systems by modeling classes appropriate to the problem.

Encapsulation: Protects the integrity of the data by preventing unauthorized access and modification.

Relationship

Data Abstraction: Concerned with what an object does.

Encapsulation: Concerned with how the object does it.

Procedural Programming vs. OOP

Paradigm

Procedural Programming: Follows a top-down approach where the program is divided into procedures or functions.

OOP: Follows a bottom-up approach where the program is organized around objects and classes.

Structure

Procedural Programming: Structures the program as a sequence of instructions or steps.

OOP: Structures the program as a collection of interacting objects.

Focus

Procedural Programming: Focuses on functions or procedures to operate on data.

OOP: Focuses on objects that encapsulate both data and methods.

Data Handling

Procedural Programming: Data is separate from functions and is often passed around to functions.

OOP: Data and methods are bundled together in objects, promoting data hiding and encapsulation.

Reusability

Procedural Programming: Code reuse is achieved through functions.

OOP: Code reuse is achieved through inheritance and polymorphism.

Examples

Procedural Programming: C, Pascal.

OOP: Java, C++, Python.

Problem Solving

Procedural Programming: Better suited for tasks where the operations are clearly defined and linear.

OOP: Better suited for complex systems with interrelated components and a need for modularity.

Garbage Collection in Java

What is Garbage Collection?

Garbage collection (GC) is the process of automatically identifying and deleting objects that are no longer needed by a program, freeing up memory space.

The Heap

Java applications allocate memory for objects on the heap. Over time, some objects become unreachable because they are no longer referenced by any part of the program.

How GC Works

  • The garbage collector scans the heap to find objects that are no longer referenced by any active part of the program.
  • These unreferenced objects are considered “garbage” and can be safely deleted to free up memory.

Roots

The garbage collector starts from a set of “roots.” Roots include local variables, active threads, and static variables. Any object not reachable from these roots is considered garbage.

Mark and Sweep

Mark Phase: The GC “marks” all objects that are reachable from the roots. It does this by traversing the graph of objects starting from the roots.

Sweep Phase: After marking, the GC “sweeps” through the heap to delete unmarked objects and reclaim their memory.

Generational GC

Java’s GC is often generational, meaning it divides objects into different generations based on their age (how long they have been in memory).

  • Young Generation: Newly created objects. Most objects die young and are quickly collected.
  • Old Generation: Objects that have survived multiple GC cycles. They are collected less frequently.

Stop-the-World

During garbage collection, the program execution might be paused temporarily. This pause is known as a “stop-the-world” event, where all application threads are stopped to allow the GC to do its work safely.

Types of Garbage Collectors

Java has several garbage collectors, each with different algorithms and performance characteristics. Common ones include:

  • Serial GC: Simple and best for small applications.
  • Parallel GC: Uses multiple threads for GC and is suited for multi-threaded applications.

Static Variables and Methods

Static Variables are class variables that are shared among all instances of a class. They are declared with the static keyword and are associated with the class itself, rather than any particular instance.

Static Methods are functions that belong to the class rather than any particular instance. They can be called without creating an instance of the class and can only access static variables and other static methods directly.

Example: Counting the Number of Objects Created

public class Counter {
    private static int count = 0;

    public Counter() {
        count++;
    }

    public static int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Counter c3 = new Counter();
        System.out.println("Number of objects created: " + Counter.getCount());
    }
}

Output:

Number of objects created: 3

Explanation

  • Static Variable (count): private static int count = 0;: This variable is declared as static, so it belongs to the Counter class itself, not to any specific instance. It keeps track of how many Counter objects have been created.
  • Constructor (Counter()): The constructor increments the static count variable each time a new Counter object is created.
  • Static Method (getCount()): public static int getCount(): This method returns the current value of the static count variable. It can be called without creating an instance of Counter.
  • Main Method: In the main method, we create three instances of the Counter class: c1, c2, and c3. We then call the static method Counter.getCount() to print the total number of Counter objects created.

Instance Variable Hiding and Finalize Method

Instance Variable Hiding

Instance variable hiding occurs when a subclass declares a variable with the same name as an instance variable in its superclass. This hides the superclass variable within the subclass. To access the hidden variable from the superclass, you need to use the super keyword.

Example

class Parent {
    int value = 10;
}

class Child extends Parent {
    int value = 20; // This hides the 'value' variable in Parent

    void displayValues() {
        System.out.println("Child value: " + value);
        System.out.println("Parent value: " + super.value);
    }

    public static void main(String[] args) {
        Child child = new Child();
        child.displayValues();
    }
}

Finalize Method

The finalize() method is called by the garbage collector on an object before it is garbage collected. It gives the object a chance to perform any necessary cleanup operations. However, the use of finalize is generally discouraged because its invocation and timing are not guaranteed.

Example

class Resource {
    protected void finalize() throws Throwable {
        System.out.println("Resource is being finalized");
        super.finalize();
    }

    public static void main(String[] args) {
        Resource resource = new Resource();
        System.gc();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Method Overloading vs. Method Overriding

Method Overloading

Definition: Method overloading involves defining multiple methods in a class with the same name but different parameters.

Inheritance: Overloading is not related to inheritance; it can occur within the same class or between a superclass and its subclass.

Compile-time Polymorphism: Overloading is an example of compile-time polymorphism (also known as static polymorphism) because the compiler determines which method to call based on the method signature.

Example: add(int a, int b) and add(double a, double b) are overloaded methods because they have the same name (add) but different parameter types.

Method Overriding

Definition: Method overriding involves redefining a method in a subclass with the same name, return type, and parameters as a method in its superclass.

Inheritance: Overriding occurs in the context of inheritance, where a subclass provides a specific implementation of a method defined in its superclass.

Runtime Polymorphism: Overriding is an example of runtime polymorphism (also known as dynamic polymorphism) because the method to be executed is determined at runtime based on the object’s actual type.

Example: If class Animal has a method makeSound() and its subclass Dog overrides this method with a specific bark sound implementation, it’s an example of method overriding.

Abstract Class vs. Interface

Abstract Class

Definition: An abstract class is a class that cannot be instantiated and may contain abstract methods (methods without a body) along with concrete methods.

Usage: Used to define a common base for related classes, providing default behavior and structure that subclasses can extend.

Implementation: Abstract classes can have instance variables, constructors, and both abstract and concrete methods.

Inheritance: Allows both single and multiple inheritance (a class can extend only one abstract class).

Example: Shape can be an abstract class with an abstract method calculateArea(), which subclasses like Circle and Rectangle must implement.

Interface

Definition: An interface is a reference type in Java that contains only abstract methods and constants (variables that are implicitly final and static).

Usage: Used to specify a set of methods that a class must implement, providing a contract for classes to adhere to.

Implementation: Interfaces cannot contain concrete methods or instance variables.

Inheritance: Allows multiple inheritance (a class can implement multiple interfaces).

Example: Drawable interface can have a method draw(), which classes like Circle, Rectangle, and Triangle can implement.

Static Binding vs. Late Binding

Static Binding

Static binding, also known as early binding, occurs during compile-time. It means that the compiler determines which method to call based on the reference type.

Example

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

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

public class Main {
    public static void main(String[] args) {
        Animal obj = new Dog(); // Reference type is Animal, but object type is Dog
        obj.sound(); // This will call the sound() method of Animal class
    }
}

In this example, the reference variable obj is of type Animal, but it points to an object of type Dog. During compile-time, the compiler determines the method to be called based on the reference type (Animal). Therefore, it calls the sound() method of the Animal class.

Late Binding

Late binding, also known as dynamic binding or runtime polymorphism, occurs during runtime. It means that the method to be called is determined based on the actual object type at runtime.

Example

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

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

public class Main {
    public static void main(String[] args) {
        Animal obj = new Dog(); // Reference type is Animal, but object type is Dog
        obj.sound(); // This will call the sound() method of the Dog class
    }
}

In this example, even though the reference variable obj is of type Animal, the actual object it points to is of type Dog. Due to late binding, the sound() method of the Dog class is called at runtime.

Super and Final Keywords

1. super Keyword

The super keyword in Java is used to refer to the superclass or parent class of a subclass. It can be used to access methods, variables, and constructors of the superclass.

Example

class Parent {
    void display() {
        System.out.println("Parent's display method");
    }
}

class Child extends Parent {
    void display() {
        super.display(); // Calling parent class method
        System.out.println("Child's display method");
    }
}

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

In this example, the Child class extends the Parent class. Inside the display() method of the Child class, super.display() is used to call the display() method of the Parent class. This allows us to access the method from the superclass.

2. final Keyword

The final keyword in Java is used to restrict the user from modifying the class, method, or variable. Once a variable, method, or class is declared as final, it cannot be overridden, modified, or subclassed, respectively.

Example

class Parent {
    final void display() {
        System.out.println("Parent's display method");
    }
}

class Child extends Parent {
    void display() { // This will give a compilation error
        System.out.println("Child's display method");
    }
}

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

In this example, the display() method in the Parent class is declared as final, meaning it cannot be overridden by subclasses. Attempting to override it in the Child class will result in a compilation error.

Checked Exceptions vs. Unchecked Exceptions

Checked Exceptions

Definition: Checked exceptions are exceptions that are checked at compile-time, meaning the compiler checks if the code handles or declares these exceptions.

Handling: Checked exceptions must be handled explicitly using try-catch blocks or by declaring them in the method signature using the throws keyword.

Examples: IOException, FileNotFoundException, SQLException are examples of checked exceptions.

Forced Handling: Checked exceptions force the programmer to handle exceptions explicitly, ensuring robust error-handling in the code.

Inheritance: Checked exceptions are subclasses of the Exception class, but not of RuntimeException.

Example

try {
    FileReader file = new FileReader("file.txt");
} catch (FileNotFoundException e) {
    System.out.println("File not found: " + e.getMessage());
}

Unchecked Exceptions

Definition: Unchecked exceptions are exceptions that are not checked at compile-time; they occur at runtime and are not required to be handled explicitly.

Handling: Unchecked exceptions can be handled using try-catch blocks, but it’s not mandatory. If not handled, they propagate up the call stack until caught or result in program termination.

Examples: NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException are examples of unchecked exceptions.

Optional Handling: Handling unchecked exceptions is optional, as the compiler does not enforce handling them explicitly.

Inheritance: Unchecked exceptions are subclasses of the RuntimeException class or its subclasses.

Example

int result;
try {
    result = 10 / 0; // ArithmeticException: division by zero
} catch (ArithmeticException e) {
    System.out.println("Error: " + e.getMessage());
}

Error vs. Exception

Error

Definition: Errors in Java represent serious problems that usually cannot be handled by the application. They are typically caused by the environment in which the application is running and are not recoverable.

Examples: OutOfMemoryError, StackOverflowError, VirtualMachineError are examples of errors.

Handling: Errors are not meant to be caught or handled by the application code. They indicate critical issues that often require intervention at the system level.

Cause: Errors are typically caused by the Java Virtual Machine (JVM) or the underlying system, such as running out of memory or encountering a hardware failure.

Consequences: Errors usually lead to abnormal termination of the program or system. They are not meant to be recovered from programmatically.

Exception

Definition: Exceptions in Java represent exceptional conditions or unexpected events that occur during the execution of a program. Unlike errors, exceptions are recoverable and can be handled by the application code.

Examples: NullPointerException, FileNotFoundException, ArithmeticException are examples of exceptions.

Handling: Exceptions are meant to be caught and handled by the application code using try-catch blocks or by declaring them in the method signature using the throws keyword.

Cause: Exceptions are typically caused by incorrect input, invalid operations, or unexpected conditions during program execution.

Consequences: Exceptions can be caught, logged, and handled gracefully within the application code, allowing the program to recover from unexpected conditions and continue executing.

Example

public class ErrorVsExceptionExample {
    public static void main(String[] args) {
        try {
            int result = divide(10, 0); // This will throw an ArithmeticException
        } catch (ArithmeticException e) {
            System.out.println("Error: " + e.getMessage());
        }

        int[] array = new int[Integer.MAX_VALUE]; // This will throw an OutOfMemoryError
    }

    public static int divide(int a, int b) {
        return a / b;
    }
}

User-Defined Exceptions in Java

Creating a user-defined exception in Java involves creating a new class that extends either Exception or one of its subclasses. Here’s a simple example:

class CustomException extends Exception {
    public CustomException(String message) {
        super(message);
    }
}

public class Main {
    static void validate(int age) throws CustomException {
        if (age < 18) {
            throw new CustomException("Age must be 18 or above");
        } else {
            System.out.println("Welcome to the club!");
        }
    }

    public static void main(String[] args) {
        try {
            validate(15);
        } catch (CustomException e) {
            System.out.println("Caught CustomException: " + e.getMessage());
        }
    }
}

Explanation

  • We define a custom exception class CustomException that extends Exception.
  • The constructor of the CustomException class initializes the exception message.
  • We define a method validate that checks if the age is less than 18. If it is, it throws a CustomException.
  • In the main method, we call the validate method with an age of 15, which throws the custom exception.