The Ultimate Guide to Java OOP Concepts

Learn the nuances of classes, constructors, inheritance, and other OOP features in Java.

Jignesh Rathod
11 min read5 days ago
java programming logo

Object-Oriented Programming (OOP) is a programming paradigm that uses objects and classes to structure code. It helps in organizing and managing complex programs by breaking them into smaller, reusable components.

OOP provides concepts such as Encapsulation, Inheritance, Polymorphism, and Abstraction, which enhance code reusability, maintainability, and flexibility.

1. Class and Object

Class is a blueprint for creating objects. It defines the properties (variables) and behaviors (methods) that objects of that class will have.

An object is an instance of a class. It is created from a class and represents a real-world entity with specific values for its properties.

// Defining a class
class Car {
// Properties (variables)
String brand;
int speed;

// Method (behavior)
void display() {
System.out.println("Car Brand: " + brand);
System.out.println("Car Speed: " + speed + " km/h");
}
}

// Main class
public class Main {
public static void main(String[] args) {
// Creating an object of the Car class
Car myCar = new Car();

// Assigning values to object properties
myCar.brand = "Toyota";
myCar.speed = 120;

// Calling the method
myCar.display();
}
}

Explanation

  • Class (Car): Defines attributes (brand and speed) and behavior (display() method).
  • Object (myCar): Created from the Car class using new Car().
  • Assigning Values: myCar.brand = "Toyota"; assigns a value to the brand attribute.
  • Calling Method: myCar.display(); prints the car details.

2. Encapsulation

Encapsulation is one of the core principles of Object-Oriented Programming (OOP). It is the process of wrapping data (variables) and methods (functions) into a single unit (class) and restricting direct access to some of an object’s details.

Key Features of Encapsulation

  • Data Hiding: The internal representation of an object is hidden from the outside world.
  • Controlled Access: Access to data is controlled using access specifiers.
  • Increased Security: Prevents unintended modifications of data.
  • Reusability & Maintainability: Makes code easier to manage and modify.

Access Specifiers

  • private: Most restrictive, only inside the same class.
  • default : Accessible within the package (No modifier needed).
  • protected: Accessible in the same package & subclasses in other packages.
  • public: No restrictions, accessible everywhere.
class Person {
// Private data variables (hidden from outside access)
private String name;
private int age;

// Public getter method to access private variable
public String getName() {
return name;
}

// Public setter method to modify private variable
public void setName(String newName) {
this.name = newName;
}

public int getAge() {
return age;
}

public void setAge(int newAge) {
if (newAge > 0) {
this.age = newAge;
} else {
System.out.println("Age must be positive.");
}
}
}

// Main class
public class Main {
public static void main(String[] args) {
Person p1 = new Person();

// Setting values using setter methods
p1.setName("John Doe");
p1.setAge(25);

// Accessing values using getter methods
System.out.println("Name: " + p1.getName());
System.out.println("Age: " + p1.getAge());
}
}

3. Inheritance

Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows one class to inherit properties and behaviors (fields and methods) from another class. It promotes code reusability, maintainability, and modularity.

Key Features of Inheritance

  • Enables code reuse, reducing redundancy.
  • Supports hierarchical classification (e.g., Animal → Dog).
  • Helps in extending the functionality of existing code.
class Parent {
void display() {
System.out.println("This is the Parent class");
}
}

class Child extends Parent { // 'extends' keyword is used for inheritance
void show() {
System.out.println("This is the Child class");
}
}

public class Main {
public static void main(String[] args) {
Child obj = new Child();
obj.display(); // Inherited method from Parent
obj.show(); // Child's own method
}
}

4. Types of Inheritance

Java supports different types of inheritance.

Single Inheritance

  • A subclass inherits from a single superclass.
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}

class Dog extends Animal {
void bark() {
System.out.println("Dog barks.");
}
}

public class Main {
public static void main(String[] args) {
Dog d = new Dog();
d.eat(); // Inherited method
d.bark(); // Child class method
}
}

