Object-oriented paradigm
The object-oriented paradigm is often associated with imperative programming, but, in practice, both functional and object-oriented paradigms can coexist. Java is living proof that supports this collaboration.
In the following section, we will briefly highlight the main object-oriented concepts as they are implemented in the Java language.
Objects and classes
Objects are the main elements of an object-oriented programming (OOP) language. An object holds both the state and the behavior.
If we think of classes as a template, objects are the implementation of the template. For example, if human is a class that defines the behavior and properties that a human being can have, you and I are objects of this human class, as we have fulfilled all the requirements of being a human. Or, if we think of car as a class, a particular Honda Civic car will be an object of this class. It will fulfill all the properties and behaviors that a car has, such as it has an engine, a steering wheel, headlights, and so on, and it has behaviors of moving forward, moving backward, and so on. We can see how the object-oriented paradigm can relate to the real world. Almost everything in the real world can be thought of in terms of classes and objects, hence it makes OOP effortless and popular.
Object-oriented programming is based on four fundamental principles:
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism (subtyping polymorphism).
Encapsulation
Encapsulation basically means the binding of attributes and behaviors. The idea is to keep the properties and behavior of an object in one place, so that it is easy to maintain and extend. Encapsulation also provides a mechanism to hide unnecessary details from the user. In Java, we can provide access specifiers to methods and attributes to manage what is visible to a user of the class, and what is hidden.
Encapsulation is one of the fundamental principles of object-oriented languages. It helps in the decoupling of different modules. Decoupled modules can be developed and maintained more or less independently. The technique through which decoupled modules/classes/code are changed internally without affecting their external exposed behavior is called code refactoring.
Abstraction
Abstraction is closely related to encapsulation, and, to some extent, it overlaps with it. Briefly, abstraction provides a mechanism that exposes what an object does and hides how the object does what it's supposed to do.
A real-world example of abstraction is a car. In order to drive a car, we don't really need to know what the car has under the hood, but we need to know the data and behavior it exposes to us. The data is exposed on the car's dashboard, and the behavior is represented by the controls we can use to drive a car.
Inheritance
Inheritance is the ability to base an object or class on another one. There is a parent or base class, which provides the top-level behavior for an entity. Every subclass entity or child class that fulfills the criteria to be a part of the parent class can inherit from the parent class and add additional behavior as required.
Let's take a real-world example. If we think of a Vehicle
as a parent class, we know a Vehicle
can have certain properties and behaviors. For example, it has an engine, doors, and so on, and behavior-wise it can move. Now all entities that fulfill these criteria—for example, Car
, Truck
, Bike
, and so on—can inherit from Vehicle
and add on top of given properties and behavior. In other words, we can say that a Car
is a type of Vehicle
.
Let's see how this will look as code; we will first create a base class named Vehicle
. The class has a single constructor, which accepts a String
(the vehicle name):
public class Vehicle { private Stringname; public Vehicle(Stringname) { this.name=name; } }
Now we can create a Car
class with a constructor. The Car
class is derived from the Vehicle
class, so it inherits and can access all the members and methods declared as protected or public in the base class:
public class Car extends Vehicle { public Car(String name) { super(name) } }
Polymorphism
In broad terms, polymorphism gives us an option to use the same interface for entities of different types. There are two major types of polymorphism, compile time and runtime. Say you have a Shape
class that has two area methods. One returns the area of a circle and it accepts single integer; that is, the radius is input and it returns the area. Another method calculates the area of a rectangle and takes two inputs, length and breadth. The compiler can decide, based on the number of arguments in the call, which area method is to be called. This is the compile-time type of polymorphism.
There is a group of techies who consider only runtime polymorphism as real polymorphism. Runtime polymorphism, also sometimes known as subtyping polymorphism, comes into play when a subclass inherits a superclass and overrides its methods. In this case, the compiler cannot decide whether the subclass implementation or superclass implementation will be finally executed, and hence a decision is taken at runtime.
To elaborate, let's take our previous example and add a new method to the vehicle type to print the type and name of the object:
public String toString() { return "Vehicle:"+name; }
We override the same method in the derived Car
class:
public String toString() { return "Car:"+name; }
Now we can see subtyping polymorphism in action. We create one Vehicle
object and one Car
object. We assign each object to a Vehicle
variable type because a Car
is also a Vehicle
. Then we invoke the toString
method for each of the objects. For vehicle1
, which is an instance of the Vehicle
class, it will invoke the Vehicle.toString()
class. For vehicle2
, which is an instance of the Car
class, the toString
method of the Car
class will be invoked:
Vehicle vehicle1 = new Vehicle("A Vehicle"); Vehicle vehicle2 = new Car("A Car") System.out.println(vehicle1.toString()); System.out.println(vehicle2.toString());