Skip to content

OOPS Concepts

A class is a blueprint or template for creating objects. It defines the properties (fields) and behaviors (methods) that the objects created from the class can have. In Java, a class is defined using the class keyword.

public class Car {
// Fields (properties)
String color;
String model;
// Methods (behaviors)
void drive() {
System.out.println("The car is driving.");
}
void stop() {
System.out.println("The car has stopped.");
}
}

An object is an instance of a class. It is created from a class and has its own set of values for the properties defined in the class. In Java, an object is created using the new keyword.

public class Main {
public static void main(String[] args) {
// Creating an object of the Car class
Car myCar = new Car();
myCar.color = "Red";
myCar.model = "Tesla Model S";
// Calling methods on the object
myCar.drive(); // Output: The car is driving.
myCar.stop(); // Output: The car has stopped.
System.out.println("Car Model: " + myCar.model); // Output: Car Model: Tesla Model S
System.out.println("Car Color: " + myCar.color); // Output: Car Color: Red
}
}
  1. Encapsulation
  2. Inheritance
  3. Polymorphism
  4. Abstraction

Encapsulation is one of the four fundamental OOP concepts in Java. It’s the practice of bundling data (variables) and the methods that operate on that data within a single unit (class), while restricting direct access to some of the object’s components.

  1. Information hiding: Encapsulation hides the internal state of an object by making variables private, preventing unauthorized direct access from outside the class.
  2. Access control: Using access modifiers (private, protected, public, default) to control the visibility and accessibility of class members.
  3. Data protection: Encapsulation prevents the object’s data from being modified unexpectedly by providing controlled access through methods.
  4. API definition: By determining what’s exposed and what’s hidden, encapsulation defines the public interface (API) of a class.
public class BankAccount {
// Private variables - hidden internal state
private String accountNumber;
private double balance;
private String ownerName;
// Constructor
public BankAccount(String accountNumber, String ownerName) {
this.accountNumber = accountNumber;
this.ownerName = ownerName;
this.balance = 0.0;
}
// Public getters - controlled access to data
public String getAccountNumber() {
return accountNumber;
}
public String getOwnerName() {
return ownerName;
}
public double getBalance() {
return balance;
}
// Public methods - controlled operations on data
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public boolean withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
return true;
}
return false;
}
}
  1. Controlled data access: Changes to the object’s state happen through well-defined methods
  2. Data validation: Input can be validated before changing object state
  3. Flexibility: Internal implementation can change without affecting external code
  4. Maintainability: Easier to debug and maintain when data access is controlled
  5. Security: Sensitive data can be protected from unauthorized access

Encapsulation is a key pillar of good object-oriented design, allowing you to create robust and maintainable code by controlling how data is accessed and modified.

Encapsulation is the bundling of data and methods that operate on that data within a single unit (like a class), whereas data hiding, a core component of encapsulation, restricts direct access to the internal data of that unit, protecting it from external modification. 

Inheritance enables a class to acquire properties and behaviors from another class, promoting code reuse and establishing an “is-a” relationship.

Key aspects:

  • Achieved using the extends keyword
  • Subclasses inherit accessible members of superclass
  • Method overriding allows customized behavior
public class Vehicle {
protected String brand;
public void start() {
System.out.println("Vehicle starting");
}
}
public class Car extends Vehicle {
private int numDoors;
@Override
public void start() {
System.out.println("Car engine starting");
}
}

Polymorphism allows objects to be treated as instances of their parent class rather than their actual class, enabling one interface to represent different underlying forms.

Key aspects:

  • Method overriding (runtime polymorphism)
  • Method overloading (compile-time polymorphism)
  • Reference variable of parent type can refer to child object
// Method overriding example
Vehicle vehicle = new Car(); // Car reference through Vehicle type
vehicle.start(); // Calls Car's implementation
// Method overloading example
public class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public int add(int a, int b, int c) { return a + b + c; }
}

Abstraction focuses on essential qualities rather than specific details, hiding complex implementation details and showing only functionality to users.

Key aspects:

  • Implemented using abstract classes and interfaces
  • Defines what a class must do, not how it does it
  • Simplifies complex systems by breaking them into manageable parts
// Abstract class example
public abstract class Shape {
abstract double calculateArea();
public void display() {
System.out.println("Area: " + calculateArea());
}
}
// Interface example
public interface Drawable {
void draw();
default void setColor(String color) {
System.out.println("Setting color to " + color);
}
}

