Skip to content

Chapter 5 Object Oriented Programming in Java

Java is an Object-Oriented Programming (OOP) language. OOP is a programming paradigm that uses "objects" to design software. It offers several benefits like code reuse, data hiding, easy maintenance, and flexibility. This chapter will introduce the OOP concepts and how they are applied in Java, focusing on classes and objects.

Understanding OOPs

At its core, OOP is about organizing and structuring your code in a way that's easy to manage, maintain, and understand. It revolves around four main principles:

  • Encapsulation: Encapsulation is about binding data (variables) and methods (functions) together in a single unit known as a class. It also involves data hiding - the data of a class is hidden from other classes, and it can be accessed through methods.

  • Inheritance: Inheritance is a mechanism by which one class acquires (inherits) the properties (fields) and methods of another class. The class that is inherited is known as the parent or superclass, and the class that does the inheriting is known as the child or subclass.

  • Polymorphism: Polymorphism means "many forms". In Java, this principle allows methods to perform different things based on the context. It is closely linked with inheritance, and it makes code more flexible and dynamic.

  • Abstraction: Abstraction means only showing essential details and hiding complexities. It reduces code complexity and at the same time makes your code extensible.

Classes and Objects

Object-oriented programming is fundamentally based on classes and objects. A class serves as a blueprint from which individual objects are created. Class definitions include both data (fields) and methods.

Class

A class is a user-defined blueprint or prototype which we use to create objects. This blueprint includes fields (data) and methods to manipulate that data.

Here's an expanded class example:

public class Car {
    // Fields (or instance variables)
    private String model;
    private String color;
    private int year;

    // Constructor
    public Car(String model, String color, int year) {
        this.model = model;
        this.color = color;
        this.year = year;
    }

    // Methods
    public void drive() {
        System.out.println("The car is driving.");
    }

    // Getter and Setter methods
    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    // More getter and setter methods...
}
In this example, the Car class has three fields: model, color, and year. The drive method is an action that we define for the Car class. The Car class also includes a constructor, which is a special method that's used when creating a new object.

We have also included getter and setter methods for the model field. These methods are used to retrieve (get) and change (set) the values of the fields.

Object

Objects are instances of a class. To use the Car class, we create an object of the class:

Car myCar = new Car("Toyota", "Red", 2022);

In this line of code, new Car("Toyota", "Red", 2022) creates a new Car object, and Car myCar declares a variable myCar to hold the Car object.

We can now use the myCar object to call the methods defined in the Car class:

myCar.drive();  // Outputs: The car is driving.

We can also use the getter and setter methods to access and modify the model of the Car:

String model = myCar.getModel();  // Gets the model of the car
myCar.setModel("Honda");  // Changes the model of the car

This approach provides more control over how fields are accessed and modified, and is a core aspect of encapsulation, one of the four main principles of object-oriented programming.

Inheritance

Inheritance is a mechanism wherein a new class is derived from an existing class. The class which is inherited is known as the parent or superclass, and the class that does the inheriting is known as the child or subclass.

Inheritance allows the child class to reuse methods and variables of the parent class, promoting code reusability and logical grouping of related code.

Here's how you define a subclass in Java:

class ChildClass extends ParentClass {
    // fields
    // methods
}

For example, suppose we have a general Vehicle class, and we want to create a specific Car class. We can use inheritance to create the Car class as a subclass of the Vehicle class:

public class Vehicle {
    protected String brand;

    public Vehicle(String brand) {
        this.brand = brand;
    }

    public void honk() {
        System.out.println("The vehicle is honking!");
    }
}

public class Car extends Vehicle {
    private String model;

    public Car(String brand, String model) {
        super(brand);
        this.model = model;
    }

    public void drive() {
        System.out.println("The car is driving.");
    }
}

In this case, Car is the subclass and Vehicle is the superclass. The Car class inherits the brand field and the honk method from the Vehicle class, and it adds a new field model and a new method drive.

