You are viewing a preview of this lesson. Sign in to start learning
Back to Java

Object-Oriented Programming

Master the pillars of OOP in Java

Object-Oriented Programming in Java

Master the fundamentals of Object-Oriented Programming (OOP) in Java with free flashcards and interactive practice. This lesson covers classes, objects, inheritance, polymorphism, encapsulation, and abstractionโ€”essential concepts for building robust, maintainable Java applications and acing technical interviews.

Welcome to Object-Oriented Programming! ๐Ÿ’ป

Object-Oriented Programming is the cornerstone of modern Java development. Instead of thinking in terms of procedures and functions, OOP allows you to model real-world entities as objects that contain both data (fields) and behavior (methods). This paradigm revolutionized software engineering by making code more modular, reusable, and easier to understand.

Think of OOP like building with LEGO blocks ๐Ÿงฑ: each piece (object) has its own shape and purpose, and you can combine them in countless ways to create complex structures. Once you understand the four pillars of OOPโ€”encapsulation, inheritance, polymorphism, and abstractionโ€”you'll be able to design elegant solutions to complex problems.

Core Concepts of OOP ๐ŸŽฏ

1. Classes and Objects

A class is a blueprint or template that defines the structure and behavior of objects. An object is a specific instance of a classโ€”it's the actual entity that exists in memory during program execution.

Real-world analogy ๐ŸŒ: Think of a class as an architectural blueprint for a house. The blueprint defines rooms, doors, and windows, but it's not a house itself. When builders construct actual houses from that blueprint, each house is an objectโ€”a concrete instance with its own address, color, and occupants.

// Class definition (the blueprint)
public class Dog {
    // Fields (data/state)
    String name;
    int age;
    String breed;
    
    // Constructor (creates objects)
    public Dog(String name, int age, String breed) {
        this.name = name;
        this.age = age;
        this.breed = breed;
    }
    
    // Methods (behavior)
    public void bark() {
        System.out.println(name + " says: Woof!");
    }
    
    public void sleep() {
        System.out.println(name + " is sleeping...");
    }
}

// Creating objects (instances)
Dog myDog = new Dog("Buddy", 3, "Golden Retriever");
Dog yourDog = new Dog("Max", 5, "Labrador");

myDog.bark();   // Output: Buddy says: Woof!
yourDog.bark(); // Output: Max says: Woof!

๐Ÿ’ก Tip: The this keyword refers to the current object instance. It's especially useful when parameter names match field names, helping distinguish between them.

2. Encapsulation ๐Ÿ”’

Encapsulation is the practice of bundling data (fields) and methods that operate on that data within a single unit (class), while restricting direct access to some components. This is achieved using access modifiers and getter/setter methods.

Why encapsulation matters: It protects object integrity by preventing external code from making invalid changes. Imagine a bank accountโ€”you shouldn't be able to directly set your balance to any value; instead, you should deposit or withdraw through controlled methods.

Access ModifierClassPackageSubclassWorld
privateโœ…โŒโŒโŒ
default (no modifier)โœ…โœ…โŒโŒ
protectedโœ…โœ…โœ…โŒ
publicโœ…โœ…โœ…โœ…
public class BankAccount {
    // Private fields - cannot be accessed directly from outside
    private String accountNumber;
    private double balance;
    private String ownerName;
    
    public BankAccount(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
        this.balance = 0.0;
    }
    
    // Getter method (read-only access)
    public double getBalance() {
        return balance;
    }
    
    // Controlled methods to modify balance
    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 or insufficient funds");
        }
    }
}

๐Ÿง  Memory device: PPPP = Private Protects, Public Permits (Private keeps things safe inside, Public allows access from anywhere)

3. Inheritance ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ

Inheritance allows a class to inherit fields and methods from another class, promoting code reuse and establishing hierarchical relationships. The class being inherited from is the parent/superclass, and the class inheriting is the child/subclass.