An abstract class is a class that cannot be instantiated and is designed to be subclassed. It can contain both abstract methods (methods without a body) and concrete methods (methods with implementation).

  1. Partial Implementation: Can contain both abstract and concrete methods
  2. Constructor: Can have constructors (though they can only be called from subclasses)
  3. Access Modifiers: Methods can have any access modifier (public, protected, private, default)
  4. Fields: Can have instance variables (fields) with any access modifier
  5. Inheritance: A class can extend only one abstract class (single inheritance for classes)
  6. Purpose: Used when related classes need to share code but also have unique implementations
public abstract class Database {
protected String connectionString;
private boolean isConnected;
// Constructor
public Database(String connectionString) {
this.connectionString = connectionString;
this.isConnected = false;
}
// Abstract method - must be implemented by subclasses
public abstract void executeQuery(String query);
// Concrete method - shared implementation
public void connect() {
System.out.println("Connecting to " + connectionString);
isConnected = true;
}
public void disconnect() {
if (isConnected) {
System.out.println("Disconnecting from database");
isConnected = false;
}
}
// Protected method - accessible to subclasses
protected boolean isConnected() {
return isConnected;
}
}
// Concrete subclass
public class MySQLDatabase extends Database {
public MySQLDatabase(String connectionString) {
super(connectionString); // Call to parent constructor
}
@Override
public void executeQuery(String query) {
if (isConnected()) {
System.out.println("Executing MySQL query: " + query);
} else {
System.out.println("Not connected to database");
}
}
}

An interface is a completely abstract type that defines a contract for classes to implement. It specifies what a class must do, but not how it does it.

  1. All Abstract Methods: Traditionally, all methods are implicitly abstract and public (prior to Java 8)
  2. No Constructors: Cannot have constructors
  3. Constants Only: Can only have constants (public static final fields)
  4. Multiple Inheritance: A class can implement multiple interfaces
  5. Default Methods: Since Java 8, can have default and static methods with implementations
  6. Private Methods: Since Java 9, can have private methods for code reuse within the interface
  7. Purpose: Used to define a contract that unrelated classes can implement
public interface Payable {
// Constants
double MINIMUM_WAGE = 15.0; // implicitly public static final
// Abstract methods
double calculatePay(); // implicitly public abstract
void processPay();
// Default method (Java 8+)
default void printPayStub() {
System.out.println("Payment amount: $" + calculatePay());
}
// Static method (Java 8+)
static boolean isValidPayAmount(double amount) {
return amount >= MINIMUM_WAGE;
}
// Private method (Java 9+)
private void internalHelperMethod() {
// Code reuse within interface
}
}
// Implementation
public class Employee implements Payable {
private double hourlyRate;
private int hoursWorked;
public Employee(double hourlyRate, int hoursWorked) {
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
@Override
public double calculatePay() {
return hourlyRate * hoursWorked;
}
@Override
public void processPay() {
System.out.println("Processing payment for employee");
}
// We can use the default implementation of printPayStub()
// or override it if needed
}
  • Use Abstract Classes When:

    • You want to share code among closely related classes
    • You need to declare non-public members
    • You want to provide a partial implementation
    • Your classes need to share common state or behavior
  • Use Interfaces When:

    • You want to define a contract for unrelated classes
    • You need multiple inheritance
    • You want to specify behavior without implementation details
    • You’re designing for API stability

A functional interface is an interface with exactly one abstract method. These interfaces can be used with lambda expressions and method references.

// Functional interface (marked with @FunctionalInterface annotation)
@FunctionalInterface
public interface Transformer<T, R> {
R transform(T input);
// Can still have default and static methods
default void printInfo() {
System.out.println("This is a transformer");
}
}
// Usage with lambda expression
public class FunctionalInterfaceExample {
public static void main(String[] args) {
// Implementation using lambda expression
Transformer<String, Integer> lengthFinder = s -> s.length();
// Using the implementation
int length = lengthFinder.transform("Hello, World!");
System.out.println("Length: " + length); // Output: Length: 13
// Method reference equivalent
Transformer<String, Integer> lengthFinder2 = String::length;
}
}

Java provides several predefined functional interfaces in the java.util.function package:

