Java OOP Deep Dive: From Blueprints to Real-World Code (A Mega One-Short Guide)

Shivprasad RoulShivprasad Roul
33 min read

Object-Oriented Programming (OOP) is more than just a buzzword; it's a powerful paradigm that shapes how we design and build robust, scalable, and maintainable software. Java, at its core, is an object-oriented language. If you're looking to master Java, understanding OOP concepts is non-negotiable.

This guide will walk you through the essential pillars and keywords of Java OOP, transforming abstract concepts into practical knowledge you can apply immediately. Let's dive in!

1. Class and Objects in Java: The Blueprint and the Real Deal

So, what's the deal with Classes and Objects?

Think of a Class like a blueprint. Just like a car company designs a blueprint before manufacturing, a class defines the structure (attributes) and behavior (methods) of future objects. It specifies everything:

  • For a Car class: how many wheels, the type of engine, dimensions, weight, available colors.

  • And what the car can do: like accelerate(), brake(), turnSteeringWheel().

An Object, on the other hand, is a real-world instance created from that class. If the class is a car blueprint, then the object is the actual car that you can drive. You can have multiple cars (objects) based on the same blueprint (class). For example, you might have two Car objects of the same model:

  • myRedCar: color = "Red", currentSpeed = 0

  • myBlueCar: color = "Blue", currentSpeed = 0 Both have the same functionality (defined by the Car class) but can have different properties (their current state).

So What Does a Class Contain?

Each class typically includes:

  • Properties (fields/attributes): These are the characteristics of an object, representing its state.

    • For a Car: color, engineType, maxSpeed.

    • For a Person: name, age.

  • Methods (behaviors/functions): These are the actions the object can perform, often manipulating its own properties.

    • For a Car: accelerate(), brake().

    • For a Person: speak(), walk().

Setting Properties & Calling Methods: The Dot Operator (.)

To set or access the properties and methods of an object, Java uses the dot operator (.). It's like saying "hey object, do this" or "give me that."

Here's a basic example:

// Define the Animal class (the blueprint)
class Animal {
    // Properties (fields/attributes)
    String name;
    String breed;
    int age;

    // A simple method (behavior)
    void makeSound() {
        System.out.println("Animal sound");
    }
}

// Main class to run our example
public class Main {
    public static void main(String[] args) {
        // Creating an object 'dog' of class 'Animal'
        // 'new Animal()' actually creates the instance in memory
        Animal dog = new Animal();
        // Setting properties using the dot operator
        dog.name = "Sheu";
        dog.breed = "Axy"; // Corrected from "Axy"; (semicolon was misplaced in original notes)
        dog.age = 5;
        // Accessing properties and printing them
        System.out.println("Dog's name: " + dog.name);
        System.out.println("Dog's breed: " + dog.breed);
        System.out.println("Dog's age: " + dog.age);
        // Calling a method on the object
        dog.makeSound(); // Output: Animal sound
    }
}

2. Why Use OOPs in the First Place?

