Java OOP Concepts Complete Tutorial

Java OOP Concepts Complete Tutorial

Master Object-Oriented Programming in Java with comprehensive examples and explanations

Introduction to OOP

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects", which contain data (attributes) and code (methods). Java is a pure object-oriented language that implements OOP principles.

Why OOP?

  • Modularity: Code is organized into classes and objects
  • Reusability: Classes can be reused in different programs
  • Maintainability: Easier to maintain and update code
  • Scalability: Easy to extend and add new features
  • Security: Data hiding through encapsulation

Four Pillars of OOP

  1. Encapsulation: Bundling data and methods together
  2. Inheritance: Creating new classes from existing ones
  3. Polymorphism: One interface, multiple implementations
  4. Abstraction: Hiding implementation details
Key Concept: In Java, everything is an object except primitive data types (int, char, boolean, etc.). Even arrays are objects in Java.

Classes and Objects

A class is a blueprint or template for creating objects. An object is an instance of a class.

Defining a Class

public class Car {
    // Attributes (Instance Variables)
    String brand;
    String model;
    int year;
    double price;
    
    // Methods
    void start() {
        System.out.println("Car is starting...");
    }
    
    void stop() {
        System.out.println("Car has stopped.");
    }
    
    void displayInfo() {
        System.out.println("Brand: " + brand);
        System.out.println("Model: " + model);
        System.out.println("Year: " + year);
        System.out.println("Price: $" + price);
    }
}

Creating Objects

public class Main {
    public static void main(String[] args) {
        // Creating an object
        Car myCar = new Car();
        
        // Setting attributes
        myCar.brand = "Toyota";
        myCar.model = "Camry";
        myCar.year = 2023;
        myCar.price = 30000.0;
        
        // Calling methods
        myCar.start();
        myCar.displayInfo();
        myCar.stop();
    }
}

Class Components

  • Instance Variables: Variables declared inside class but outside methods
  • Methods: Functions that define behavior of objects
  • Constructors: Special methods for object initialization
  • Blocks: Static and instance initialization blocks
  • Nested Classes: Classes defined inside other classes

Encapsulation

Encapsulation is the mechanism of wrapping data (variables) and code (methods) together as a single unit. It also provides data hiding by making variables private and providing public methods to access them.

Benefits of Encapsulation

  • Data hiding and security
  • Flexibility to change implementation
  • Better control over data validation
  • Easier maintenance

Example: Encapsulated Class

public class BankAccount {
    // Private variables - data hiding
    private String accountNumber;
    private double balance;
    private String accountHolder;
    
    // Public constructor
    public BankAccount(String accountNumber, String accountHolder) {
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        this.balance = 0.0;
    }
    
    // Public getter methods
    public String getAccountNumber() {
        return accountNumber;
    }
    
    public double getBalance() {
        return balance;
    }
    
    public String getAccountHolder() {
        return accountHolder;
    }
    
    // Public setter methods with validation
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: $" + amount);
        } else {
            System.out.println("Invalid deposit amount!");
        }
    }
    
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: $" + amount);
        } else {
            System.out.println("Invalid withdrawal amount or insufficient funds!");
        }
    }
    
    public void displayBalance() {
        System.out.println("Account Holder: " + accountHolder);
        System.out.println("Account Number: " + accountNumber);
        System.out.println("Balance: $" + balance);
    }
}

Using the Encapsulated Class

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("ACC123", "John Doe");
        
        account.deposit(1000.0);
        account.deposit(500.0);
        account.withdraw(300.0);
        account.displayBalance();
        
        // Cannot directly access private variables
        // account.balance = 10000; // Compilation error!
    }
}
Best Practice: Always make instance variables private and provide public getter and setter methods. This gives you control over how data is accessed and modified.

Inheritance

Inheritance is a mechanism where a new class (child/subclass) acquires the properties and behaviors of an existing class (parent/superclass). It promotes code reusability and establishes an "is-a" relationship.