  • Predicate<T>: Tests if an input is true or false
  • Function<T, R>: Applies a function to an input and returns a result
  • Consumer<T>: Performs an action on an input
  • Supplier<T>: Returns a result without an input
  • BiFunction<T, U, R>: Applies a function to two inputs and returns a result
  • UnaryOperator<T>: Applies a function to a single input of the same type
  • BinaryOperator<T>: Applies a function to two inputs of the same type
// Using Predicate
Predicate<String> isEmpty = s -> s.isEmpty();
// Using Function
Function<String, Integer> length = s -> s.length();
// Using Consumer
Consumer<String> print = s -> System.out.println(s);
// Using Supplier
Supplier<String> randomString = () -> "Random";
// Using BiFunction
BiFunction<String, String, String> concat = (s1, s2) -> s1 + s2;
// Using UnaryOperator
UnaryOperator<String> reverse = s -> new StringBuilder(s).reverse().toString();
// Using BinaryOperator
BinaryOperator<String> append = (s1, s2) -> s1 + s2;

Java supports various types of inheritance patterns, each with specific use cases and characteristics.

A class inherits from only one superclass. This is the most common form of inheritance in Java.

public class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
public class Dog extends Animal { // Single inheritance
public void bark() {
System.out.println("Dog is barking");
}
}

A class inherits from a class which itself inherits from another class, forming a chain of inheritance.

public class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
public class Mammal extends Animal {
public void breathe() {
System.out.println("Mammal is breathing");
}
}
public class Dog extends Mammal { // Multilevel inheritance
public void bark() {
System.out.println("Dog is barking");
}
}
// Usage
Dog dog = new Dog();
dog.eat(); // From Animal
dog.breathe(); // From Mammal
dog.bark(); // From Dog

Multiple classes inherit from a single superclass.

public class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
public class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
public class Cat extends Animal { // Another subclass of Animal
public void meow() {
System.out.println("Cat is meowing");
}
}

4. Multiple Inheritance (Through Interfaces)

Section titled “4. Multiple Inheritance (Through Interfaces)”

Java doesn’t support multiple inheritance of classes (to avoid the “diamond problem”), but it allows multiple inheritance through interfaces.

public interface Swimmer {
void swim();
}
public interface Flyer {
void fly();
}
// Duck implements multiple interfaces
public class Duck implements Swimmer, Flyer {
@Override
public void swim() {
System.out.println("Duck is swimming");
}
@Override
public void fly() {
System.out.println("Duck is flying");
}
}

A combination of two or more types of inheritance. In Java, this is typically achieved using a mix of class inheritance and interface implementation.

public class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
public interface Swimmer {
void swim();
}
public interface Flyer {
void fly();
}
public class Bird extends Animal implements Flyer {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
public class Penguin extends Bird implements Swimmer {
@Override
public void fly() {
System.out.println("Penguin cannot fly");
}
@Override
public void swim() {
System.out.println("Penguin is swimming");
}
}

Java avoids the “diamond problem” (ambiguity that arises when a class inherits from two classes with a common ancestor) by not supporting multiple inheritance of classes.

However, with interfaces, Java 8+ provides a solution using the super keyword to explicitly call the desired implementation:

public interface A {
default void show() {
System.out.println("A's show");
}
}
public interface B extends A {
default void show() {
System.out.println("B's show");
}
}
public interface C extends A {
default void show() {
System.out.println("C's show");
}
}
// This would cause a compilation error without explicit override
public class D implements B, C {
// Must override to resolve the conflict
@Override
public void show() {
B.super.show(); // Explicitly call B's implementation
// or C.super.show(); to call C's implementation
}
}

Inner classes are classes defined within other classes. They allow for logical grouping of classes and increased encapsulation.

A non-static class defined at the member level of another class. It has access to all members (including private) of the outer class.

public class OuterClass {
private int outerField = 10;
// Member inner class
public class InnerClass {
public void display() {
// Can access private members of outer class
System.out.println("Outer field value: " + outerField);
}
}
public void createInner() {
InnerClass inner = new InnerClass();
inner.display();
}
}
// Creating an instance of inner class from outside
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.display();

A static class defined at the member level of another class. It cannot access non-static members of the outer class directly.

public class OuterClass {
private static int staticOuterField = 20;
private int instanceOuterField = 30;
// Static nested class
public static class StaticNestedClass {
public void display() {
// Can access static members of outer class
System.out.println("Static outer field: " + staticOuterField);
// Cannot access instance members directly
// System.out.println(instanceOuterField); // Error
}
}
}
// Creating an instance of static nested class
OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();
nested.display();

A class defined within a method or block. It can access all members of the enclosing class and final or effectively final local variables of the enclosing method.

public class OuterClass {
private int outerField = 40;
public void method(final int param) {
final int localVar = 50;
int effectivelyFinalVar = 60; // Effectively final (not modified after initialization)
// Local inner class
class LocalInnerClass {
public void display() {
// Can access outer class members
System.out.println("Outer field: " + outerField);
// Can access final or effectively final local variables
System.out.println("Method parameter: " + param);
System.out.println("Local variable: " + localVar);
System.out.println("Effectively final: " + effectivelyFinalVar);
}
}
// Create and use the local inner class
LocalInnerClass local = new LocalInnerClass();
local.display();
}
}

A class defined without a name, typically used to create a one-time implementation of an interface or extension of a class.

public interface Clickable {
void onClick();
}
public class Button {
private Clickable clickHandler;
public void setClickHandler(Clickable handler) {
this.clickHandler = handler;
}
public void click() {
if (clickHandler != null) {
clickHandler.onClick();
}
}
}
// Using anonymous inner class
Button button = new Button();
// Anonymous inner class implementing Clickable
button.setClickHandler(new Clickable() {
@Override
public void onClick() {
System.out.println("Button clicked!");
}
});
button.click(); // Output: Button clicked!
// With Java 8+ lambda (for functional interfaces)
button.setClickHandler(() -> System.out.println("Button clicked with lambda!"));
  1. Encapsulation: Inner classes can access private members of the outer class
  2. Logical grouping: Related classes can be kept together
  3. More readable and maintainable code: Helper classes can be defined close to where they’re used
  4. Reduced code complexity: Anonymous classes reduce the need for separate class files for simple implementations

Constructor chaining is the process of calling one constructor from another constructor within the same class or from a parent class.

1. Constructor Chaining Within the Same Class (this())

Section titled “1. Constructor Chaining Within the Same Class (this())”

Using this() to call another constructor in the same class.

public class Person {
private String name;
private int age;
private String address;
// Primary constructor
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
// Calls the primary constructor
public Person(String name, int age) {
this(name, age, "Unknown"); // Constructor chaining
}
// Calls the two-parameter constructor
public Person(String name) {
this(name, 0); // Constructor chaining
}
// Default constructor
public Person() {
this("John Doe"); // Constructor chaining
}
public void displayInfo() {
System.out.println("Name: " + name + ", Age: " + age + ", Address: " + address);
}
}

2. Constructor Chaining to Parent Class (super())

Section titled “2. Constructor Chaining to Parent Class (super())”

Using super() to call a constructor in the parent class.

public class Vehicle {
private String make;
private String model;
private int year;
public Vehicle(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
public Vehicle() {
this("Unknown", "Unknown", 2023);
}
public void displayInfo() {
System.out.println("Vehicle: " + year + " " + make + " " + model);
}
}
public class Car extends Vehicle {
private int numDoors;
private String engineType;
public Car(String make, String model, int year, int numDoors, String engineType) {
super(make, model, year); // Call to parent constructor
this.numDoors = numDoors;
this.engineType = engineType;
}
public Car(String make, String model, int year) {
this(make, model, year, 4, "Gasoline"); // Call to another constructor in same class
}
public Car() {
super(); // Call to parent's default constructor
this.numDoors = 4;
this.engineType = "Gasoline";
}
@Override
public void displayInfo() {
super.displayInfo(); // Call parent method
System.out.println("Doors: " + numDoors + ", Engine: " + engineType);
}
}
  1. this() or super() must be the first statement in a constructor
  2. You cannot use both this() and super() in the same constructor
  3. Constructor chaining helps avoid code duplication
  4. If a class doesn’t explicitly call a parent constructor, the compiler adds an implicit call to the parent’s default constructor
  5. If the parent class doesn’t have a default constructor, the child class must explicitly call one of the parent’s constructors

When creating an object, constructors are executed in a specific order:

