Object Oriented Programming in Java

Object Oriented Programming in Java

Part 3

The Pillars of Object-Oriented Programming

Object-oriented programming is built on four fundamental principles: encapsulation, inheritance, polymorphism, and abstraction. Each of these principles plays a crucial role in designing modular, reusable, and maintainable software systems.

  1. Encapsulation: Encapsulation involves bundling data (attributes) and methods (functions) that operate on the data into a single unit, known as a class. This unit acts as a protective barrier, preventing outside interference and misuse of data, while allowing controlled access through well-defined interfaces.

  2. Inheritance: Inheritance enables a class (subclass) to inherit properties and behaviors from another class (superclass). This promotes code reuse, extensibility, and hierarchical organization of classes, forming the basis of a powerful software design paradigm.

  3. Polymorphism: Polymorphism allows objects of different classes to be treated as objects of a common superclass. This flexibility enables dynamic method invocation, where the method to be invoked is determined at runtime based on the type of object, fostering code flexibility and scalability.

  4. Abstraction: Abstraction involves modeling real-world entities as simplified representations within a software system. It hides the complex implementation details of objects, focusing on essential characteristics and behaviors relevant to the problem domain. Abstraction promotes code maintainability, scalability, and understanding.

Now that we've refreshed our understanding of these core OOP principles, let's delve deeper into the concept of inheritance and its significance in Java programming.

Inheritance:

To inherit a class, you simply incorporate the definition of one class into another by using the extends keyword.