Syntax

class ParentClass {
    // Parent class members
}

class ChildClass extends ParentClass {
    // Child class members
}

Example: Inheritance

// Parent class
class Animal {
    String name;
    int age;
    
    void eat() {
        System.out.println(name + " is eating.");
    }
    
    void sleep() {
        System.out.println(name + " is sleeping.");
    }
}

// Child class
class Dog extends Animal {
    String breed;
    
    void bark() {
        System.out.println(name + " is barking!");
    }
    
    void displayInfo() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
        System.out.println("Breed: " + breed);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.name = "Buddy";
        myDog.age = 3;
        myDog.breed = "Golden Retriever";
        
        myDog.eat();      // Inherited from Animal
        myDog.sleep();    // Inherited from Animal
        myDog.bark();     // Own method
        myDog.displayInfo();
    }
}

Types of Inheritance

  • Single Inheritance: One child class extends one parent class
  • Multilevel Inheritance: Chain of inheritance (A → B → C)
  • Hierarchical Inheritance: Multiple children from one parent
  • Multiple Inheritance: Not supported in Java (use interfaces instead)

Method Overriding

When a child class provides a specific implementation of a method that is already defined in the parent class, it's called method overriding.

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

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Dog barks: Woof! Woof!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Cat meows: Meow! Meow!");
    }
}
Important: Java doesn't support multiple inheritance with classes to avoid the diamond problem. However, multiple inheritance can be achieved using interfaces.

Polymorphism

Polymorphism means "many forms". It allows objects of different types to be treated as objects of a common type. In Java, polymorphism is achieved through method overriding and method overloading.

Types of Polymorphism

  1. Compile-time Polymorphism (Method Overloading): Multiple methods with same name but different parameters
  2. Runtime Polymorphism (Method Overriding): Child class overrides parent class method

Method Overloading (Compile-time)

class Calculator {
    // Method overloading - same name, different parameters
    int add(int a, int b) {
        return a + b;
    }
    
    int add(int a, int b, int c) {
        return a + b + c;
    }
    
    double add(double a, double b) {
        return a + b;
    }
    
    String add(String a, String b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(5, 3));           // Calls int version
        System.out.println(calc.add(5, 3, 2));       // Calls three-parameter version
        System.out.println(calc.add(5.5, 3.2));      // Calls double version
        System.out.println(calc.add("Hello", "World")); // Calls String version
    }
}

Method Overriding (Runtime)

class Shape {
    void draw() {
        System.out.println("Drawing a shape");
    }
    
    double calculateArea() {
        return 0;
    }
}

class Circle extends Shape {
    double radius;
    
    Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    void draw() {
        System.out.println("Drawing a circle");
    }
    
    @Override
    double calculateArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle extends Shape {
    double length, width;
    
    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    @Override
    void draw() {
        System.out.println("Drawing a rectangle");
    }
    
    @Override
    double calculateArea() {
        return length * width;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape1 = new Circle(5.0);
        Shape shape2 = new Rectangle(4.0, 6.0);
        
        shape1.draw();  // Calls Circle's draw()
        System.out.println("Area: " + shape1.calculateArea());
        
        shape2.draw();  // Calls Rectangle's draw()
        System.out.println("Area: " + shape2.calculateArea());
    }
}
Key Point: In runtime polymorphism, the method to be called is determined at runtime based on the actual object type, not the reference type.

Abstraction

Abstraction is the process of hiding implementation details and showing only essential features. In Java, abstraction is achieved using abstract classes and interfaces.

Abstract Class

An abstract class cannot be instantiated. It may contain abstract methods (without body) and concrete methods (with body).

// Abstract class
abstract class Vehicle {
    String brand;
    String model;
    
    // Concrete method
    void displayInfo() {
        System.out.println("Brand: " + brand);
        System.out.println("Model: " + model);
    }
    