Real-world analogy ๐ŸŒ: Think of biological inheritance. A "Mammal" class might have properties like "warm-blooded" and "has hair." Dogs, cats, and humans are all specific types of mammals that inherit these characteristics but add their own unique features.

        INHERITANCE HIERARCHY

            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
            โ”‚  Animal  โ”‚
            โ”‚          โ”‚
            โ”‚ +eat()   โ”‚
            โ”‚ +sleep() โ”‚
            โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
                  โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚                 โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”
    โ”‚   Dog   โ”‚      โ”‚    Cat    โ”‚
    โ”‚         โ”‚      โ”‚           โ”‚
    โ”‚ +bark() โ”‚      โ”‚ +meow()   โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
// Superclass (parent)
public class Animal {
    protected String name;
    protected int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void eat() {
        System.out.println(name + " is eating.");
    }
    
    public void sleep() {
        System.out.println(name + " is sleeping.");
    }
}

// Subclass (child) - inherits from Animal
public class Dog extends Animal {
    private String breed;
    
    // Constructor must call parent constructor
    public Dog(String name, int age, String breed) {
        super(name, age);  // Calls Animal constructor
        this.breed = breed;
    }
    
    // Dog-specific method
    public void bark() {
        System.out.println(name + " barks: Woof!");
    }
    
    // Method overriding
    @Override
    public void eat() {
        System.out.println(name + " is eating dog food.");
    }
}

// Usage
Dog myDog = new Dog("Buddy", 3, "Beagle");
myDog.eat();    // Output: Buddy is eating dog food.
myDog.sleep();  // Output: Buddy is sleeping. (inherited)
myDog.bark();   // Output: Buddy barks: Woof!

๐Ÿ’ก Tip: Use the super keyword to access parent class constructors and methods. The @Override annotation helps catch errors by ensuring you're actually overriding a parent method.

๐Ÿค” Did you know? Java doesn't support multiple inheritance (a class can't extend multiple classes) to avoid the "diamond problem." However, Java allows implementing multiple interfaces, which we'll discuss later!

4. Polymorphism ๐ŸŽญ

Polymorphism (Greek for "many forms") allows objects of different classes to be treated as objects of a common superclass. It's the ability of a single interface to represent different underlying forms (data types).

There are two types:

  • Compile-time polymorphism (Method Overloading): Multiple methods with the same name but different parameters
  • Runtime polymorphism (Method Overriding): Subclass provides specific implementation of a method already defined in its superclass

Real-world analogy ๐ŸŒ: Think of a universal remote control. The "power" button works on your TV, stereo, and DVD player, but each device responds differently. Same button (interface), different behaviors (implementations).

// Method Overloading (compile-time polymorphism)
public class Calculator {
    // Same method name, different parameters
    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;
    }
}

// Method Overriding (runtime polymorphism)
public class Shape {
    public void draw() {
        System.out.println("Drawing a shape");
    }
}

public class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle โญ•");
    }
}

public class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle โ–ญ");
    }
}

// Polymorphic behavior
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
Shape shape3 = new Shape();

shape1.draw(); // Output: Drawing a circle โญ•
shape2.draw(); // Output: Drawing a rectangle โ–ญ
shape3.draw(); // Output: Drawing a shape

๐Ÿ’ก Tip: Polymorphism enables you to write flexible, extensible code. You can add new Shape subclasses without modifying code that uses the Shape interface.

5. Abstraction ๐ŸŽจ

Abstraction means hiding complex implementation details and showing only essential features. It's achieved through abstract classes and interfaces.

Abstract classes:

  • Cannot be instantiated directly
  • Can have both abstract methods (no implementation) and concrete methods (with implementation)
  • Use abstract keyword

Interfaces:

  • Pure abstraction (traditionally all methods were abstract)
  • A class can implement multiple interfaces
  • Use implements keyword
  • Since Java 8, interfaces can have default and static methods

Real-world analogy ๐ŸŒ: Think of a car dashboard. You see the speedometer, steering wheel, and pedals (abstraction), but you don't see the complex engine mechanics, fuel injection systems, or transmission details (hidden implementation).

// Abstract class
public abstract class Vehicle {
    protected String brand;
    