  1. Static initializers of the parent class
  2. Static initializers of the child class
  3. Instance initializers of the parent class
  4. Parent class constructor
  5. Instance initializers of the child class
  6. Child class constructor
public class Parent {
static {
System.out.println("1. Parent static initializer");
}
{
System.out.println("3. Parent instance initializer");
}
public Parent() {
System.out.println("4. Parent constructor");
}
}
public class Child extends Parent {
static {
System.out.println("2. Child static initializer");
}
{
System.out.println("5. Child instance initializer");
}
public Child() {
super(); // This is implicit if not written
System.out.println("6. Child constructor");
}
public static void main(String[] args) {
new Child();
}
}

Interview Questions on Constructor Chaining

Section titled “Interview Questions on Constructor Chaining”
  1. What happens if a parent class doesn’t have a default constructor?

    • Child classes must explicitly call one of the parent’s constructors using super()
  2. Can you call a constructor from a method?

    • No, constructors can only be called from other constructors using this() or super()
  3. What’s the difference between initialization blocks and constructors?

    • Initialization blocks run for every constructor, while constructors can have different implementations
    • Initialization blocks run before the constructor body
  4. Can you have recursive constructor calls?

    • No, it would cause a compilation error (recursive constructor invocation)
  5. What is the purpose of constructor chaining?

    • To reuse code and avoid duplication
    • To ensure proper initialization of objects
    • To provide multiple ways to create objects with different parameters