Object-Oriented Programming isn't just a fancy way to write code; it offers tangible benefits like....

  1. Modularity: Your code is divided into self-contained units (classes). This makes it easier to design, maintain, debug, and test. If something goes wrong with a Car object, you know to look in the Car class.

  2. Code Reuse: With features like inheritance (which we'll cover soon!), you can extend existing code without rewriting it. This means more efficiency and less redundancy.

  3. Flexibility (Polymorphism): Objects can be treated as instances of their parent class or an interface they implement. This allows for cleaner, more adaptable code that can handle different object types in a uniform way.

  4. Real-World Mapping: OOP helps you model real-world entities (like cars, people, bank accounts) naturally. This makes problem-solving more intuitive because your code structure can mirror the problem domain.

  5. Encapsulation (Data Hiding): Bundling data (attributes) and methods that operate on the data within a single unit (class). It also involves protecting the internal state of an object from outside interference.

3. Methods: What Objects Can Do

Methods define the behaviors or actions an object can perform. Think of them as functions that "live" inside classes and typically act on the object's data (its properties).

Example:

class Person {
    // Properties
    String name;
    int age;
    // Method to make the person speak
    void speak() {
        System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
    }
    // Method to display details (could be combined with speak or separate)
    void displayDetails() {
        System.out.println(name + " is " + age + " and they are walking.");
    }
}

public class PersonDemo {
    public static void main(String[] args) {
        Person person1 = new Person();
        person1.name = "John";
        person1.age = 30;
        person1.speak();// Output: Hello, my name is John and I am 30 years old.
        person1.displayDetails();// Output: John is 30 and they are walking.
    }
}

Types of Methods

1. Parameterized Methods

These methods accept input parameters (arguments) so they can perform operations using those values.

class Messenger {
    void sendMessage(String message) { // 'message' is a parameter
        System.out.println("Sending: " + message);
    }
    int addNumbers(int a, int b) { // 'a' and 'b' are parameters
        return a + b;
    }
}

public class MessengerDemo {
    public static void main(String[] args) {
        Messenger outlook = new Messenger();
        outlook.sendMessage("Hello World!"); // "Hello World!" is an argument
        int sum = outlook.addNumbers(10, 20); // 10 and 20 are arguments
        System.out.println("Sum: " + sum);    // Output: Sum: 30
    }
}

2. Default Parameters in Java? (Method Overloading to the Rescue!)

Java does not support default parameters directly in method definitions like Python or C++. However, you can achieve similar behavior using method overloading.

Method overloading means having multiple methods in the same class with the same name but different parameter lists (different number or types of parameters). Here's how we can mimic default parameters:

class Greeter {
    // Method that takes a custom message
    void say(String message) {
        System.out.println(message);
    }
    // Overloaded method without parameters - acts as the "default"
    void say() {
        System.out.println("Default Greeting!"); // The "default" message
    }
}

public class TestGreeter {
    public static void main(String[] args) {
        Greeter greeter = new Greeter();
        greeter.say("Hello there!"); // Calls the version with String parameter
                                     // Output: Hello there!
        greeter.say();               // Calls the version with no parameters
                                     // Output: Default Greeting!
    }
}

4. Method Overloading vs. Method Overriding

Let's clarify this common point of confusion:

  • Method Overloading:

    • Same method name.

    • Different parameters (number, type, or order of types).

    • Within the same class.

    • Resolved at compile-time (static polymorphism).

    • Purpose: Provide multiple ways to call a method with different inputs.

    • eg area of circle triangle etc as difft shape have diff formulas

  • Method Overriding:

    • Same method name.

    • Same parameters (number, type, and order).

    • In a subclass providing a different implementation of a method defined in its superclass.

    • Resolved at run-time (dynamic polymorphism).

    • Purpose: Allow a subclass to provide a specific implementation of a method that is already provided by its superclass.

    • (We'll dive deeper into overriding when we get to Inheritance.)

5. The this Keyword: Referring to the Current Object

The this keyword is a reference variable in Java that refers to the current object — the object whose method or constructor is being called. Why is it useful?

  1. Distinguish Instance Variables from Parameters: When a method parameter or a local variable has the same name as an instance variable, this helps to disambiguate. this.variableName refers to the instance variable, while variableName (without this) refers to the parameter or local variable.

  2. Passing the Current Object as an Argument: You can pass the current object as an argument to another method using this.

  3. Calling Another Constructor from a Constructor (Constructor Chaining): this() can be used to call another constructor of the same class.

class Student {
    String name;
    int id;

    // Constructor using 'this' to differentiate instance variable from parameter
    public Student(String name, int id) {
        // 'this.name' is the instance variable
        // 'name' is the parameter
        this.name = name;
        // 'this.id' is the instance variable
        // 'id' is the parameter
        this.id = id;
        System.out.println("Student object created: " + this.name);
    }

    void display() {
        // Here, 'this.id' and 'id' would refer to the same instance variable
        // 'this' is often optional if there's no ambiguity
        System.out.println("ID: " + this.id + ", Name: " + this.name);
    }
    void updateName(String name) {
        // Using 'this' to clearly set the instance variable 'name'
        this.name = name;
    }
}

public class TestStudent {
    public static void main(String[] args) {
        Student s1 = new Student("Alice", 101); // Output during construction: Student object created: Alice
        s1.display(); // Output: ID: 101, Name: Alice

        Student s2 = new Student("Bob", 102);   // Output during construction: Student object created: Bob
        s2.display();   // Output: ID: 102, Name: Bob
        s2.updateName("Robert");
        s2.display();   // Output: ID: 102, Name: Robert
    }
}

6. Constructors: Building Your Objects

Maybe to construct/build something? So what is a constructor? Exactly! In OOPs, we can't build any object without a constructor. A constructor is a special method that is called automatically when an object of a class is created (using the new keyword). Its main job is to initialize the object's state (its properties).

Key characteristics of a constructor:

  • It has the same name as the class.

  • It has no explicit return type (not even void).

The Default Constructor

But wait, in our first Animal example, we didn't write any constructor, but we could still create objects! So then what?

Well, Java (and C++) are helpful. If you don't provide any constructor in your class, the compiler provides a default constructor for you.

  • This default constructor takes no arguments.

  • It usually initializes instance variables to their default values (0 for numbers, null for objects, false for booleans).

Defining Our Own Constructor

Let's try defining our own:

class Person {
    String name;
    int age;
    // This is our constructor
    // It has the same name as the class and no return type.
    public Person() {
        System.out.println("Creating an object of Person...");
        // We can initialize properties here
        name = "Default Name";
        age = 0;
    }

    void speak() {
        System.out.println(name + " says: I can Speak");
    }

    void tell(String whatToSay) {
        System.out.println(name + " says: " + whatToSay);
    }
}

public class ConstructorDemo {
    public static void main(String[] args) {
        // When 'new Person()' is called, the Person() constructor executes.
        Person shiv = new Person(); // Output: Creating an object of Person...

        shiv.speak();              // Output: Default Name says: I can Speak
        shiv.tell("Hello!");       // Output: Default Name says: Hello!
        System.out.println(shiv.name + " is " + shiv.age); // Output: Default Name is 0
    }
}

Parameterized Constructor

Constructors, like methods, can also be parameterized. This is super useful for initializing an object with specific values right when it's created.

Basically a constructor that accepts arguments.

and what's the are benefits ??

class Person {
    String name;
    int age;

    // This is a parameterized constructor
    public Person(String n, int a) { // 'n' and 'a' are parameters
        System.out.println("Creating an object with name and age...");
        // Assigning the passed name to the object's name
        name = n; // Or using 'this.name = n;' for clarity
        // Assigning the passed age to the object's age
        age = a;  // Or using 'this.age = a;'
    }

    // Overloaded constructor (no-argument)
    public Person() {
        System.out.println("Creating an object with default values...");
        name = "No Name";
        age = -1; // Indicate not set
    }

    void displayDetails() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

public class ParameterizedConstructorDemo {
    public static void main(String[] args) {
        // Creating an object using the parameterized constructor
        // The note had 'Person shiv = new Person(name: "Shiv");' which isn't valid Java
        // for passing arguments. It should be positional.
        Person shiv = new Person("Shiv", 25); // Output: Creating an object with name and age...
        shiv.displayDetails();                // Output: Name: Shiv, Age: 25

        Person anjali = new Person("Anjali", 30); // Output: Creating an object with name and age...
        anjali.displayDetails();                   // Output: Name: Anjali, Age: 30

        Person unknown = new Person(); // Output: Creating an object with default values...
        unknown.displayDetails();       // Output: Name: No Name, Age: -1
    }
}

Constructor Overloading

Just like methods, you can have multiple constructors in the same class, as long as they have different parameter lists (different number or types of arguments). This is called constructor overloading. It's a type of polymorphism.

In one class, we can have many constructors.

class Box {
    double width;
    double height;
    double depth;

    // Constructor 1: No arguments (default-like behavior if we define it)
    public Box() {
        System.out.println("Box object created with default dimensions (0).");
        this.width = this.height = this.depth = 0; // Or some sensible defaults
    }
    // Constructor 2: Takes one argument (assumes a cube)
    public Box(double side) {
        System.out.println("Box object created as a cube.");
        this.width = this.height = this.depth = side;
    }
    // Constructor 3: Takes three arguments
    public Box(double w, double h, double d) {
        System.out.println("Box object created with specific dimensions.");
        this.width = w;
        this.height = h;
        this.depth = d;
    }
    void displayVolume() {
        System.out.println("Volume is " + (width * height * depth));
    }
}
public class ConstructorOverloadingDemo {
    public static void main(String[] args) {
        Box box1 = new Box();                       // Calls Constructor 1
        box1.displayVolume();                     // Output: Volume is 0.0
        Box box2 = new Box(10);                   // Calls Constructor 2
        box2.displayVolume();                     // Output: Volume is 1000.0
        Box box3 = new Box(3, 4, 5);              // Calls Constructor 3
        box3.displayVolume();                     // Output: Volume is 60.0
    }
}

7. Getter and Setter Methods: Controlled Access

Sometimes, you want to control how the properties (fields) of your class are accessed or modified. This is a key part of Encapsulation.

Often, you'll make your class properties private (we'll discuss access modifiers next) and then provide public methods to get (read) and set (write) their values.

  • Getter: A method that returns the value of a private field. Conventionally named getXxx() for field xxx (or isXxx() for booleans).

  • Setter: A method that sets or updates the value of a private field. Conventionally named setXxx(). Setters can include validation logic.

Access Modifiers

Access modifiers define the visibility/accessibility of classes, properties, and methods. Common ones are:

  • public: Accessible from any other class.

  • private: Accessible only within its own class. This is key for encapsulation.

  • protected: Accessible within its own package and by subclasses (even if in different packages).

  • Default (no modifier): Accessible only within its own package.

In simple terms: Anyone can access data members (properties) if they are public. But if they are private, they can only be accessed by methods within that same class.

Here's an example with getters and setters:

class Account {
    private String accountNumber; // private property
    private double balance;       // private property

    public Account(String accNum, double initialBalance) {
        this.accountNumber = accNum;
        if (initialBalance >= 0) {
            this.balance = initialBalance;
        } else {
            this.balance = 0;
            System.out.println("Initial balance cannot be negative. Set to 0.");
        }
    }
    // Getter for accountNumber (read-only after construction)
    public String getAccountNumber() {
        return this.accountNumber;
    }
    // Getter for balance
    public double getBalance() {
        return this.balance;
    }
    // Setter for balance (e.g., deposit)
    public void deposit(double amount) {
        if (amount > 0) {
            this.balance += amount;
            System.out.println(amount + " deposited. New balance: " + this.balance);
        } else {
            System.out.println("Deposit amount must be positive.");
        }
    }
    // Another "setter-like" method for withdrawal
    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
            System.out.println(amount + " withdrawn. New balance: " + this.balance);
            return true;
        } else if (amount > this.balance) {
            System.out.println("Insufficient funds for withdrawal of " + amount);
            return false;
        } else {
            System.out.println("Withdrawal amount must be positive.");
            return false;
        }
    }
}