We can now create a Car object and call both the methods from the Car class and the inherited methods from the Vehicle class:

Car myCar = new Car("Toyota", "Corolla");
myCar.honk();  // Outputs: The vehicle is honking!
myCar.drive();  // Outputs: The car is driving.

As you can see, inheritance allows you to build a hierarchy of classes where subclasses can inherit and reuse code from their superclasses, while adding their own specific features.

Polymorphism

Polymorphism is another fundamental concept in object-oriented programming. It allows objects of different classes to be treated as objects of a superclass. The term polymorphism means "many forms", indicating that the same piece of code can behave differently based on the runtime type of the object it's acting upon.

In Java, polymorphism is achieved through method overriding and interfaces.

Method Overriding

Method overriding is a feature that allows a subclass to provide a specific implementation of a method that is already defined in its superclass. When a method in a subclass has the same name, parameters, and return type as a method in its superclass, the method in the subclass is said to override the method in the superclass.

Here's an example:

public class Vehicle {
    public void move() {
        System.out.println("The vehicle is moving.");
    }
}

public class Car extends Vehicle {
    @Override
    public void move() {
        System.out.println("The car is driving.");
    }
}

In this case, the Car class overrides the move method of the Vehicle class. Now, if we create a Car object and call the move method, the overridden method in the Car class will be called:

Vehicle myVehicle = new Car();
myVehicle.move();  // Outputs: The car is driving.

Even though the type of the myVehicle variable is Vehicle, the move method of the Car class is called because the runtime type of the myVehicle object is Car. This is called dynamic method dispatch and it's a key aspect of polymorphism in Java.

Interfaces

In Java, an interface is a reference type, similar to a class, that can contain only constants, method signatures, default methods, static methods, and nested types. Interfaces support multiple inheritance, which classes do not.

An interface is defined using the interface keyword:

interface Animal {
    void makeSound();
}

The Animal interface includes a single method, makeSound(). This method has no body — it's an abstract method.

Implementing an Interface

When a class implements an interface, it provides a concrete implementation for all the abstract methods in the interface. To declare that a class is implementing an interface, we use the implements keyword:

class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("The dog barks.");
    }
}

In the above example, the Dog class is implementing the Animal interface and providing an implementation for the makeSound method.

A class can implement multiple interfaces. To do so, simply separate the interface names with a comma:

class Dog implements Animal, Pet {
    // Implement methods from both interfaces...
}

Using Interfaces

The main purpose of an interface is to enforce particular behaviour for classes. You can use an interface as a method parameter or a return type. You can also use an interface as a field type.

class PetShop {
    Animal pet;

    public PetShop(Animal pet) {
        this.pet = pet;
    }

    public void showPetSound() {
        pet.makeSound();
    }
}

public static void main(String[] args) {
    Dog myDog = new Dog();
    PetShop shop = new PetShop(myDog);
    shop.showPetSound();  // Outputs: The dog barks.
}

In this example, the PetShop class has a field pet of type Animal. We can pass any object that implements the Animal interface to the PetShop constructor. When we call the showPetSound method, it calls the makeSound method on the Animal object, which results in "The dog barks." being printed to the console.

Default Methods and Static Methods

From Java 8 onwards, interfaces can also contain default and static methods.

Default Methods: Default methods enable new functionality to be added to the interfaces of libraries and ensure binary compatibility with code written for older versions of those interfaces. They are defined using the default keyword:

interface Animal {
    void makeSound();

    default void eat() {
        System.out.println("The animal eats.");
    }
}

Now, any class implementing Animal interface has access to eat method as well.

Static Methods: Static methods in interfaces are similar to default methods, except that we cannot override them in the classes that implement the interfaces. They are defined using the static keyword:

interface Animal {
    void makeSound();

    static void breathe() {
        System.out.println("The animal breathes.");
    }
}

You can call static methods directly from the interface name, like Animal.breathe().