Multilevel Inheritance

  • A class inherits from another class, which itself is inherited from another class, forming a chain.
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}

class Dog extends Animal {
void bark() {
System.out.println("Dog barks.");
}
}

class Puppy extends Dog {
void weep() {
System.out.println("Puppy weeps.");
}
}

public class Main {
public static void main(String[] args) {
Puppy p = new Puppy();
p.eat(); // Inherited from Animal
p.bark(); // Inherited from Dog
p.weep(); // Puppy’s own method
}
}
  • Puppy inherits from Dog, which inherits from Animal.

Hierarchical Inheritance

  • A single parent class has multiple child classes.
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}

class Dog extends Animal {
void bark() {
System.out.println("Dog barks.");
}
}

class Cat extends Animal {
void meow() {
System.out.println("Cat meows.");
}
}

public class Main {
public static void main(String[] args) {
Dog d = new Dog();
d.eat(); // Inherited from Animal
d.bark(); // Dog's own method

Cat c = new Cat();
c.eat(); // Inherited from Animal
c.meow(); // Cat's own method
}
}
  • Dog and Cat both inherit from Animal, but have their own behaviors.

5. Polymorphism

The word “Polymorphism” means “many forms”, and in Java, it allows a single method, class, or interface to have multiple implementations.

Key Features of Polymorphism

  • Enhances code reusability and maintainability.
  • Allows flexibility in method usage.
  • Helps achieve dynamic behavior at runtime.

Types of Polymorphism

  1. Compile-time Polymorphism (Method Overloading)
  2. Runtime Polymorphism (Method Overriding)

Compile-time Polymorphism (Method Overloading)

  • Occurs when multiple methods in the same class have the same name but different parameters.
  • It is determined at compile time.
  • It improves code readability.
class MathOperations {
// Method with two parameters
int add(int a, int b) {
return a + b;
}

// Method with three parameters (overloaded)
int add(int a, int b, int c) {
return a + b + c;
}

// Method with double parameters (different data type)
double add(double a, double b) {
return a + b;
}
}

public class Main {
public static void main(String[] args) {
MathOperations obj = new MathOperations();

System.out.println(obj.add(5, 10)); // Calls first method
System.out.println(obj.add(5, 10, 15)); // Calls second method
System.out.println(obj.add(5.5, 10.5)); // Calls third method
}
}
  • Three methods with the same name (add) but different parameters.
  • The correct method is selected at compile time based on arguments.

Runtime Polymorphism (Method Overriding)

  • Occurs when a subclass provides a specific implementation of a method already defined in its superclass.
  • It is determined at runtime.
  • Enables dynamic method dispatch.
  • The method in the subclass must have the same name and parameters as the superclass.
  • The subclass method cannot have a lower access level (e.g., changing public to private is not allowed).
class Animal {
void makeSound() {
System.out.println("Animal makes a sound.");
}
}

class Dog extends Animal {
@Override
void makeSound() { // Overriding method
System.out.println("Dog barks.");
}
}

public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // Upcasting
myAnimal.makeSound(); // Calls the Dog's overridden method
}
}

Method Overriding with super

If we want to call the parent class method inside the overridden method, we use super.

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

class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // Calls parent method
System.out.println("Dog barks.");
}
}

public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.makeSound(); // Calls both parent and child method
}
}

6. Constructor

A constructor in Java is a special method that is used to initialize objects. It is called automatically when an object of a class is created.

Key Features of Constructors

  • Same name as the class.
  • No return type (not even void).
  • Called automatically when an object is instantiated.
  • Used to initialize objects.

Types of Constructors

  1. Default Constructor (No-argument Constructor)
  2. Parameterized Constructor
  3. Copy Constructor (Manually Implemented)

Default Constructor

  • A default constructor is a constructor that takes no parameters and initializes default values.
class Car {
Car() { // Default constructor
System.out.println("Car is created!");
}
}

public class Main {
public static void main(String[] args) {
Car myCar = new Car(); // Constructor is automatically called
}
}
  • The constructor is automatically invoked when new Car() is executed.