    // Abstract method - must be implemented by child classes
    abstract void start();
    abstract void stop();
    abstract double calculateFuelEfficiency();
}

// Concrete class extending abstract class
class Car extends Vehicle {
    Car(String brand, String model) {
        this.brand = brand;
        this.model = model;
    }
    
    @Override
    void start() {
        System.out.println("Car engine started");
    }
    
    @Override
    void stop() {
        System.out.println("Car engine stopped");
    }
    
    @Override
    double calculateFuelEfficiency() {
        return 15.5; // km per liter
    }
}

// Another concrete class
class Motorcycle extends Vehicle {
    Motorcycle(String brand, String model) {
        this.brand = brand;
        this.model = model;
    }
    
    @Override
    void start() {
        System.out.println("Motorcycle engine started");
    }
    
    @Override
    void stop() {
        System.out.println("Motorcycle engine stopped");
    }
    
    @Override
    double calculateFuelEfficiency() {
        return 35.0; // km per liter
    }
}

Key Points about Abstract Classes

  • Cannot be instantiated directly
  • Can have both abstract and concrete methods
  • Can have constructors, instance variables, and static methods
  • Child class must implement all abstract methods
  • Can have final methods

Interfaces

An interface is a reference type in Java that contains only abstract methods, default methods, static methods, and constants. It's a contract that classes must follow.

Defining an Interface

interface Drawable {
    // Constant (implicitly public, static, final)
    String COLOR = "Black";
    
    // Abstract method (implicitly public and abstract)
    void draw();
    
    // Default method (Java 8+)
    default void display() {
        System.out.println("Displaying drawable object");
    }
    
    // Static method (Java 8+)
    static void printInfo() {
        System.out.println("This is a Drawable interface");
    }
}

interface Resizable {
    void resize(int percentage);
}

// Class implementing interface
class Circle implements Drawable, Resizable {
    double radius;
    
    Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a circle with radius: " + radius);
    }
    
    @Override
    public void resize(int percentage) {
        radius = radius * (1 + percentage / 100.0);
        System.out.println("Circle resized. New radius: " + radius);
    }
}

Interface vs Abstract Class

Interface Abstract Class
Only abstract methods (before Java 8) Can have both abstract and concrete methods
Multiple inheritance supported Single inheritance only
Only constants (public static final) Can have instance variables
Cannot have constructors Can have constructors
All methods are public by default Can have any access modifier
Java 8+ Features: Interfaces can now have default methods (with implementation) and static methods. This allows adding new methods to interfaces without breaking existing implementations.

Constructors

A constructor is a special method that is called when an object is created. It has the same name as the class and doesn't have a return type.

Types of Constructors

  1. Default Constructor: No parameters, provided by compiler if no constructor is defined
  2. Parameterized Constructor: Takes parameters to initialize object
  3. Copy Constructor: Creates object from another object

Constructor Examples

class Student {
    String name;
    int age;
    String course;
    
    // Default constructor
    Student() {
        name = "Unknown";
        age = 0;
        course = "Not assigned";
        System.out.println("Default constructor called");
    }
    
    // Parameterized constructor
    Student(String name, int age, String course) {
        this.name = name;
        this.age = age;
        this.course = course;
        System.out.println("Parameterized constructor called");
    }
    
    // Copy constructor
    Student(Student other) {
        this.name = other.name;
        this.age = other.age;
        this.course = other.course;
        System.out.println("Copy constructor called");
    }
    
    void display() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
        System.out.println("Course: " + course);
    }
}

public class Main {
    public static void main(String[] args) {
        Student s1 = new Student();  // Default constructor
        Student s2 = new Student("John", 20, "Computer Science");  // Parameterized
        Student s3 = new Student(s2);  // Copy constructor
        
        s2.display();
        s3.display();
    }
}

Constructor Chaining

One constructor can call another constructor of the same class using this().

class Person {
    String name;
    int age;
    String city;
    
