Object-Oriented Programming in Java: Classes, Objects, and Core OOP Principles
Welcome back to the Java Fundamentals series! 👋
Object-Oriented Programming (OOP) is the backbone of Java. Understanding OOP concepts like classes, objects, inheritance, polymorphism, encapsulation, and abstraction is crucial for becoming an effective Java developer.
Classes and Objects
Classes are blueprints or templates that define the structure and behavior of objects. Objects are instances of classes that contain actual data and can perform actions.
Creating a Class
Here's how to create a basic class in Java:
public class Car {
// Instance variables (attributes)
private String brand;
private String model;
private int year;
private double price;
// Constructor
public Car(String brand, String model, int year, double price) {
this.brand = brand;
this.model = model;
this.year = year;
this.price = price;
}
// Methods (behaviors)
public void startEngine() {
System.out.println(brand + " " + model + " engine started!");
}
public void displayInfo() {
System.out.println("Car: " + year + " " + brand + " " + model +
" - $" + price);
}
// Getters and Setters
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
if (price > 0) {
this.price = price;
}
}
}
Creating and Using Objects
public class CarDemo {
public static void main(String[] args) {
// Creating objects (instances) of the Car class
Car car1 = new Car("Toyota", "Camry", 2023, 25000.0);
Car car2 = new Car("Honda", "Civic", 2022, 22000.0);
// Using object methods
car1.displayInfo();
car1.startEngine();
car2.displayInfo();
car2.startEngine();
// Accessing object properties through getters/setters
System.out.println("Car 1 brand: " + car1.getBrand());
car2.setPrice(23000.0);
System.out.println("Updated car 2 price: $" + car2.getPrice());
}
}
Output:
Car: 2023 Toyota Camry - $25000.0
Toyota Camry engine started!
Car: 2022 Honda Civic - $22000.0
Honda Civic engine started!
Car 1 brand: Toyota
Updated car 2 price: $23000.0
Constructors
Constructors are special methods that initialize objects when they are created. They have the same name as the class and no return type.
Types of Constructors
1. Default Constructor
public class Student {
private String name;
private int age;
// Default constructor
public Student() {
this.name = "Unknown";
this.age = 0;
}
public void displayInfo() {
System.out.println("Student: " + name + ", Age: " + age);
}
}
2. Parameterized Constructor
public class Student {
private String name;
private int age;
// Parameterized constructor
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// Constructor overloading
public Student(String name) {
this.name = name;
this.age = 18; // Default age
}
public void displayInfo() {
System.out.println("Student: " + name + ", Age: " + age);
}
}
3. Copy Constructor (Manual Implementation)
public class Student {
private String name;
private int age;
// Regular constructor
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// Copy constructor
public Student(Student other) {
this.name = other.name;
this.age = other.age;
}
public void displayInfo() {
System.out.println("Student: " + name + ", Age: " + age);
}
}
Constructor Example Usage
public class ConstructorDemo {
public static void main(String[] args) {
// Using different constructors
Student student1 = new Student("Alice", 20);
Student student2 = new Student("Bob");
Student student3 = new Student(student1); // Copy constructor
student1.displayInfo(); // Student: Alice, Age: 20
student2.displayInfo(); // Student: Bob, Age: 18
student3.displayInfo(); // Student: Alice, Age: 20
}
}
Inheritance
Inheritance allows a class to inherit properties and methods from another class. It promotes code reusability and establishes an "is-a" relationship.
Basic Inheritance Example
// Parent class (Superclass)
public class Vehicle {
protected String brand;
protected int year;
protected double price;
public Vehicle(String brand, int year, double price) {
this.brand = brand;
this.year = year;
this.price = price;
}
public void start() {
System.out.println(brand + " vehicle started.");
}
public void displayInfo() {
System.out.println("Vehicle: " + year + " " + brand + " - $" + price);
}
}
// Child class (Subclass)
public class Motorcycle extends Vehicle {
private String engineType;
public Motorcycle(String brand, int year, double price, String engineType) {
super(brand, year, price); // Call parent constructor
this.engineType = engineType;
}
// Method overriding
@Override
public void start() {
System.out.println(brand + " motorcycle engine revved up!");
}
// Additional method specific to Motorcycle
public void wheelie() {
System.out.println(brand + " motorcycle doing a wheelie!");
}
@Override
public void displayInfo() {
super.displayInfo(); // Call parent method
System.out.println("Engine Type: " + engineType);
}
}
Multilevel Inheritance
// Grandparent class
public class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
public void move() {
System.out.println(brand + " is moving.");
}
}
// Parent class
public class Car extends Vehicle {
protected int doors;
public Car(String brand, int doors) {
super(brand);
this.doors = doors;
}
public void honk() {
System.out.println(brand + " car is honking!");
}
}
// Child class
public class SportsCar extends Car {
private int topSpeed;
public SportsCar(String brand, int doors, int topSpeed) {
super(brand, doors);
this.topSpeed = topSpeed;
}
public void turboBoost() {
System.out.println(brand + " sports car activated turbo boost! Top speed: " + topSpeed + " mph");
}
}
Inheritance Usage Example
public class InheritanceDemo {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle("Generic", 2020, 15000);
Motorcycle motorcycle = new Motorcycle("Harley", 2023, 18000, "V-Twin");
SportsCar sportsCar = new SportsCar("Ferrari", 2, 200);
// Using inherited and overridden methods
vehicle.start(); // Generic vehicle started.
motorcycle.start(); // Harley motorcycle engine revved up!
motorcycle.wheelie(); // Harley motorcycle doing a wheelie!
sportsCar.move(); // Ferrari is moving.
sportsCar.honk(); // Ferrari car is honking!
sportsCar.turboBoost(); // Ferrari sports car activated turbo boost! Top speed: 200 mph
// Polymorphism in action
Vehicle[] vehicles = {vehicle, motorcycle};
for (Vehicle v : vehicles) {
v.start(); // Calls appropriate start() method
}
}
}
Polymorphism
Polymorphism allows objects of different types to be treated as objects of a common base type. It enables a single interface to represent different underlying forms (data types).
Method Overriding (Runtime Polymorphism)
public class Animal {
public void makeSound() {
System.out.println("The animal makes a sound");
}
public void eat() {
System.out.println("The animal eats food");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks: Woof! Woof!");
}
@Override
public void eat() {
System.out.println("The dog eats dog food");
}
// Dog-specific method
public void wagTail() {
System.out.println("The dog wags its tail");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("The cat meows: Meow! Meow!");
}
@Override
public void eat() {
System.out.println("The cat eats cat food");
}
// Cat-specific method
public void purr() {
System.out.println("The cat purrs");
}
}
Method Overloading (Compile-time Polymorphism)
public class Calculator {
// Method overloading - 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;
}
public String add(String a, String b) {
return a + b;
}
}
Polymorphism Example Usage
public class PolymorphismDemo {
public static void main(String[] args) {
// Runtime polymorphism
Animal[] animals = {
new Dog(),
new Cat(),
new Animal()
};
// Same method call, different behaviors
for (Animal animal : animals) {
animal.makeSound(); // Calls appropriate overridden method
animal.eat();
System.out.println("---");
}
// Compile-time polymorphism
Calculator calc = new Calculator();
System.out.println("Add integers: " + calc.add(5, 3));
System.out.println("Add doubles: " + calc.add(5.5, 3.2));
System.out.println("Add three integers: " + calc.add(1, 2, 3));
System.out.println("Add strings: " + calc.add("Hello, ", "World!"));
}
}
Output:
The dog barks: Woof! Woof!
The dog eats dog food
---
The cat meows: Meow! Meow!
The cat eats cat food
---
The animal makes a sound
The animal eats food
---
Add integers: 8
Add doubles: 8.7
Add three integers: 6
Add strings: Hello, World!
Encapsulation
Encapsulation is the practice of hiding internal implementation details and exposing only necessary information through public methods. It's achieved using access modifiers and getter/setter methods.
Access Modifiers
Modifier | Class | Package | Subclass | World |
---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
default | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
Encapsulation Example
public class BankAccount {
// Private fields (encapsulated)
private String accountNumber;
private String accountHolder;
private double balance;
private String pin;
// Constructor
public BankAccount(String accountNumber, String accountHolder, String pin) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.pin = pin;
this.balance = 0.0;
}
// Public methods to interact with private data
public boolean deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: $" + amount);
return true;
}
System.out.println("Invalid deposit amount");
return false;
}
public boolean withdraw(double amount, String inputPin) {
if (!validatePin(inputPin)) {
System.out.println("Invalid PIN");
return false;
}
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("Withdrawn: $" + amount);
return true;
}
System.out.println("Invalid withdrawal amount or insufficient funds");
return false;
}
public double getBalance(String inputPin) {
if (validatePin(inputPin)) {
return balance;
}
System.out.println("Invalid PIN");
return -1;
}
// Private helper method
private boolean validatePin(String inputPin) {
return this.pin.equals(inputPin);
}
// Getters for safe access to some private data
public String getAccountNumber() {
return accountNumber;
}
public String getAccountHolder() {
return accountHolder;
}
// Setter with validation
public boolean changePin(String oldPin, String newPin) {
if (validatePin(oldPin) && newPin.length() >= 4) {
this.pin = newPin;
System.out.println("PIN changed successfully");
return true;
}
System.out.println("PIN change failed");
return false;
}
}
Using the Encapsulated Class
public class EncapsulationDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount("12345", "John Doe", "1234");
// Accessing public methods (safe interface)
account.deposit(1000);
account.deposit(500);
System.out.println("Balance: $" + account.getBalance("1234"));
account.withdraw(200, "1234");
account.withdraw(200, "wrong"); // Wrong PIN
System.out.println("Final Balance: $" + account.getBalance("1234"));
// Cannot directly access private fields
// System.out.println(account.balance); // Compilation error
// account.pin = "0000"; // Compilation error
// Safe access through public methods
System.out.println("Account holder: " + account.getAccountHolder());
account.changePin("1234", "5678");
}
}
Abstraction
Abstraction hides complex implementation details and shows only essential features. It's achieved through abstract classes and interfaces.
Abstract Classes
// Abstract class
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Abstract method (must be implemented by subclasses)
public abstract double calculateArea();
public abstract double calculatePerimeter();
// Concrete method (can be inherited as-is)
public void displayColor() {
System.out.println("Color: " + color);
}
// Concrete method with common behavior
public void displayInfo() {
displayColor();
System.out.println("Area: " + calculateArea());
System.out.println("Perimeter: " + calculatePerimeter());
}
}
// Concrete subclass
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
@Override
public double calculatePerimeter() {
return 2 * Math.PI * radius;
}
}
// Another concrete subclass
public class Rectangle extends Shape {
private double length;
private double width;
public Rectangle(String color, double length, double width) {
super(color);
this.length = length;
this.width = width;
}
@Override
public double calculateArea() {
return length * width;
}
@Override
public double calculatePerimeter() {
return 2 * (length + width);
}
}
Interfaces
// Interface
public interface Drawable {
// All methods in interface are public and abstract by default
void draw();
void erase();
// Default method (Java 8+)
default void display() {
System.out.println("Displaying the drawable object");
}
// Static method (Java 8+)
static void printInfo() {
System.out.println("This is a drawable interface");
}
}
// Another interface
public interface Resizable {
void resize(double factor);
boolean isResizable();
}
// Class implementing multiple interfaces
public class GraphicsShape extends Shape implements Drawable, Resizable {
private boolean resizable;
public GraphicsShape(String color, boolean resizable) {
super(color);
this.resizable = resizable;
}
@Override
public double calculateArea() {
return 0; // Generic implementation
}
@Override
public double calculatePerimeter() {
return 0; // Generic implementation
}
@Override
public void draw() {
System.out.println("Drawing a " + color + " graphics shape");
}
@Override
public void erase() {
System.out.println("Erasing the graphics shape");
}
@Override
public void resize(double factor) {
if (resizable) {
System.out.println("Resizing shape by factor: " + factor);
} else {
System.out.println("Shape is not resizable");
}
}
@Override
public boolean isResizable() {
return resizable;
}
}
Abstraction Example Usage
public class AbstractionDemo {
public static void main(String[] args) {
// Using abstract class through concrete implementations
Shape circle = new Circle("Red", 5.0);
Shape rectangle = new Rectangle("Blue", 4.0, 6.0);
circle.displayInfo();
System.out.println("---");
rectangle.displayInfo();
System.out.println("---");
// Using interfaces
GraphicsShape gShape = new GraphicsShape("Green", true);
gShape.draw();
gShape.display(); // Default method from interface
gShape.resize(1.5);
gShape.erase();
// Static method from interface
Drawable.printInfo();
// Cannot instantiate abstract class or interface
// Shape shape = new Shape("Color"); // Compilation error
// Drawable drawable = new Drawable(); // Compilation error
}
}
Key Java Keywords and Concepts
The this
Keyword
The this
keyword refers to the current object instance.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name; // Distinguish between parameter and instance variable
this.age = age;
}
public Person setName(String name) {
this.name = name;
return this; // Method chaining
}
public Person setAge(int age) {
this.age = age;
return this; // Method chaining
}
public void displayInfo() {
System.out.println("Name: " + this.name + ", Age: " + this.age);
}
public void compareAge(Person other) {
if (this.age > other.age) {
System.out.println(this.name + " is older than " + other.name);
} else if (this.age < other.age) {
System.out.println(this.name + " is younger than " + other.name);
} else {
System.out.println(this.name + " and " + other.name + " are the same age");
}
}
}
The super
Keyword
The super
keyword refers to the parent class.
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating");
}
}
public class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // Call parent constructor
this.breed = breed;
}
@Override
public void eat() {
super.eat(); // Call parent method
System.out.println("The " + breed + " dog is eating dog food");
}
}
The final
Keyword
The final
keyword can be used with variables, methods, and classes.
// Final class - cannot be extended
public final class MathConstants {
// Final variables - constants
public static final double PI = 3.14159;
public static final int MAX_VALUE = 100;
// Final method - cannot be overridden
public final void displayConstants() {
System.out.println("PI: " + PI + ", MAX_VALUE: " + MAX_VALUE);
}
}
public class FinalExample {
public void demonstrateFinal() {
final int localConstant = 42; // Final local variable
// localConstant = 50; // Compilation error
final List<String> list = new ArrayList<>();
list.add("Hello"); // OK - can modify contents
// list = new ArrayList<>(); // Compilation error - cannot reassign
}
}
The static
Keyword
The static
keyword creates class-level members that belong to the class rather than instances.
public class Counter {
private static int totalCount = 0; // Class variable
private int instanceCount = 0; // Instance variable
public Counter() {
totalCount++; // Increment class variable
instanceCount++; // Increment instance variable
}
// Static method
public static int getTotalCount() {
return totalCount;
// return instanceCount; // Compilation error - cannot access instance variable
}
// Instance method
public int getInstanceCount() {
return instanceCount;
}
// Static block - executed when class is first loaded
static {
System.out.println("Counter class loaded");
totalCount = 0;
}
}
Best Practices for OOP in Java
1. Follow Naming Conventions
- Use PascalCase for class names:
BankAccount
,StudentManager
- Use camelCase for method and variable names:
calculateTax()
,firstName
- Use UPPER_SNAKE_CASE for constants:
MAX_SIZE
,DEFAULT_VALUE
2. Encapsulation Guidelines
- Make fields
private
unless there's a specific reason not to - Provide public getter/setter methods for controlled access
- Validate input in setter methods
3. Inheritance Best Practices
- Use inheritance for true "is-a" relationships
- Prefer composition over inheritance when possible
- Make classes
final
if they shouldn't be extended
4. Interface Design
- Keep interfaces focused and cohesive
- Use interfaces to define contracts
- Prefer interfaces over abstract classes for multiple inheritance
5. Method Design
- Keep methods small and focused on a single responsibility
- Use meaningful method names that describe what they do
- Limit the number of parameters (consider using objects for multiple parameters)
Conclusion
Object-Oriented Programming is fundamental to Java development. Understanding these core concepts will make you a more effective Java developer:
- Classes and Objects: The building blocks of OOP
- Constructors: Initialize objects properly
- Inheritance: Reuse code and create hierarchies
- Polymorphism: Write flexible, maintainable code
- Encapsulation: Protect data and provide controlled access
- Abstraction: Hide complexity and focus on essential features
Happy coding! 💻