public class BankDemo {
    public static void main(String[] args) {
        Account myAccount = new Account("ACC12345", 100.0);
        // System.out.println(myAccount.balance); // COMPILE ERROR! 'balance' is private
        System.out.println("Account: " + myAccount.getAccountNumber()); // OK
        System.out.println("Initial Balance: " + myAccount.getBalance()); // OK
        myAccount.deposit(50.0);    // 50.0 deposited. New balance: 150.0
        myAccount.withdraw(30.0);   // 30.0 withdrawn. New balance: 120.0
        myAccount.withdraw(150.0);  // Insufficient funds...
        myAccount.deposit(-10.0);   // Deposit amount must be positive.
        System.out.println("Final Balance: " + myAccount.getBalance()); // Final Balance: 120.0
    }
}

Why is this important (Encapsulation)?

  • Code Readability: Clearer how data is managed through dedicated methods.

  • Code Security/Integrity: Protects data from unintended direct modification. Allows for validation logic within setters (e.g., balance cannot be negative, age must be positive). You control how your data is changed.

  • Flexibility: You can change the internal implementation of how a property is stored or calculated without affecting the code that uses the getter/setter, as long as the method signature remains the same.

8. The static Keyword: Class-Level Members

When you put the static keyword on a class member (a variable or a method), it means that member belongs to the class itself, not to any particular instance (object) of the class.

  • Static Variables (Class Variables):

    • Shared among all objects of that class. There's only one copy of a static variable, regardless of how many objects are created.

    • If one object changes a static variable, the change is visible to all other objects.

    • Example: Person.populationCount. If john.populationCount and jane.populationCount are accessed, they refer to the same populationCount variable in the Person class.

  • Static Methods (Class Methods):

    • Can be called directly on the class name, without needing to create an object (e.g., Math.sqrt()).

    • Can only access static variables and other static methods of the class directly.

    • Cannot use the this keyword because this refers to an instance, and static methods are not tied to any specific instance.

    • Cannot directly access instance variables or instance methods (they would need an object reference to do so).

