Java Full Course 2026 | Encapsulation & Inheritance in Java
This chapter explores the foundational pillars of Object-Oriented Programming (OOP) in Java, focusing on Encapsulation and Inheritance. It details how to protect data integrity using access modifiers, organize codebases with packages, establish hierarchical relationships between classes, and leverage the super keyword to manage parent-child interactions.
1. The Core Mental Model of Encapsulation
At its core, Object-Oriented Programming aims to mimic real-world entities. Real-world objects naturally keep their internal state (data) and behaviors (actions) bound together. Encapsulation is the mechanism that enforces this boundary.
Real-Life Analogy: The Medicine Capsule
Think of a medical capsule. Inside a single protective outer wrapper, multiple distinct chemical compounds (medicines) are grouped and sealed together.
In Java, a Class acts as this capsule wrapper. It binds together:
- Data (represented as variables/fields)
- Behavior (represented as methods)
Encapsulation consists of two primary rules:
- Grouping: Bundling data and behavior together within a single unit (the class).
- Access Restriction: Preventing direct, unrestricted external access to an object's internal state.
2. Naive vs. Encapsulated Design (The Bank Account Problem)
To understand why unrestricted data access is dangerous, consider a real-world object that is not physical but highly sensitive: a Bank Account.
The Naive Approach (Unrestricted Access)
In a naive implementation, fields are left open to direct modification by any external class.
class BankAccount {
double balance; // Default access
}
public class Main {
public static void main(String[] args) {
BankAccount ba = new BankAccount();
// Direct, unrestricted modification
ba.balance = 1000000.00;
System.out.println("Balance: " + ba.balance);
}
}
The Problem: In the real world, you cannot simply write down a new balance on a piece of paper and hand it to a bank. To change your balance, you must perform a validated action: a deposit or a withdrawal. Unrestricted access bypasses all business logic and security checks.
The Encapsulated Approach (Restricted Access)
By hiding the raw data and exposing controlled behaviors, we protect the integrity of the object's state.
class BankAccount {
private double balance; // Hidden from external access
// Controlled entry point for increasing balance
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
// Controlled entry point for decreasing balance
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
} else {
System.out.println("Invalid amount or Insufficient balance!");
}
}
// Controlled read-only access
public double getBalance() {
return balance;
}
}
3. Java Access Modifiers Explained
Access modifiers determine the visibility of classes, variables, methods, and constructors. Java provides four levels of access control:
Root-Level Class Restrictions
While class members (variables/methods) can use all four modifiers, root-level (outer) classes can only be declared as public or default.
Note: Root-level classes cannot be marked as
privateorprotected.
- A
privateroot class would mean no other class could ever access it, making it completely useless.protectedrelies on inheritance relationships which apply to class members, not the root-level container itself.
4. Controlled Access: Getters, Setters, and Business Validations
To allow controlled read and write access to private fields, Java developers use Getters and Setters. These are public methods that act as intermediaries.
Student Class with Validated Setters
class Student {
private String name;
private int age;
private String college;
// Constructor
public Student(String name, int age, String college) {
this.name = name;
setAge(age); // Use setter to enforce validation during initialization
this.college = college;
}
// Getter for Name (Read-Only)
public String getName() {
return name;
}
// Setter for Name with validation
public void setName(String name) {
if (name != null && !name.trim().isEmpty()) {
this.name = name;
}
}
// Getter for Age
public int getAge() {
return age;
}
// Setter for Age with business rule validation
public void setAge(int age) {
if (age >= 17 && age <= 100) { // Enforce realistic student age limits
this.age = age;
} else {
System.out.println("Invalid age provided!");
}
}
// Getter for College
public String getCollege() {
return college;
}
// Setter for College with simulated validation
public void setCollege(String college) {
// Conceptually: We could call an API or check a database list of valid universities
if (isValidCollege(college)) {
this.college = college;
} else {
System.out.println("College does not exist in our registry!");
}
}
private boolean isValidCollege(String collegeName) {
// Simple mock validation
return collegeName.equals("IIT Guwahati") || collegeName.equals("IIT Delhi");
}
}
5. Understanding Java Packages & Namespace Management
A Package is physically a folder on your disk that groups related classes and interfaces together. Packages prevent naming collisions and help organize large codebases.
Directory Structure Example
Consider a scenario where both a school and a college package contain a class named Student.
Declaring and Importing Packages
Inside college/Student.java:
package college; // Must be the very first line of code
public class Student {
public void print() {
System.out.println("College Student");
}
}
Inside school/Student.java:
package school;
public class Student {
public void print() {
System.out.println("School Student");
}
}
Resolving Class Name Collisions in Demo2.java
If you need to use both Student classes in the same file, you cannot import both directly using simple names because they will collide. Instead, you must use their Fully Qualified Names.
import college.Student; // Imports the college version
public class Demo2 {
public static void main(String[] args) {
// 1. Using the imported simple name (College Student)
Student collegeStudent = new Student();
collegeStudent.print();
// 2. Using the fully qualified name to avoid collision (School Student)
school.Student schoolStudent = new school.Student();
schoolStudent.print();
}
}
Wildcard Imports
You can import all classes within a package using the wildcard * operator:
import college.*; // Imports Student, Teacher, and any other class in 'college'
6. How the JVM Loads Packages at Runtime
Java provides both User-Defined Packages (like school and college) and Inbuilt Packages (provided by the Java Standard Library).
Common Inbuilt Packages
java.lang: Contains fundamental classes (e.g.,String,System,Math). This package is automatically imported into every Java file without explicit declaration.java.util: Contains utility classes and the Collections Framework (e.g.,ArrayList,Scanner).java.io: Contains classes for input and output stream operations.
JVM Compilation and Loading Flow
When you compile and run a class that uses external imports, the compiler and JVM resolve dependencies by loading compiled bytecode (.class) files into memory.
Demo2.java (Source Code)
|
| Compiled by javac
v
Demo2.class (Bytecode)
|
| JVM executes Demo2.class
| Finds import: java.util.ArrayList
v
JVM locates ArrayList.class in JDK Runtime
|
| Loads ArrayList bytecode into memory
v
Execution completes without ambiguity
7. Introduction to Inheritance & The "Is-A" Relationship
Inheritance is the mechanism by which one class acquires the properties (fields) and behaviors (methods) of another class. It models the "Is-A" relationship found in the real world.
Real-World Examples
- Car Is-A Vehicle
- Bike Is-A Vehicle
- EngineeringStudent Is-A Student
Key Terminology
- Super Class (Parent Class): The class whose properties are inherited.
- Sub Class (Child Class): The class that inherits from the super class. It can add its own unique fields and methods in addition to those inherited.
8. Code Reusability in Action: Implementing Inheritance
By using the extends keyword, a subclass automatically inherits all non-private members of its parent class, eliminating redundant code.
Redundancy-Free Implementation
// Parent Class
class Student {
public void markAttendance() {
System.out.println("Attendance marked.");
}
}
// Child Class inheriting from Student
class EngineeringStudent extends Student {
// Inherits markAttendance() automatically
// Unique behavior specific to Engineering Students
public void attendLab() {
System.out.println("Engineering lab session attended.");
}
}
public class Demo3 {
public static void main(String[] args) {
EngineeringStudent engStudent = new EngineeringStudent();
// Calling inherited method
engStudent.markAttendance();
// Calling subclass-specific method
engStudent.attendLab();
Student regularStudent = new Student();
regularStudent.markAttendance();
// regularStudent.attendLab(); // ERROR: Parent class does not know about child-specific methods!
}
}
9. The Four Types of Inheritance
Visualizing Multi-Level and Hierarchical Structures
Multi-Level:
Hierarchical:
Student (Parent)
/ \
/ \
EngineeringStudent MedicalStudent
(Child A) (Child B)
10. Why Java Disallows Multiple Inheritance: The Diamond Problem
Java does not allow a class to extend multiple classes (e.g., class Child extends ParentA, ParentB is illegal). This restriction prevents ambiguity caused by the Diamond Problem.
The Diamond Problem Workflow
Code Simulation of the Ambiguity
If Java allowed multiple inheritance, the following scenario would break runtime execution:
/* CONCEPTUAL ONLY - THIS WILL NOT COMPILE IN JAVA */
class ParentA {
public void show() {
System.out.println("Parent A");
}
}
class ParentB {
public void show() {
System.out.println("Parent B");
}
}
// If this were allowed:
class Child extends ParentA, ParentB {
// Inherits both show() methods
}
// In Main:
Child obj = new Child();
obj.show(); // Which 'show()' should execute? ParentA's or ParentB's?
Because the compiler cannot resolve this ambiguity, multiple inheritance with classes is banned in Java. It can, however, be achieved safely using Interfaces.
11. Mastering the 'super' Keyword
While the this keyword refers to the current object instance, the super keyword refers to the immediate parent class instance.
Three Primary Use Cases of super
Use Case 1: Accessing Parent Variables (Resolving Name Shadowing)
If a child class declares a variable with the same name as a parent class variable, the parent's variable is shadowed. Use super to access it.
class Parent {
int x = 10;
}
class Child extends Parent {
int x = 20;
public void printX() {
System.out.println("Child's x: " + x); // Prints 20
System.out.println("Parent's x: " + super.x); // Prints 10
}
}
Use Case 2: Invoking Parent Methods
Use super to call a parent's method when the child class has overridden it.
class Student {
public void printDetails() {
System.out.println("Printing Student metadata...");
}
}
class EngineeringStudent extends Student {
@Override
public void printDetails() {
super.printDetails(); // Invokes parent's printDetails()
System.out.println("Printing Engineering-specific metadata...");
}
}
Use Case 3: Calling Parent Constructors (Constructor Cheining)
You can call a parent class constructor using super(). This call must be the very first statement in the child class constructor.
class Student {
String name;
int age;
// Parameterized Parent Constructor
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
class EngineeringStudent extends Student {
String college;
public EngineeringStudent(String name, int age, String college) {
// Call parent constructor to initialize inherited fields
super(name, age);
this.college = college; // Initialize child-specific field
}
}
Note: If you do not explicitly call
super()in a child constructor, the Java compiler automatically inserts an implicit, no-argumentsuper()call as the first line. If the parent class only defines a parameterized constructor and lacks a default constructor, omitting the explicitsuper(arguments)call will cause a compile-time error.
Summary and Key Takeaways
- Encapsulation protects data integrity by combining variables and methods into a single class and restricting direct access using the
privatemodifier. - Getters and Setters provide controlled access to private fields, allowing you to validate data before reading or writing it.
- Packages organize code into hierarchical folder structures, preventing class name collisions across different modules.
- Fully qualified names (e.g.,
package.ClassName) resolve naming conflicts when two imported packages contain classes with identical names. - Inheritance promotes code reusability by allowing a subclass to inherit fields and methods from a superclass using the
extendskeyword. - Java does not support multiple inheritance with classes to avoid the Diamond Problem, which causes method call ambiguity at runtime.
- The
superkeyword allows subclasses to access shadowed variables, call overridden methods, and invoke parent constructors. - Constructor chaining requires any explicit call to
super()orthis()to be the absolute first statement inside a constructor's body.