    public Vehicle(String brand) {
        this.brand = brand;
    }
    
    // Abstract method (no implementation)
    public abstract void start();
    
    // Concrete method (with implementation)
    public void stop() {
        System.out.println(brand + " vehicle stopped.");
    }
}

public class Car extends Vehicle {
    public Car(String brand) {
        super(brand);
    }
    
    @Override
    public void start() {
        System.out.println(brand + " car started with ignition key.");
    }
}

// Interface
public interface Flyable {
    void fly();  // Implicitly public and abstract
    void land();
}

public class Airplane extends Vehicle implements Flyable {
    public Airplane(String brand) {
        super(brand);
    }
    
    @Override
    public void start() {
        System.out.println(brand + " airplane engines started.");
    }
    
    @Override
    public void fly() {
        System.out.println(brand + " airplane is flying โœˆ๏ธ");
    }
    
    @Override
    public void land() {
        System.out.println(brand + " airplane is landing.");
    }
}
FeatureAbstract ClassInterface
MethodsCan have both abstract and concrete methodsAbstract by default (can have default/static since Java 8)
FieldsCan have any type of fieldsOnly public static final constants
ConstructorCan have constructorsCannot have constructors
MultipleA class can extend only one abstract classA class can implement multiple interfaces
Access ModifiersCan use any access modifierMethods are public by default

Detailed Examples with Explanations ๐Ÿ“

Example 1: Building a Library Management System

Let's create a practical system that demonstrates all OOP principles:

// Abstract base class
public abstract class LibraryItem {
    private String id;
    private String title;
    protected boolean isCheckedOut;
    
    public LibraryItem(String id, String title) {
        this.id = id;
        this.title = title;
        this.isCheckedOut = false;
    }
    
    // Abstract method - each item type implements differently
    public abstract void displayInfo();
    
    // Concrete methods - common to all items
    public void checkOut() {
        if (!isCheckedOut) {
            isCheckedOut = true;
            System.out.println(title + " checked out successfully.");
        } else {
            System.out.println(title + " is already checked out.");
        }
    }
    
    public void returnItem() {
        if (isCheckedOut) {
            isCheckedOut = false;
            System.out.println(title + " returned successfully.");
        }
    }
    
    public String getTitle() {
        return title;
    }
}

// Concrete subclass - Book
public class Book extends LibraryItem {
    private String author;
    private int pages;
    
    public Book(String id, String title, String author, int pages) {
        super(id, title);
        this.author = author;
        this.pages = pages;
    }
    
    @Override
    public void displayInfo() {
        System.out.println("๐Ÿ“š Book: " + getTitle());
        System.out.println("   Author: " + author);
        System.out.println("   Pages: " + pages);
        System.out.println("   Status: " + (isCheckedOut ? "Checked Out" : "Available"));
    }
}

// Concrete subclass - DVD
public class DVD extends LibraryItem {
    private String director;
    private int runtime; // in minutes
    
    public DVD(String id, String title, String director, int runtime) {
        super(id, title);
        this.director = director;
        this.runtime = runtime;
    }
    
    @Override
    public void displayInfo() {
        System.out.println("๐Ÿ“€ DVD: " + getTitle());
        System.out.println("   Director: " + director);
        System.out.println("   Runtime: " + runtime + " minutes");
        System.out.println("   Status: " + (isCheckedOut ? "Checked Out" : "Available"));
    }
}

// Usage demonstration
public class LibraryDemo {
    public static void main(String[] args) {
        // Polymorphism - array of parent type holds different child objects
        LibraryItem[] items = new LibraryItem[3];
        items[0] = new Book("B001", "Clean Code", "Robert Martin", 464);
        items[1] = new DVD("D001", "The Matrix", "Wachowski", 136);
        items[2] = new Book("B002", "Effective Java", "Joshua Bloch", 416);
        
        // Display all items
        for (LibraryItem item : items) {
            item.displayInfo(); // Polymorphic method call
            System.out.println();
        }
        
        // Check out an item
        items[0].checkOut();
        items[0].displayInfo();
    }
}