class subclass-name extends superclass-name 
    { // body of class }

// Example
class Box extends BoxTypes{

    }

Note:

  • You can only specify one superclass for any subclass that you create.

  • Java does not support the inheritance of multiple superclasses into a single subclass.

  • No class can be a superclass of itself.

class A extends A {
    // Class A tries to inherit from itself, which is not allowed
    // This will result in a compilation error: cyclic inheritance involving A
}
// This you can't do
  • Although a subclass includes all of the members of its superclass, it cannot access those members of the superclass that have been declared as private.

Superclass variable can reference a subclass object:

  • It is important to understand that it is the type of the reference variable not the type of the object that it refers to that determines what members can be accessed.

  • When a reference to a subclass object is assigned to a superclass reference variable, you will have access only to those parts of the object defined by the superclass.

// Superclass
public class Box {
    double l;
    double h;
    double w;

    Box(double l, double h, double w) {
        super(); // It is not giving any error because
//        Class Object is the root of the class hierarchy.
//        Every class has Object as a superclass.
//        All objects, including arrays, implement the methods of this class.

        this.l = l;
        this.h = h;
        this.w = w;
    }

    Box() {
        this.l = -1;
        this.h = -1;
        this.w = -1;
    }

    Box(double side) {
        this.l = side;
        this.h = side;
        this.w = side;
    }

    Box(Box old) {
        this.l = old.l;
        this.h = old.h;
        this.w = old.w;
    }

    public void information(){
        System.out.println("Running the box");
    }
}
// SubClass

public class BoxWeight extends Box{
    double weight;

    public BoxWeight(double l, double h, double w, double weight) {
        super(l, h, w); // Calling the constructor
        this.weight = weight;
    }
    BoxWeight() {
        this.weight = -1;
    }

    BoxWeight(BoxWeight other) {
        super(other);
        weight = other.weight;
    }
    BoxWeight(double side, double weight) {
        super(side);
        this.weight = weight;
    }
}
// Main Class

public class Main {
    public static void main(String[] args) {

//        In Java, when you create an instance of a subclass using a superclass reference variable, only the members and methods of the
//        superclass are directly accessible through that reference. This is because the reference variable is of the superclass type,
//        so the compiler only allows access to the members and methods defined in the superclass.
//        Box box6 =  new BoxWeight(1,2,3,4);
//        System.out.println(box6.weight); // not working

//        there are many variables in both parent and child class
//        You are given access to variables that are in ref var type i.e. BoxWeight
//        Hence you should have access to box variable
//        this also means that the ones you are trying to access should also be initialsed
//        but here when the object itself is of type parent class, how will you call the constructor
//        hence that is why this is the error
        BoxWeight box7 =  new Box(2,3,4);
//        System.out.println(box7);

    }
}

Super:

Whenever a subclass needs to refer to its immediate superclass, it can do so by use of the keyword super. super has two general forms. The first calls the superclass’ constructor. The second is used to access a member of the superclass that has been hidden by a member of a subclass.

BoxWeight(double w, double h, double d, double m) {
     super(w, h, d); // call superclass constructor weight = m; 
}
  • Here, BoxWeight( ) calls super( ) with the arguments w, h, and d. This causes the Box constructor to be called, which initializes width, height, and depth using these values. BoxWeight no longer initializes these values itself.

  • It only needs to initialize the value unique to it: weight. This leaves Box free to make these values private if desired.

  • Thus, super( ) always refers to the superclass immediately above the calling class. This is true even in a multileveled hierarchy.

public class Box {
  private double width;
  private double height;
  private double depth;

  public Box(double width, double height, double depth) {
    this.width = width;
    this.height = height;
    this.depth = depth;
  }

  public Box(Box other) {
    this.width = other.width;
    this.height = other.height;
    this.depth = other.depth;
  }

  // Other methods for the Box class (e.g., to calculate volume)
}

public class BoxWeight extends Box {
  private double weight;

  public BoxWeight(double width, double height, double depth, double weight) {
    super(width, height, depth);  // Call the parent class constructor
    this.weight = weight;
  }

  public BoxWeight(BoxWeight other) {
    super(other);  // Call the parent class copy constructor
    this.weight = other.weight;
  }
}

Notice that super() is passed an object of type BoxWeight not of type Box. This still invokes the constructor Box(Box ob).

NOTE: A superclass variable can be used to reference any object derived from that class. Thus, we are able to pass a BoxWeight object to the Box constructor.Of course,Box only has knowledge of its own members.

  1. Accessing Superclass Members (super.member):

    • Used within a subclass to access a member (method or variable) of the superclass.

    • Useful when a subclass has a member with the same name as a member in the superclass (avoids ambiguity).

    • Example: super.width could be used in BoxWeight to access the inherited width from Box.

    • Superclass might not be aware of additional members introduced in subclasses.

Additional Notes:

  • If a subclass constructor doesn't explicitly call super(), the Java compiler automatically inserts a call to the no-argument constructor of the superclass (if it exists).

  • Not using super() can lead to unexpected behavior if the superclass constructor performs crucial initialization steps.

Using final with Inheritance:

  • Declaring Constants: The final keyword can be used to create constants, which are variables whose values cannot be changed after initialization. This ensures consistency in your code.

  •   public final class MathUtils {
        public static final double PI = 3.14159;
    
        public static double calculateArea(double radius) {
          return PI * radius * radius;
        }
      }
      // In this example, the PI variable is declared as final within the 
      // MathUtils class. This ensures that the value of pi (3.14159) 
      // cannot be changed after initialization, promoting consistency in 
      // calculations.
    
  • Preventing Method Overriding (final methods):

    • By declaring a method as final in the superclass, you prevent subclasses from overriding it.

    • This can be useful for core functionalities that should not be altered in subclasses.

    • final methods can sometimes offer a performance benefit:

      • The compiler might inline the final method's bytecode directly into the calling method, eliminating the overhead of a method call.

      • This "inlining" is possible because the compiler knows the method cannot be overridden.

    • Final methods are resolved at compile time (early binding) due to the absence of overriding, while non-final methods are resolved at runtime (late binding).

    •   public class Animal {
          public final void makeSound() {
            System.out.println("Generic animal sound");
          }
        }
      
        public class Dog extends Animal {
          // Cannot override makeSound() because it's final in the superclass
          // public void makeSound() { // This would cause a compile-time error
          //   System.out.println("Woof!");
          // }
        }
        // Here, the makeSound() method in the Animal class is declared as 
        // final. This prevents the Dog class (or any other subclass) from 
        // overriding this method. The Dog class cannot define its own 
        // makeSound() behavior.
      
  • Preventing Class Inheritance (final classes):

    • Use the final keyword before the class declaration to prevent inheritance from that class.

    • This might be appropriate for utility classes or classes representing a fundamental concept that shouldn't have subclasses.

    • Declaring a class as final implicitly declares all its methods as final as well.

    • A class cannot be both abstract (incomplete and relies on subclasses) and final.

    •   public final class StringUtils {
          public static String toUpperCase(String str) {
            return str.toUpperCase();
          }
        }
        // The StringUtils class is declared as final. This means no other 
        // class can inherit from StringUtils and create subclasses. 
        // This might be appropriate if StringUtils only contains static 
        // utility methods for string manipulation and you don't want any 
        // subclasses with potentially modified behavior.
      

Additional Notes:

  • While static methods can be inherited, overriding them in subclasses doesn't make sense because the parent class method will always be called regardless of the object used.

  • Static interface methods cannot be overridden as they have implementations defined in the interface. They must have a body.

  • Polymorphism (ability of objects of different classes to respond to the same method call) does not apply to instance variables in inheritance.

Polymorphism

Polymorphism is a fundamental concept in object-oriented programming that allows objects of different classes to respond to the same method call in different ways. Here's a breakdown of its key aspects with examples:

  1. Method Overloading:
  • Definition: In Java, you can define multiple methods within the same class with the same name, as long as their parameter lists (number, types, or order) differ.

  • Example:

class OverloadDemo {
  void test(double a) {
    System.out.println("Inside test(double) a: " + a);
  }

  void test(int i) {  // Overloaded method with different parameter type
    System.out.println("Inside test(int) i: " + i);
  }
}

class Overload {
  public static void main(String args[]) {
    OverloadDemo ob = new OverloadDemo();
    int i = 88;
    ob.test(i);        // Calls test(int) based on parameter type
    ob.test(123.2);    // Calls test(double)
  }
}

In this example, the OverloadDemo class has two test methods with different parameter types (int and double). When ob.test(i) is called, Java chooses the appropriate overloaded method (test(int)) based on the argument type (int).

2. Returning Objects:

  • Concept: Methods can return objects. When a method creates a new object and returns it, the calling code receives a reference to that object.

  • Example:

class Test {
  int a;
  Test(int i) {
    a = i;
  }

  Test incrByTen() {
    Test temp = new Test(a + 10);  // Create a new Test object
    return temp;                     // Return a reference to the new object
  }
}

class RetOb {
  public static void main(String args[]) {
    Test ob1 = new Test(2);
    Test ob2;
    ob2 = ob1.incrByTen();
    System.out.println("ob1.a: " + ob1.a);  // Output: 2 (original object)
    System.out.println("ob2.a: " + ob2.a);  // Output: 12 (new object created by incrByTen)
  }
}

Here, the incrByTen method in the Test class creates a new Test object with an incremented value and returns a reference to it. This allows the calling code (ob2) to access the modified value.

3. Method Overriding:

  • Concept: In inheritance, when a subclass defines a method with the same name and signature (return type and parameter list) as a method in its superclass, it's called method overriding. The subclass method's implementation replaces the superclass method's implementation for objects of the subclass.

  • Example: (Using the Box and BoxWeight classes from previous discussions)

class Box {
  double width;
  double height;
  double depth;

  public Box(double width, double height, double depth) {
    this.width = width;
    this.height = height;
    this.depth = depth;
  }

  public void displayVolume() {
    System.out.println("Volume: " + width * height * depth);
  }
}

class BoxWeight extends Box { // Subclass inherits from Box
  double weight;

  public BoxWeight(double width, double height, double depth, double weight) {
    super(width, height, depth);  // Call superclass constructor
    this.weight = weight;
  }

  @Override  // Override displayVolume to include weight
  public void displayVolume() {
    super.displayVolume();       // Call superclass displayVolume
    System.out.println("Weight: " + weight);
  }
}

In this example, the BoxWeight class overrides the displayVolume method from the Box class. When you call displayVolume on a BoxWeight object, the overridden version in BoxWeight executes, printing both volume and weight.

4. Dynamic Method Dispatch:

  • Concept: This is the mechanism by which Java determines which version of an overridden method to call at runtime. When you call a method through a reference variable of a superclass that can refer to objects.

Abstraction:

  • Abstraction focuses on the essential functionalities that a user or another part of the code needs to interact with. It hides the underlying implementation details.

  • Example:

public interface Shape {
  double calculateArea();  // Abstract method - defines functionality without implementation
}

class Square implements Shape {
  private double side;

  public Square(double side) {
    this.side = side;
  }

  @Override
  public double calculateArea() {
    return side * side;
  }
}

In this example, the Shape interface represents the abstraction. It defines a method calculateArea() that specifies the functionality without providing the implementation details. The Square class implements the Shape interface and provides the concrete implementation for calculateArea().

Encapsulation:

  • Encapsulation binds data (attributes) and methods that operate on that data together within a single unit (class).

  • It protects the data by controlling access using modifiers (public, private, protected).

  • Example:

public class Account {
  private double balance;  // Encapsulated data (private)

  public void deposit(double amount) {
    balance += amount;
  }

  public double getBalance() {
    return balance;
  }

The Account class demonstrates encapsulation. The balance attribute is private, restricting direct access from outside the class. The class provides public methods (deposit and getBalance) to interact with the data in a controlled manner.