    Person() {
        this("Unknown", 0, "Unknown");  // Calling parameterized constructor
    }
    
    Person(String name, int age) {
        this(name, age, "Unknown");  // Constructor chaining
    }
    
    Person(String name, int age, String city) {
        this.name = name;
        this.age = age;
        this.city = city;
    }
}

Access Modifiers

Access modifiers control the visibility and accessibility of classes, methods, and variables.

Types of Access Modifiers

Modifier Same Class Same Package Subclass Different Package
private
default
protected
public

Example

class AccessExample {
    private int privateVar = 1;
    int defaultVar = 2;
    protected int protectedVar = 3;
    public int publicVar = 4;
    
    private void privateMethod() {
        System.out.println("Private method");
    }
    
    void defaultMethod() {
        System.out.println("Default method");
    }
    
    protected void protectedMethod() {
        System.out.println("Protected method");
    }
    
    public void publicMethod() {
        System.out.println("Public method");
    }
}

Static Keyword

The static keyword is used for memory management. Static members belong to the class rather than instances.

Static Variables

class Counter {
    static int count = 0;  // Static variable - shared by all instances
    
    Counter() {
        count++;  // Incremented for each object creation
    }
    
    static void displayCount() {  // Static method
        System.out.println("Total objects created: " + count);
    }
}

public class Main {
    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Counter c3 = new Counter();
        
        Counter.displayCount();  // Output: 3
    }
}

Static Methods

  • Can be called without creating an object
  • Can only access static variables and methods
  • Cannot use this or super

Static Block

class StaticBlockExample {
    static int value;
    
    // Static block - executed when class is loaded
    static {
        value = 100;
        System.out.println("Static block executed");
    }
    
    StaticBlockExample() {
        System.out.println("Constructor executed");
    }
}

Final Keyword

The final keyword is used to restrict modification.

Uses of Final

  • Final Variable: Cannot be reassigned (constant)
  • Final Method: Cannot be overridden
  • Final Class: Cannot be extended
final class MathUtils {  // Final class - cannot be extended
    final double PI = 3.14159;  // Final variable - constant
    
    final double calculateArea(double radius) {  // Final method - cannot be overridden
        return PI * radius * radius;
    }
}

this and super Keywords

this Keyword

this refers to the current object instance.

class Person {
    String name;
    int age;
    
    Person(String name, int age) {
        this.name = name;  // 'this' refers to current object
        this.age = age;
    }
    
    void display() {
        System.out.println("Name: " + this.name);
        System.out.println("Age: " + this.age);
    }
}

super Keyword

super refers to the parent class object.

class Animal {
    String name;
    
    Animal(String name) {
        this.name = name;
    }
    
    void display() {
        System.out.println("Animal: " + name);
    }
}

class Dog extends Animal {
    String breed;
    
    Dog(String name, String breed) {
        super(name);  // Calling parent constructor
        this.breed = breed;
    }
    
    @Override
    void display() {
        super.display();  // Calling parent method
        System.out.println("Breed: " + breed);
    }
}

Best Practices

OOP Best Practices in Java

  • Always use encapsulation: Make instance variables private
  • Use meaningful names: Class names should be nouns, method names should be verbs
  • Follow single responsibility: Each class should have one reason to change
  • Prefer composition over inheritance: Use "has-a" relationship when possible
  • Use interfaces for contracts: Define behavior through interfaces
  • Avoid deep inheritance hierarchies: Keep inheritance levels shallow
  • Use final for constants: Make constants final and static
  • Override equals() and hashCode(): When comparing objects
  • Use @Override annotation: When overriding methods
  • Document your code: Use JavaDoc comments

Common Mistakes to Avoid

  • Making all variables public
  • Creating too many static members
  • Deep inheritance hierarchies
  • Not using interfaces when appropriate
  • Ignoring encapsulation
Previous
Next Post »

BOOK OF THE DAY