Parameterized Constructor

  • A parameterized constructor allows us to pass arguments during object creation.
class Car {
String model;
int year;

Car(String m, int y) { // Parameterized constructor
model = m;
year = y;
}

void display() {
System.out.println("Model: " + model + ", Year: " + year);
}
}

public class Main {
public static void main(String[] args) {
Car myCar = new Car("Toyota", 2023); // Passing parameters
myCar.display();
}
}
  • This allows custom initialization based on the parameters passed.

Copy Constructor (Manually Implemented)

  • A copy constructor creates a new object by copying values from an existing object.
class Car {
String model;
int year;

// Parameterized constructor
Car(String m, int y) {
model = m;
year = y;
}

// Copy constructor
Car(Car c) {
model = c.model;
year = c.year;
}

void display() {
System.out.println("Model: " + model + ", Year: " + year);
}
}

public class Main {
public static void main(String[] args) {
Car car1 = new Car("Honda", 2022); // Original object
Car car2 = new Car(car1); // Copying object

car1.display();
car2.display();
}
}
  • The copy constructor creates car2 as an exact copy of car1.

Constructor Overloading

  • Just like methods, constructors can be overloaded to provide multiple ways to initialize an object.
class Student {
String name;
int age;

// Default constructor
Student() {
name = "Unknown";
age = 0;
}

// Parameterized constructor
Student(String n, int a) {
name = n;
age = a;
}

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

public class Main {
public static void main(String[] args) {
Student s1 = new Student(); // Calls default constructor
Student s2 = new Student("John", 20); // Calls parameterized constructor

s1.display();
s2.display();
}
}
  • Overloading provides flexibility in object creation.

Using this in Constructor

  • The this keyword refers to the current instance of the class. It is often used in constructors to differentiate instance variables from parameters.
class Person {
String name;
int age;

// Using 'this' to differentiate instance variables
Person(String name, int age) {
this.name = name;
this.age = age;
}

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

public class Main {
public static void main(String[] args) {
Person p1 = new Person("Alice", 25);
p1.display();
}
}
  • this.name = name; ensures that the instance variable name is assigned correctly.

Calling One Constructor from Another (this())

  • this() can be used to call another constructor within the same class.
class Employee {
String name;
int age;

// Constructor 1
Employee() {
this("Default Name", 18); // Calls Constructor 2
}

// Constructor 2
Employee(String name, int age) {
this.name = name;
this.age = age;
}

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

public class Main {
public static void main(String[] args) {
Employee e1 = new Employee(); // Calls default constructor
Employee e2 = new Employee("Bob", 30); // Calls parameterized constructor

e1.display();
e2.display();
}
}
  • Constructor chaining improves code reusability.

7. Constructors in Inheritance

In Java, constructors are not inherited by subclasses, but the parent class constructor is always called when a subclass object is created. This ensures that the superclass is properly initialized before the subclass constructor executes.

How Constructors Work in Inheritance

  • When a child class object is created, the constructor of the parent class is executed first, followed by the child class constructor.
  • super() is used to explicitly call the parent class constructor.
  • If super() is not written, Java automatically calls the default constructor of the parent class.

Constructor Execution

class Parent {
Parent() {
System.out.println("Parent class constructor called.");
}
}

class Child extends Parent {
Child() {
System.out.println("Child class constructor called.");
}
}

public class Main {
public static void main(String[] args) {
Child obj = new Child(); // Creates an object of Child
}
}
  • Even though we created an object of Child, the Parent constructor runs first because Java automatically calls super() at the start of Child's constructor.

Using super() to Call Parent Constructor

  • If the parent class has a parameterized constructor, we must explicitly call it using super().
class Parent {
Parent(String msg) {
System.out.println("Parent constructor: " + msg);
}
}

class Child extends Parent {
Child(String msg) {
super(msg); // Calls the Parent constructor
System.out.println("Child constructor: " + msg);
}
}

public class Main {
public static void main(String[] args) {
Child obj = new Child("Hello!");
}
}
  • The super(msg); statement ensures that the parent class constructor is called with an argument.

8. Instance Initialization Block (IIB)

An Instance Initialization Block (IIB) is a block of code inside a class that runs before the constructor when an object is created. It is mainly used to initialize instance variables.