class Thing {
    String instanceName;       // Instance variable (each object has its own)
    static int count = 0;    // Static variable (shared by all Thing objects)
    public Thing(String name) {
        this.instanceName = name;
        count++; // Increment static count each time a Thing is created
        System.out.println(instanceName + " created. Total things: " + count);
    }
    // Instance method (operates on a specific object's data)
    public void displayName() {
        System.out.println("My instance name is: " + this.instanceName);
        // Can access static members from an instance method:
        // System.out.println("Current count from instance: " + count);
    }
    // Static method (belongs to the class)
    public static void displayCount() {
        System.out.println("Total things created so far: " + count);
        // System.out.println("My name is: " + this.instanceName); // ERROR! Cannot use 'this' here.
        // System.out.println(instanceName); // ERROR! Cannot access instance variable directly.
    }
}
public class StaticDemo {
    public static void main(String[] args) {
        Thing.displayCount(); // Call static method on class: Total things created so far: 0
        Thing t1 = new Thing("Widget"); // Widget created. Total things: 1
        Thing t2 = new Thing("Gadget"); // Gadget created. Total things: 2
        t1.displayName();     // My instance name is: Widget
        t2.displayName();     // My instance name is: Gadget
        Thing.displayCount(); // Total things created so far: 2
        // Access static variable via class name (recommended)
        System.out.println("Count via class: " + Thing.count); // Output: Count via class: 2
        // Or via an instance (not recommended as it can be misleading, implies it's an instance var)
        // System.out.println("Count via t1: " + t1.count); // Still accesses the static 'count'
    }
}

Correct way to deal with static variables/methods? Usually access static members using the class name (e.g., Thing.count, Thing.displayCount()), not through an instance variable (like t1.count), as the latter can be misleading.

Can't use this with static variable/method, why? The this keyword refers to the current instance of a class. Static variables and methods belong to the class itself, not to any specific instance. So, in a static context (like a static method or initializing a static variable), there is no "current instance" for this to refer to.

9. The 4 Musketeers of OOPs

1. Abstraction

Abstraction means hiding complex implementation details and showing only the essential features of an object. It focuses on what an object does rather than how it does it.

  • Example: When you drive a car, you interact with the steering wheel, accelerator, and brakes. You don't need to know the intricate mechanics of the engine, transmission, or braking system to operate the car. The car's internal complexity is abstracted away, providing you with a simplified interface.

  • In Code: Abstract classes and interfaces (discussed later) are primary tools for abstraction. They define a contract of what methods should be available, but the concrete implementation can vary.

2. Encapsulation

Encapsulation is the bundling of data (attributes/properties) and the methods that operate on that data into a single unit (a class). It also often involves restricting direct access to some of an object's components, a concept known as information hiding (achieved using access modifiers like private).

  • Example: A Capsule for medicine. The medicine (data) is contained within the capsule (object/class), and you interact with it in a controlled way (e.g., swallowing it – a method). You don't typically open it up and manipulate the raw ingredients.

  • In Code: We achieve encapsulation by:

    • Declaring instance variables as private.

    • Providing public getter and setter methods to control access and modification of these private variables. This protects the internal state of an object and ensures data integrity (e.g., a setter for age can prevent negative values).

If we didn't use encapsulation, we might have global variables and functions scattered everywhere, making the codebase confusing, hard to manage, and prone to errors where data is changed unexpectedly.

3. Inheritance

Inheritance is a mechanism where a new class (subclass or child class) derives properties and methods from an existing class (superclass or parent class). It promotes code reusability and establishes an "is-a" relationship (e.g., a Dog is an Animal).

It's just like in humans, where a child inherits features from their parents.

How this is done / Syntax (in Java): The extends keyword is used.

// ParentClass (Superclass)
class ParentClass {
    String familyName;
    public ParentClass(String familyName) {
        this.familyName = familyName;
        System.out.println("ParentClass constructor called for family: " + familyName);
    }
    void greet() {
        System.out.println("Hello from the family!");
    }
}
// ChildClass (Subclass) inherits from ParentClass
class ChildClass extends ParentClass {
    String childName;
    public ChildClass(String familyName, String childName) {
        super(familyName); // Calls the ParentClass constructor. MUST be the first line.
        this.childName = childName;
        System.out.println("ChildClass constructor called for: " + childName);
    }
    void introduce() {
        // Accesses familyName inherited from ParentClass
        System.out.println("I am " + childName + " of family " + familyName + ".");
    }
    // Method Overriding: ChildClass provides its own version of greet()
    @Override
    void greet() {
        super.greet(); // Optionally call the parent's version
        System.out.println("And a special hello from " + childName + "!");
    }
}
public class InheritanceDemo {
    public static void main(String[] args) {
        ChildClass child = new ChildClass("Smith", "John");
        // Output from constructors:
        // ParentClass constructor called for family: Smith
        // ChildClass constructor called for: John
        child.greet();
        // Output:// Hello from the family!// And a special hello from John!
        child.introduce();
        // Output: I am John of family Smith.
        System.out.println(child.childName + " " + child.familyName); // Accessing properties // Output: John Smith
    }
}