Key takeaways from this example:

  • Encapsulation: Private fields with controlled access
  • Inheritance: Book and DVD inherit from LibraryItem
  • Polymorphism: Array of LibraryItem can hold different types
  • Abstraction: Abstract displayInfo() method forces subclasses to provide implementations

Example 2: Payment Processing System

This example shows how interfaces enable multiple inheritance of behavior:

// Interface for payment processing
public interface Payable {
    void processPayment(double amount);
    boolean refund(double amount);
}

// Interface for transactions
public interface Transactional {
    String getTransactionId();
    void logTransaction(String details);
}

// Base class for payment methods
public abstract class PaymentMethod {
    protected String accountHolder;
    protected double balance;
    
    public PaymentMethod(String accountHolder, double balance) {
        this.accountHolder = accountHolder;
        this.balance = balance;
    }
    
    public double getBalance() {
        return balance;
    }
}

// CreditCard implements multiple interfaces
public class CreditCard extends PaymentMethod implements Payable, Transactional {
    private String cardNumber;
    private String transactionId;
    
    public CreditCard(String accountHolder, String cardNumber, double creditLimit) {
        super(accountHolder, creditLimit);
        this.cardNumber = maskCardNumber(cardNumber);
        this.transactionId = "";
    }
    
    @Override
    public void processPayment(double amount) {
        if (amount <= balance) {
            balance -= amount;
            transactionId = generateTransactionId();
            System.out.println("๐Ÿ’ณ Charged $" + amount + " to card ending in " + cardNumber);
            logTransaction("Payment: $" + amount);
        } else {
            System.out.println("โŒ Insufficient credit limit");
        }
    }
    
    @Override
    public boolean refund(double amount) {
        balance += amount;
        transactionId = generateTransactionId();
        logTransaction("Refund: $" + amount);
        System.out.println("โœ… Refunded $" + amount + " to card");
        return true;
    }
    
    @Override
    public String getTransactionId() {
        return transactionId;
    }
    
    @Override
    public void logTransaction(String details) {
        System.out.println("[LOG] " + details + " | TxnID: " + transactionId);
    }
    
    private String maskCardNumber(String number) {
        return "****" + number.substring(number.length() - 4);
    }
    
    private String generateTransactionId() {
        return "TXN" + System.currentTimeMillis();
    }
}

// PayPal - different implementation of same interfaces
public class PayPal extends PaymentMethod implements Payable, Transactional {
    private String email;
    private String transactionId;
    
    public PayPal(String accountHolder, String email, double balance) {
        super(accountHolder, balance);
        this.email = email;
        this.transactionId = "";
    }
    
    @Override
    public void processPayment(double amount) {
        if (amount <= balance) {
            balance -= amount;
            transactionId = "PP" + System.currentTimeMillis();
            System.out.println("๐Ÿ’ฐ Paid $" + amount + " via PayPal (" + email + ")");
            logTransaction("Payment: $" + amount);
        } else {
            System.out.println("โŒ Insufficient PayPal balance");
        }
    }
    
    @Override
    public boolean refund(double amount) {
        balance += amount;
        transactionId = "PP" + System.currentTimeMillis();
        logTransaction("Refund: $" + amount);
        System.out.println("โœ… Refunded $" + amount + " to PayPal");
        return true;
    }
    
    @Override
    public String getTransactionId() {
        return transactionId;
    }
    
    @Override
    public void logTransaction(String details) {
        System.out.println("[PAYPAL LOG] " + details + " | Email: " + email);
    }
}

Why this design is powerful:

  • Different payment methods share common interfaces
  • New payment types can be added without changing existing code
  • Code that uses Payable interface works with any payment method
  • Multiple interfaces allow mixing different capabilities

Example 3: Game Character System

This example demonstrates constructor chaining and method overriding:

public class Character {
    private String name;
    private int health;
    private int level;
    
    // Default constructor
    public Character() {
        this("Unknown", 100, 1);
    }
    
    // Constructor with name only
    public Character(String name) {
        this(name, 100, 1);
    }
    
