There are 4 main concepts of OOPs. Acronym EAIP represents Encapsulation, Abstraction, Inheritance, and Polymorphism.

  1. Encapsulation: Encapsulation is the concept of bundling data (attributes) and methods (functions) that operate on the data into a single unit or class. It restricts direct access to some of the object’s components, which is a way of protecting the internal state of the object. Instead of accessing data directly, encapsulation encourages using methods, known as “getters” and “setters,” to retrieve or modify the data.

    Information Hiding - Improves Security. Restricts direct access to some of the Object’s data. In Java hiding information is achieved through "Access Modifiers" like public, protected, default, and private that are used to restrict access to certain data

  2. Abstraction: A class in any OOP language is an example of Abstraction. Abstraction is the process of hiding the complex implementation details of a function or an object and showing only the essential features to the user. It allows you to work with higher-level interfaces without needing to know all of the intricate details of how they work internally.

    Implementation Hiding. Example: class, Code written in Declarative Programming Style

  3. Inheritance: Inheritance is a mechanism where one class (child or subclass) inherits attributes and behaviors (methods) from another class (parent or superclass). This promotes code reusability and establishes a relationship between classes. Inheritance allows a subclass to acquire properties and methods from the parent class while also adding its own unique features or overriding inherited behaviors.

    IS-A Relationship, Code Reusability

  4. Polymorphism: Polymorphism allows objects to be treated as instances of their parent class, even if they have unique implementations. This concept enables the same method to perform different behaviors based on the object that is calling it. Polymorphism can be implemented through method overriding (same method name, different behavior in derived class) or method overloading (same method name, different parameters in the same class, depending on the language).

    Compile Time Polymorphism( Method Overloading aka static binding doesn’t require inheritance)

    • The method println() is an overloaded method in java.io.PrintStream

    •  // Overloaded methods of println()
       public void println(Object x);
       public void println(String x);
       public void println(char x);
       public void println(boolean x);
       public void println(int x);
       public void println(long x);
       public void println(float x);
       public void println(double x);
      

      Runtime polymorphism(Method Overriding aka dynamic polymorphism requires inheritance)

    • The clearest way to express polymorphism is through abstract classes or interfaces.
    • Reference StackOverflow answer
    • Example: A slightly modified version of the example from the above citation

      • abstract class Vehicle {
          String name;
        
          Vehicle(String name) { // You can't directly instantiate an abstract class, but this is called from subTypes
            this.name = name;
          }
        
          void printVehicleInfo() { // concrete method
            System.out.println("Vehicle name " + name + " and it has got " + getWheels() + " wheels");
          }
        
          abstract int getWheels(); // abstract method
        }
        
        class Bicycle extends Vehicle {
        
          Bicycle(String name) {
            super("Bicycle");
          }
        
          @Override
          int getWheels() { // Overridden method
            return 2;
          }
        }
        
        class Car extends Vehicle {
        
          Car(String name) {
            super("Car");
          }
        
          @Override
          int getWheels() { // Overridden method
            return 4;
          }
        }
        
        class Truck extends Vehicle {
        
          Truck(String name) {
            super("Truck");
          }
        
          @Override
          int getWheels() { // Overridden method
            return 18;
          }
        }
        
        public class QuickCheck {
        
          // This method is an example of polymorphic code
          static void printVehicleInfo(Vehicle vehicle) { // Program to abstractions
            vehicle.printVehicleInfo(); // its subtype is resolved at runtime
          }
        
          public static void main(String[] args) {
        
            List<Vehicle> vehicles = new ArrayList<>(); // Polymorphic List
            vehicles.add(new Bicycle("bicycle"));
            vehicles.add(new Car("Car"));
            vehicles.add(new Truck("Car"));
        
            vehicles.stream().forEach(vehicle -> 
            // Polymorphic call
            printVehicleInfo(vehicle));
          }
        }
        
      • In the above example QuickCheck.printVehicleInfo(vehicle) is polymorphic code, because when you add a new sub type to Vehicle, no need to change this method, because it is written in polymorphic way i.e., programmed to abstractions(meaning reference type is set as abstract class or interfaces).
      • Also, the subtype of vehicle is resolved dynamically/runtime and appropriate printVehicleInfo() method is called. Hence it is known as dynamic/runtime polymorphism.
  • Here is an example covering all the 4 OOPs concepts
    • OOPS Sample

References:

  1. Linkedin learning course
  2. Programming with Mosh
  3. Poor Encapsulation example
  4. Encapsulation and Abstraction
  5. Abstraction is achieved through interfaces and abstract classes
  6. An code example talking about Encapsulation and Abstraction in one go
  7. Reference from SO answer
  8. Polymorphism reference from SO answer