What a child can access: This depends on access modifiers in the parent class:

  • public members: Accessible by the child class.

  • protected members: Accessible by the child class (even if in a different package).

  • private members: Not directly accessible by the child class. The child class must use public or protected getters/setters if provided by the parent to interact with private parent members.

  • Default (package-private) members: Accessible if the child class is in the same package.

Types of Inheritance Java Supports:

  • Single Inheritance: A class can inherit from only one superclass. (e.g., Class B extends Class A)

  • Multilevel Inheritance: A class can inherit from a class that itself inherits from another class. (e.g., Class C extends Class B, and Class B extends Class A. So C -> B -> A)

  • Hierarchical Inheritance: One parent class can have multiple child classes. (e.g., Class B extends Class A and Class C extends Class A)

Java does not support multiple inheritance of classes (a class inheriting from two or more unrelated classes directly, e.g., Class C extends Class A, Class B). This is primarily to avoid the "diamond problem" (ambiguity if both A and B have a method with the same signature). Multiple inheritance of type can be achieved using interfaces (covered next).

Overriding: As seen in the example, a child class can provide a specific implementation for a method that is already defined in its parent class. This is method overriding.

  • Example: A parent Car class has a generic accelerate() method. A child ElectricCar class might override accelerate() to describe silent electric motor acceleration, while a SportsCar child class might override it to describe loud engine revving.

4. Polymorphism

Polymorphism basically means "many forms." It allows objects of different classes to respond to the same method call in different ways. It's a powerful concept that enables flexibility and extensibility.

Types of Polymorphism:

  • Compile-time Polymorphism (Method Overloading):

    • Already discussed. The compiler decides which method to call at compile time based on the method signature (name and parameter list).

    • Example: Having calculator.add(int a, int b) and calculator.add(double a, double b).

  • Run-time Polymorphism (Method Overriding):

    • This is a core concept, achieved through inheritance and overriding.

    • A subclass provides a specific implementation for a method that is already defined in its superclass.

    • The decision of which method version to execute (parent's or child's) is made at runtime based on the actual object type, not the reference variable's type.

Example (Method Overriding for Run-time Polymorphism):

class Animal {
    void makeSound() {
        System.out.println("Generic animal sound");
    }
}
class Dog extends Animal {
    @Override // Good practice
    void makeSound() {
        System.out.println("Woof woof!");
    }
}
class Cat extends Animal {
    @Override // Good practice
    void makeSound() {
        System.out.println("Meow!");
    }
}
public class PolymorphismDemo {
    public static void main(String[] args) {
        // 'myAnimal' is an Animal reference, but it can point to Dog or Cat objects
        Animal myAnimal;
        myAnimal = new Dog(); // myAnimal now refers to a Dog object
        myAnimal.makeSound();   // Calls Dog's makeSound() -> Output: Woof woof!
        myAnimal = new Cat(); // myAnimal now refers to a Cat object
        myAnimal.makeSound();   // Calls Cat's makeSound() -> Output: Meow!
        Animal anotherAnimal = new Animal();
        anotherAnimal.makeSound(); // Calls Animal's makeSound() -> Output: Generic animal sound
    }
}

In this example, myAnimal.makeSound() calls a different actual method depending on whether myAnimal currently holds a Dog object or a Cat object. This decision is made at runtime.

10. abstract Keyword: Classes and Methods

The abstract keyword is used for classes and methods to achieve abstraction.