    // Full constructor (called by others)
    public Character(String name, int health, int level) {
        this.name = name;
        this.health = health;
        this.level = level;
    }
    
    public void attack() {
        System.out.println(name + " performs a basic attack!");
    }
    
    public void takeDamage(int damage) {
        health -= damage;
        System.out.println(name + " took " + damage + " damage. Health: " + health);
    }
    
    public boolean isAlive() {
        return health > 0;
    }
}

public class Warrior extends Character {
    private int armor;
    
    public Warrior(String name) {
        super(name, 150, 1);  // Warriors start with more health
        this.armor = 20;
    }
    
    @Override
    public void attack() {
        System.out.println("โš”๏ธ " + super.toString() + " swings a mighty sword!");
    }
    
    // New method specific to Warrior
    public void shieldBlock() {
        System.out.println("๐Ÿ›ก๏ธ Warrior raises shield, blocking incoming damage!");
    }
    
    @Override
    public void takeDamage(int damage) {
        int reducedDamage = Math.max(0, damage - armor);
        super.takeDamage(reducedDamage);
        System.out.println("(Armor absorbed " + (damage - reducedDamage) + " damage)");
    }
}

public class Mage extends Character {
    private int mana;
    
    public Mage(String name) {
        super(name, 80, 1);  // Mages have less health
        this.mana = 100;
    }
    
    @Override
    public void attack() {
        if (mana >= 10) {
            mana -= 10;
            System.out.println("๐Ÿ”ฎ " + super.toString() + " casts a fireball! (Mana: " + mana + ")");
        } else {
            System.out.println("๐Ÿ’ซ Not enough mana for spell!");
        }
    }
    
    public void meditate() {
        mana = 100;
        System.out.println("๐Ÿง˜ Mage meditates and restores mana to full.");
    }
}

Example 4: Static Members and Utility Classes

Static members belong to the class itself, not to instances:

public class MathUtils {
    // Static constant
    public static final double PI = 3.14159265359;
    
    // Static variable - shared by all instances
    private static int calculationCount = 0;
    
    // Static method - can be called without creating an object
    public static double calculateCircleArea(double radius) {
        calculationCount++;
        return PI * radius * radius;
    }
    
    public static double calculateCircleCircumference(double radius) {
        calculationCount++;
        return 2 * PI * radius;
    }
    
    public static int getCalculationCount() {
        return calculationCount;
    }
    
    // Private constructor prevents instantiation
    private MathUtils() {
        throw new UnsupportedOperationException("Utility class");
    }
}

// Usage - no object creation needed
double area = MathUtils.calculateCircleArea(5.0);
double circumference = MathUtils.calculateCircleCircumference(5.0);
System.out.println("Calculations performed: " + MathUtils.getCalculationCount());

๐Ÿ’ก Tip: Use static methods for utility functions that don't need object state. Common examples: Math.sqrt(), Arrays.sort(), Collections.shuffle().

Common Mistakes to Avoid โš ๏ธ

1. Forgetting to Call super() in Constructors

โŒ Wrong:

public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, int age, String breed) {
        // Missing super(name, age)!
        this.breed = breed;  // Compiler error!
    }
}

โœ… Right:

public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, int age, String breed) {
        super(name, age);  // Must be first line
        this.breed = breed;
    }
}

2. Breaking Encapsulation with Public Fields

โŒ Wrong:

public class Student {
    public String name;  // Anyone can modify!
    public int grade;    // No validation possible!
}

โœ… Right:

public class Student {
    private String name;
    private int grade;
    
    public void setGrade(int grade) {
        if (grade >= 0 && grade <= 100) {
            this.grade = grade;
        } else {
            throw new IllegalArgumentException("Invalid grade");
        }
    }
}

3. Overriding vs Overloading Confusion

โŒ Wrong - thinking this is overriding:

public class Animal {
    public void makeSound() {
        System.out.println("Some sound");
    }
}

public class Dog extends Animal {
    // This is overLOADING, not overRIDING - different signature!
    public void makeSound(String mood) {
        System.out.println("Woof woof");
    }
}