  • Syntax: { /* Code */ } (enclosed in curly braces, but not inside a method or constructor)
  • Runs every time an object is created, before the constructor.
  • Executes in the order it appears in the class.
  • Used to share common initialization code among multiple constructors.
class Example {
// Instance Initialization Block (IIB)
{
System.out.println("Instance Initialization Block executed.");
}

// Constructor
Example() {
System.out.println("Constructor executed.");
}

public static void main(String[] args) {
Example obj1 = new Example(); // Creates first object
Example obj2 = new Example(); // Creates second object
}
}

9. Static and Non-Static Members

In Java, members of a class (variables, methods, blocks, and inner classes) can be classified as static or non-static (instance members).

Static Members

  • Declared using the static keyword.
  • Belong to the class rather than any specific object.
  • Shared among all objects of the class.
  • Loaded into memory only once, when the class is loaded.
  • Can be accessed using the class name (ClassName.member).
  • Do not require object creation to be accessed.

Static Variable

class Student {
static String schoolName = "ABC High School"; // Static variable

String name; // Non-static (Instance) variable
int age;

Student(String name, int age) {
this.name = name;
this.age = age;
}

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

public static void main(String[] args) {
Student s1 = new Student("Alice", 20);
Student s2 = new Student("Bob", 22);

s1.display();
s2.display();

// Changing static variable affects all objects
Student.schoolName = "XYZ Academy";
s1.display();
s2.display();
}
}
  • The static variable schoolName is shared among all objects.
  • When one object changes it, the change reflects in all objects.

Static Method

class MathUtils {
// Static method to calculate square
static int square(int num) {
return num * num;
}
}

class Main {
public static void main(String[] args) {
// Calling the static method from MathUtils class
System.out.println("Square of 5: " + MathUtils.square(5));
}
}
  • Static methods do not require an object to be called.
  • They are called using the class name (MathUtils.square(5)).

Static Block

  • A static block is used to initialize static variables and is executed only once when the class is loaded.
class TestUtils {
static int x;

// Static Block
static {
x = 100;
System.out.println("Static block executed.");
}
}

class Main {
public static void main(String[] args) {
// Accessing the static variable from TestUtils class
System.out.println("Value of x: " + TestUtils.x);
}
}
  • The static block runs only once when the class is loaded.

Non-Static Members (Instance Members)

  • Do not use the static keyword.
  • Belong to individual objects, not the class.
  • Every object has its own copy of instance members.
  • Can only be accessed via an object.
  • Instance members can access both static and non-static members.
class Example {
static int staticVar = 10; // Static variable
int instanceVar = 20; // Instance variable

// Static Method
static void staticMethod() {
System.out.println("Static Method: staticVar = " + staticVar);
// Error! Cannot access instanceVar in a static method.
// System.out.println("Instance Variable: " + instanceVar);
}

// Instance Method
void instanceMethod() {
System.out.println("Instance Method: staticVar = " + staticVar);
System.out.println("Instance Method: instanceVar = " + instanceVar);
}

public static void main(String[] args) {
// Calling static method
Example.staticMethod();

// Creating an object to call instance method
Example obj = new Example();
obj.instanceMethod();
}
}

Important Rules

  • Static members cannot access non-static members directly because non-static members belong to objects.
  • Non-static members can access both static and non-static members.
  • Static methods can only call other static methods or use static variables.
  • Static blocks execute only once when the class is loaded.

When to Use Static and Non-Static Members?

  • Use static when the data is shared among all objects (e.g., constants, utility methods).
  • Use non-static when each object should have its own copy of a variable (e.g., name, age).

10. Universal Class (Object Class)

Coming Soon… :-)

--

--

Jignesh Rathod
Jignesh Rathod

Written by Jignesh Rathod

Passionate about teaching, simplifying technology, creating innovative solutions, and empowering the next generation of developers.

No responses yet