Abstract Class:

  • An abstract class cannot be instantiated (can't create objects of an abstract class directly using new).

  • It can have both abstract methods (methods without a body) and concrete methods (methods with implementation).

  • It's designed to be subclassed. Subclasses must implement all abstract methods of the parent abstract class, or they too must be declared abstract.

  • Why is it important? To stop the user from creating an object of a parent class that is too generic to be useful on its own.

    • Example: Shape could be an abstract class. You don't create a generic "Shape"; you create specific objects like Circle or Square (which extend Shape).

    • If a class like Vehicle is not abstract, we can make objects of Vehicle and also objects of Car (a subclass). But if Vehicle is just a concept, we might want to discourage creating generic Vehicle objects and only allow specific types like Car or Motorcycle.

Abstract Method:

  • An abstract method is declared without an implementation (no body, ends with a semicolon).

      public abstract void draw(); // Notice the semicolon and no {}
    
  • It can only exist within an abstract class (or an interface, where methods are implicitly abstract by default before Java 8).

  • It forces subclasses to provide a concrete implementation for that method. It defines a contract that subclasses must fulfill.

Example: Abstract Class and Methods

What about an abstract method in a parent class? So what to do? Subclasses must implement it.

// Abstract class
abstract class Vehicle {
    String modelName;
    boolean isEngineOn;
    // Concrete method (constructor)
    public Vehicle(String modelName) {
        this.modelName = modelName;
        this.isEngineOn = false;
        System.out.println(modelName + " vehicle blueprint created.");
    }
    // Abstract methods - no implementation here, subclasses must provide it
    public abstract void startEngine();
    public abstract void stopEngine();
    // Concrete method
    public void honk() {
        System.out.println("Generic vehicle honk!");
    }
    public String getModelName() {
        return modelName;
    }
}
// Concrete subclass Car
class Car extends Vehicle {
    public Car(String modelName) {
        super(modelName); // Call superclass constructor
    }
    @Override
    public void startEngine() {
        if (!isEngineOn) {
            isEngineOn = true;
            System.out.println(getModelName() + " car engine started with a key turn.");
        } else {
            System.out.println(getModelName() + " car engine is already on.");
        }
    }
    @Override
    public void stopEngine() {
        if (isEngineOn) {
            isEngineOn = false;
            System.out.println(getModelName() + " car engine stopped.");
        } else {
            System.out.println(getModelName() + " car engine is already off.");
        }
    }
    // Car can also override concrete methods if needed
    @Override
    public void honk() {
        System.out.println(getModelName() + " car says: Beep beep!");
    }
}
// Concrete subclass Motorcycle
class Motorcycle extends Vehicle {
    public Motorcycle(String modelName) {
        super(modelName);
    }
    @Override
    public void startEngine() {
        if(!isEngineOn){
            isEngineOn = true;
            System.out.println(getModelName() + " motorcycle engine started with a kick.");
        } else {
            System.out.println(getModelName() + " motorcycle engine is already on.");
        }
    }
    @Override
    public void stopEngine() {
        if(isEngineOn){
            isEngineOn = false;
            System.out.println(getModelName() + " motorcycle engine stopped.");
        } else {
             System.out.println(getModelName() + " motorcycle engine is already off.");
        }
    }
    // Motorcycle will use the generic honk() from Vehicle unless overridden
}
public class AbstractDemo {
    public static void main(String[] args) {
        // Vehicle myVehicle = new Vehicle("SomeVehicle"); // ERROR! Cannot instantiate abstract class
        Vehicle myCar = new Car("Sedan"); // Sedan vehicle blueprint created.
        myCar.startEngine();              // Sedan car engine started with a key turn.
        myCar.honk();                     // Sedan car says: Beep beep!
        myCar.stopEngine();               // Sedan car engine stopped.
        Vehicle myBike = new Motorcycle("SportBike"); // SportBike vehicle blueprint created.
        myBike.startEngine();                       // SportBike motorcycle engine started with a kick.
        myBike.honk();                              // Generic vehicle honk! (Motorcycle didn't override it)
        myBike.stopEngine();                        // SportBike motorcycle engine stopped.
    }
}

11. interface: Pure Contracts

What is an interface? Maybe something related to structure? Yes, an interface in Java is a reference type, similar to a class, that is primarily a collection of abstract methods. A class implements an interface, thereby inheriting the abstract methods of the interface and promising to provide implementations for them.

  • It defines a contract that implementing classes must adhere to.

  • A class can implement multiple interfaces (this is how Java achieves a form of multiple inheritance of type or behavior, not state).

  • Before Java 8:

    • Methods in an interface were implicitly public and abstract. You couldn't have concrete methods.

    • Variables declared in an interface were implicitly public, static, and final (constants).

  • Java 8 and later:

    • Interfaces can have default methods (concrete methods with an implementation) and static methods (also with implementation). This provides more flexibility.

And when using an interface, we need to override the existing (abstract) functions. But why? Because the interface only defines what methods a class should have (the contract), not how they should be implemented. The implementing class is responsible for providing the actual logic.

We can use many interfaces in one class.

Example: Interfaces

// Interface 1: Defines actions a person can take
interface IPersonActions {
    // All fields in an interface are implicitly public static final
    // int MAX_AGE = 120; // Example of a constant

    // All methods (pre-Java 8) are implicitly public abstract
    boolean canVote(int currentAge);
    void performDailyActivity();
}

// Interface 2: Defines actions a worker can take
interface IWorkerActions {
    void goToWork();
    double calculateSalary(int hoursWorked);
}

// A class can implement multiple interfaces
class Person implements IPersonActions, IWorkerActions {
    String name;
    int age;
    double hourlyRate;

    public Person(String name, int age, double hourlyRate) {
        this.name = name;
        this.age = age;
        this.hourlyRate = hourlyRate;
    }

    // Implementing methods from IPersonActions
    @Override
    public boolean canVote(int currentAge) { // Parameter name can be different
        return currentAge >= 18;
    }

    @Override
    public void performDailyActivity() {
        System.out.println(name + " is eating and sleeping.");
    }

    // Implementing methods from IWorkerActions
    @Override
    public void goToWork() {
        System.out.println(name + " is going to work.");
    }

    @Override
    public double calculateSalary(int hoursWorked) {
        // double rate = 15.0; // Example rate from notes, better to make it a property
        return hoursWorked * this.hourlyRate;
    }

    // A regular method of the Person class
    public void introduce() {
        System.out.println("Hi, I'm " + name + " and I am " + age + " years old.");
    }
}

public class InterfaceDemo {
    public static void main(String[] args) {
        Person shiv = new Person("Shiv", 25, 20.0); // Name, Age, Hourly Rate

        shiv.introduce(); // Hi, I'm Shiv and I am 25 years old.

        System.out.println("Can Shiv vote? " + shiv.canVote(shiv.age)); // Can Shiv vote? true
        shiv.performDailyActivity(); // Shiv is eating and sleeping.
        shiv.goToWork();             // Shiv is going to work.

        System.out.println("Shiv's salary for 40 hours: " + shiv.calculateSalary(40));
        // Shiv's salary for 40 hours: 800.0

        // You can also have references of interface type
        IPersonActions actions = shiv; // 'actions' can only see IPersonActions methods
        actions.performDailyActivity(); // Shiv is eating and sleeping.
        // actions.goToWork(); // COMPILE ERROR! goToWork() is not in IPersonActions

        IWorkerActions worker = shiv;
        System.out.println("Salary via worker ref: " + worker.calculateSalary(10)); // Salary via worker ref: 200.0
    }
}

12. Other Important Keywords

final keyword

The final keyword can be used in several contexts to denote "unchangeability":

  • final variable: Its value cannot be changed once assigned. It creates a constant. Must be initialized when declared or in the constructor.

      final double PI = 3.14159;
      // PI = 3.14; // ERROR! Cannot assign a value to final variable PI
    
  • final method: A final method cannot be overridden by subclasses.

      class Base {
          final void show() {
              System.out.println("Base::show() called - I am final!");
          }
      }
    
      class Derived extends Base {
          // void show() { } // ERROR! show() in Derived cannot override show() in Base;
                            // overridden method is final
      }
    
  • final class: A final class cannot be subclassed (inherited from). This is often done for security or to ensure immutability.

      final class Immutable {
          // ... class members ...
      }
    
      // class MoreImmutable extends Immutable {} // ERROR! Cannot inherit from final Immutable
    

    The String class in Java is a good example of a final class.

super keyword

The super keyword is a reference variable that is used to refer to the immediate parent class object.

  • super to call parent class constructor: super() is used to call the constructor of the immediate parent class.

    • It must be the first statement in a subclass constructor.

    • If you don't explicitly call super() in a subclass constructor, Java implicitly inserts a call to the parent's no-argument super() constructor. If the parent doesn't have a no-arg constructor, you'll get a compile error unless you explicitly call another super(...) constructor.

    class Parent {
        String message;
        Parent() {
            this.message = "Default Parent";
            System.out.println("Parent constructor called (default)");
        }
        Parent(String msg) {
            this.message = msg;
            System.out.println("Parent constructor with message: " + msg);
        }
    }

    class Child extends Parent {
        Child() {
            // Implicit call to super() here if not specified.
            // super(); // Explicitly calls Parent's no-arg constructor
            System.out.println("Child constructor called (default). Parent message: " + super.message);
        }
        Child(String childMsg, String parentMsg) {
            super(parentMsg); // Calls Parent(String msg) constructor. Must be first.
            System.out.println("Child constructor with message: " + childMsg + ". Parent message: " + super.message);
        }
    }
    // public class SuperConstructorDemo { ... main method with new Child()... }
    // Output when `new Child("Hi Child", "Hi Parent")` is created:
    // Parent constructor with message: Hi Parent
    // Child constructor with message: Hi Child. Parent message: Hi Parent
  • super to call parent class methods: Used if the subclass has overridden a parent class method and you still want to call the parent's version from within the subclass's overriding method.

      class Animal {
          void eat() {
              System.out.println("Animal is eating");
          }
      }
      class Dog extends Animal {
          @Override
          void eat() {
              super.eat(); // Calls Animal's eat() method
              System.out.println("Dog is eating bones");
          }
      }
      // Output when dog.eat() is called:
      // Animal is eating
      // Dog is eating bones
    
  • super to access parent class instance variables: If a child and parent have instance variables with the same name (name shadowing - generally discouraged), super.variableName refers to the parent's variable, and this.variableName (or just variableName) refers to the child's.

13. instanceof operator

The instanceof operator is used to test whether an object is an instance of a particular class, a subclass of that class, or an instance of a class that implements a particular interface. It returns a boolean value.

interface Runnable {} // A simple marker interface

class Animal {}
class Dog extends Animal implements Runnable {}
class Cat extends Animal {}

public class InstanceOfDemo {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        Animal myAnimal = myDog; // Upcasting: Dog is an Animal
        Cat myCat = new Cat();

        System.out.println("myDog instanceof Dog: " + (myDog instanceof Dog));             // true
        System.out.println("myDog instanceof Animal: " + (myDog instanceof Animal));         // true
        System.out.println("myDog instanceof Runnable: " + (myDog instanceof Runnable));     // true
        System.out.println("myDog instanceof Cat: " + (myDog instanceof Cat));             // false (compile error with unrelated types if not in hierarchy)

        System.out.println("myAnimal instanceof Dog: " + (myAnimal instanceof Dog));         // true (myAnimal currently refers to a Dog)
        System.out.println("myAnimal instanceof Animal: " + (myAnimal instanceof Animal));     // true
        System.out.println("myAnimal instanceof Cat: " + (myAnimal instanceof Cat));         // false

        // System.out.println("myAnimal instanceof String: " + (myAnimal instanceof String)); // COMPILE ERROR: Incompatible types

        Animal anotherAnimal = new Animal();
        System.out.println("anotherAnimal instanceof Dog: " + (anotherAnimal instanceof Dog)); // false
        System.out.println("anotherAnimal instanceof Animal: " + (anotherAnimal instanceof Animal)); // true

        // Useful for safe casting
        if (myAnimal instanceof Dog) {
            Dog specificDog = (Dog) myAnimal; // Safe to cast
            System.out.println("Successfully cast myAnimal to Dog.");
        }
    }
}