โœ… Right - proper overriding:

public class Dog extends Animal {
    @Override  // Same signature
    public void makeSound() {
        System.out.println("Woof woof");
    }
}

4. Trying to Instantiate Abstract Classes or Interfaces

โŒ Wrong:

Animal animal = new Animal();  // Error if Animal is abstract!
Runnable task = new Runnable();  // Error - Runnable is an interface!

โœ… Right:

Animal animal = new Dog();  // Use concrete subclass
Runnable task = new Runnable() {  // Anonymous class
    @Override
    public void run() {
        System.out.println("Running task");
    }
};

5. Misusing Static Members

โŒ Wrong:

public class Counter {
    private static int count = 0;  // Shared by ALL objects!
    
    public void increment() {
        count++;  // All counters share same count!
    }
}

โœ… Right (if you want separate counts):

public class Counter {
    private int count = 0;  // Each object has its own
    
    public void increment() {
        count++;
    }
}

6. Violating the "Is-A" Relationship

โŒ Wrong - inheritance misuse:

// A car is NOT a wheel! Don't use inheritance here.
public class Car extends Wheel {
    // Wrong relationship
}

โœ… Right - use composition:

public class Car {
    private Wheel[] wheels = new Wheel[4];  // Car HAS wheels
    private Engine engine;  // Car HAS an engine
}

๐Ÿง  Memory device: HASI = Has-A use composition, Is-A use inheritance

๐Ÿ”ง Try This: Mini-Exercises

Exercise 1: Create a Rectangle class with private width and height fields. Add methods to calculate area and perimeter. Create a Square subclass that ensures width equals height.

Exercise 2: Design an interface Drawable with a draw() method. Create classes Circle, Triangle, and Line that implement this interface. Write a method that accepts an array of Drawable objects and calls draw() on each.

Exercise 3: Create an abstract class Employee with fields for name and salary. Create subclasses Manager and Developer with different bonus calculation methods. Use polymorphism to calculate total payroll.

Key Takeaways ๐ŸŽฏ

  1. Classes are blueprints, objects are instances created from those blueprints
  2. Encapsulation protects data integrity using private fields and public methods
  3. Inheritance enables code reuse through parent-child relationships (use extends)
  4. Polymorphism allows treating objects of different types uniformly through a common interface
  5. Abstraction hides complexity using abstract classes and interfaces
  6. Use access modifiers strategically: private for fields, public for interfaces
  7. Constructor chaining with super() initializes parent class properly
  8. Method overriding changes behavior; method overloading provides multiple versions
  9. Static members belong to the class, not instances
  10. Prefer composition over inheritance when modeling "has-a" relationships

๐Ÿ“‹ Quick Reference Card

ConceptKeywordPurpose
Class DefinitionclassBlueprint for objects
Object CreationnewInstantiate a class
InheritanceextendsChild inherits from parent
Interface ImplementationimplementsClass fulfills interface contract
Method Override@OverrideReplace parent method
Parent ReferencesuperAccess parent class members
Current ObjectthisReference to current instance
Abstract Classabstract classCannot instantiate, can have abstract methods
InterfaceinterfacePure contract (all methods abstract)
Class MemberstaticBelongs to class, not instances
ConstantfinalCannot be changed after initialization
Private AccessprivateOnly within same class
Protected AccessprotectedSame package + subclasses
Public AccesspublicAccessible from anywhere

๐Ÿ“š Further Study

  1. Oracle Java Tutorials - Object-Oriented Programming Concepts: https://docs.oracle.com/javase/tutorial/java/concepts/
  2. GeeksforGeeks - Object Oriented Programming in Java: https://www.geeksforgeeks.org/object-oriented-programming-oops-concept-in-java/
  3. Baeldung - OOP Concepts in Java: https://www.baeldung.com/java-oop

Master these OOP fundamentals, and you'll have a solid foundation for building sophisticated Java applications! Remember: good OOP design makes code more maintainable, testable, and scalable. ๐Ÿš€