Abstraction

Abstraction is a process of hiding the implementation details from the user and providing only the functionality. In Java, abstraction is achieved using abstract classes and interfaces.

Abstract Classes

An abstract class in Java is a class that can't be instantiated, which means you cannot create new instances of an abstract class. The purpose of an abstract class is to function as a base for subclasses. It works as a template intended to be extended by other classes to create a full implementation.

You define an abstract class using the abstract keyword:

abstract class Animal {
    abstract void makeSound();
}

In the Animal abstract class, there's an abstract method makeSound that has no body. If a class includes abstract methods, then the class itself must be declared abstract.

A concrete class (a class that can be instantiated) that extends an abstract class must provide an implementation for all abstract methods:

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("The dog barks.");
    }
}

In this case, the Dog class extends the Animal abstract class and provides an implementation for the makeSound method.

Abstraction Through Interfaces

As we discussed earlier, interfaces play a vital role in abstraction. An interface can only declare methods but can't implement them (with the exception of default and static methods introduced in Java 8). All methods declared in an interface are implicitly abstract unless they're static or default.

Here's how you can use an interface to achieve abstraction:

interface Animal {
    void makeSound();
}

class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("The dog barks.");
    }
}

In this case, the Dog class implements the Animal interface and provides an implementation for the makeSound method.

Abstraction is fundamental in large systems development. It allows us to break complex systems into smaller, more manageable parts. Through abstraction, we can simplify our code, improve modularity, and enhance maintainability.

Encapsulation

Encapsulation is a fundamental concept in object-oriented programming. It refers to the bundling of data, along with the methods that operate on that data, into a single unit called a class. In Java, encapsulation is typically achieved by making the class variables private and providing public setter and getter methods for those variables.

This concept serves as a protective shield that prevents the data from being accessed by code outside this shield.

Consider the following example:

public class Student {
    private String name;  // Private variable

    public String getName() {  // Getter Method
        return name;
    }

    public void setName(String name) {  // Setter Method
        this.name = name;
    }
}

In the Student class, name is a private variable, making it accessible only within the Student class. We cannot directly access the name variable from outside the class. However, we provide two public methods - getName and setName. The getName method returns the value of name, and the setName method sets the value of name.

This concept is particularly useful when you want to restrict access to certain class variables and methods. Java provides several access modifiers to set access levels for classes, variables, methods, and constructors:

Modifier Class Package Subclass World
public Y Y Y Y
protected Y Y Y N
no modifier Y Y N N
private Y N N N

Here's a brief explanation of each column:

  • Class: The member can be accessed from within the same class.
  • Package: The member can be accessed from other classes in the same package.
  • Subclass: The member can be accessed from subclasses.
  • World: The member can be accessed from any class.

And here's a brief explanation of each access modifier:

  • public: The field is accessible from all classes.
  • protected: The field is accessible within the same package and from subclasses.
  • no modifier (default): The field is accessible within the same package.
  • private: The field is accessible only within the same class.

Encapsulation brings several benefits:

  • Control over data: Encapsulation gives you control over who can access and modify your data.
  • Increased Security: As encapsulation hides the variables, they can't be accessed directly.
  • Flexibility and Maintainability: If a change is needed, only the setter and getter methods would need to be changed.
  • Reusability: Encapsulated code is more modular and can be reused across several programs.

In conclusion, encapsulation in Java allows you to protect your data by hiding its attributes (variables) and only allowing access through methods (getter and setter). This can make your code more secure, flexible, and easy to maintain.

Summarization

Concept Description Example
Inheritance Enables one class to inherit properties and methods from another class class Car extends Vehicle
Polymorphism Allows us to perform a single action in different ways Overriding the move method in different classes that extend the Vehicle class
Abstraction Hides the details and shows only the essentials abstract class Animal with an abstract method makeSound
Encapsulation Hides the data to protect it from outside interference class Student with a private variable name and public getter and setter methods