14. Inner Classes (Nested Classes)

Java allows you to define a class within another class. Such a class is called a nested class or inner class. They are useful for:

  • Logically grouping classes that are only used in one place.

  • Increasing encapsulation.

  • Creating more readable and maintainable code.

There are a few types:

1. Regular Inner Class (or Non-Static Nested Class)

  • Defined within another class without the static keyword.

  • An object of an inner class can exist only within an instance of the outer class.

  • It has access to all members (including private) of the outer class instance.

  • To instantiate, you need an instance of the Outer class: OuterClass.InnerClass innerObj = outerObj.new InnerClass();

class Outer {
    private int outerVar = 10;
    private String outerMessage = "Hello from Outer!";

    // Regular Inner Class
    class Inner {
        void display() {
            // Can access outer class's private members
            System.out.println("Outer variable: " + outerVar);
            System.out.println("Outer message: " + outerMessage);
            // System.out.println("Accessing outer this: " + Outer.this.outerVar); // explicit way
        }
    }

    void testInner() {
        Inner innerObj = new Inner(); // Instantiating Inner from within Outer
        innerObj.display();
    }
}

public class RegularInnerDemo {
    public static void main(String[] args) {
        Outer outerObj = new Outer();
        outerObj.testInner();
        // Output:
        // Outer variable: 10
        // Outer message: Hello from Outer!

        // To instantiate Inner from outside Outer (but still need an Outer instance):
        Outer.Inner innerObjExternally = outerObj.new Inner();
        innerObjExternally.display();
        // Output:
        // Outer variable: 10
        // Outer message: Hello from Outer!
    }
}

2. Static Nested Class

  • A nested class declared with the static keyword.

  • It does not have access to instance members (non-static fields and methods) of the outer class. It can only access static members of the outer class.

  • It's more like a regular top-level class that has been nested for packaging convenience or to associate it closely with its outer class.

  • It does not need an instance of the outer class to be instantiated: OuterClass.StaticNestedClass nestedObj = new OuterClass.StaticNestedClass();

class OuterStatic {
    private static int staticOuterVar = 20;
    private int instanceOuterVar = 25; // Non-static

    // Static Nested Class
    static class StaticNested {
        void display() {
            System.out.println("Static Outer variable: " + staticOuterVar);
            // System.out.println(instanceOuterVar); // ERROR! Cannot access non-static member
        }
    }
}

public class StaticNestedDemo {
    public static void main(String[] args) {
        // To instantiate:
        OuterStatic.StaticNested nestedObj = new OuterStatic.StaticNested();
        nestedObj.display(); // Output: Static Outer variable: 20
    }
}

3. Local Inner Class

  • Defined within a method body.

  • It's only visible within the scope of that method.

  • Can access local variables of the method only if they are final or effectively final (Java 8+). (Effectively final means the variable's value is never changed after initialization).

  • Cannot be declared public, private, protected, or static.

class OuterLocal {
    void myMethod() {
        final int localVar = 30; // Must be final or effectively final
        String effectivelyFinalVar = "Hello from method!";
        // localVar = 31; // This would make localVar not effectively final

        // Local Inner Class
        class LocalInner {
            void display() {
                System.out.println("Local variable from method: " + localVar);
                System.out.println("Effectively final variable: " + effectivelyFinalVar);
                // Can also access OuterLocal's instance members if OuterLocal is not static context
            }
        }

        LocalInner li = new LocalInner();
        li.display();
    }
}

public class LocalInnerDemo {
    public static void main(String[] args) {
        OuterLocal ol = new OuterLocal();
        ol.myMethod();
        // Output:
        // Local variable from method: 30
        // Effectively final variable: Hello from method!
    }
}

4. Anonymous Inner Class

  • An inner class without a name.

  • It's declared and instantiated in a single statement.

  • Typically used for creating objects of an interface or an abstract class "on the fly," often for event handlers or simple, one-off implementations.

  • They are expressions, so they must be part of a statement.

interface Greeter {
    void greet();
    void farewell(String name);
}

abstract class SpecialGreeter {
    abstract void specialGreeting();
    void commonMessage() {
        System.out.println("This is a common message.");
    }
}

public class AnonymousInnerDemo {
    public static void main(String[] args) {
        // Anonymous Inner Class implementing an interface
        Greeter englishGreeting = new Greeter() {
            @Override
            public void greet() {
                System.out.println("Hello!");
            }
            @Override
            public void farewell(String name) {
                System.out.println("Goodbye, " + name + "!");
            }
        }; // Semicolon is part of the assignment statement

        englishGreeting.greet();    // Output: Hello!
        englishGreeting.farewell("Alice"); // Output: Goodbye, Alice!

        // Anonymous Inner Class extending an abstract class
        SpecialGreeter mySpecialGreeter = new SpecialGreeter() {
            @Override
            void specialGreeting() {
                System.out.println("A very special anonymous greeting!");
            }
        };

        mySpecialGreeter.specialGreeting(); // A very special anonymous greeting!
        mySpecialGreeter.commonMessage();   // This is a common message.

        // Anonymous Inner Class extending a concrete class (less common but possible)
        // Example: Runnable for a thread
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous Runnable running!");
            }
        };
        Thread t = new Thread(r);
        t.start(); // Output: Anonymous Runnable running!
    }
}
0
Subscribe to my newsletter

Read articles from Shivprasad Roul directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shivprasad Roul
Shivprasad Roul

Software developer with a strong foundation in React, Node.js, PostgreSQL, and AI-driven applications. Experienced in remote sensing, satellite image analysis, and vector databases. Passionate about defense tech, space applications, and problem-solving. Currently building AI-powered solutions and preparing for a future in special forces.