Introduction to Object-Oriented Programming in Java

Introduction to Object-Oriented Programming in Java

E.P.I.C concept (Encapsulation, Polymorphism, Inheritance, Composition)

Table of contents

No heading

No headings in the article.

So just because we use classes and objects in our program doesn't mean we are doing OOP. Classes and objects are equally used in other programming paradigms, which include:

(1) Procedural Programming: Here, we simply create classes and methods. We also fill up our classes with so many dependent codes. Any change made to one code might affect another code which would disrupt the entire system. The procedural style of programming does not support code re-usability. We can see a good example in the code snippet below:

public class Account {
     public static void main (String [] args)
 {          int baseSalary = 50_000;
            int hourlySalary = 100_000;
            int monthlySalary = 200_000;

       int wage = calculateSalary (baseSalary, hourlySalary, monthlySalary);
           System.out.println(wage);
 }
  public static int calculateSalary (int baseSalary, int hourlySalary, int monthlySalary) {
         return baseSalary + (hourlySalary * monthlySalary);
      }
}

The above code snippet shows a procedural programming approach that displays some methods fused like spaghetti which are more error-prone. In our next example, we would consider a more efficient style of programming called the Object-Oriented Programming (OOP) paradigm.

(2) Object-Oriented Programming: In this article, our focus would be on the Object-Oriented Programming approach. The OOP has four main types, namely:

  • Encapsulation.
  • Polymorphism.
  • Inheritance.
  • Composition.

(1) Encapsulation: This simply means that we should bundle the data and methods that operate on the data inside a unit. To do this, we would change our previous code by introducing a new class called Employee that contains all the attributes of the Employee class and some new methods that can access them whenever we declare them using 'private' access modifiers. Using a private access modifier is good software engineering practice, as it enhances the principle known as information hiding. This helps to condition any changes to be made on Employee class, which goes through a method validation using what we refer to as setter and getter methods. These methods help to design fault-tolerant systems by validating every data being passed into the system as we have below:

public class Employee{
   private int baseSalary;
   private int hourlySalary;
   private int monthlySalary;

public int calculateWage(){
    return baseSalary + (hourlySalary * monthlySalary);

     }
}

Notice that we didn't have to hardcode the values of our instance variables ( baseSalary, hourlySalary, monthlySalary) as we did in our first code. And we didn't have to pass parameters to our methods as we did earlier. This is the beauty of OOP! Next, we will introduce our setters and getters methods below:

public class Employee{
   private int baseSalary;
   private int hourlySalary;
   private int monthlySalary;


public static void setBaseSalary(int baseSalary){
    if (baseSalary < 0)
       throw new IllegalArguementException("Base Salary cannot be blessed than zero");
              this.baseSalary = baseSalary;
}

public int calculateWage(){
    return baseSalary + (hourlySalary * monthlySalary);
    }
}

From the above code snippet, we introduced a setter's method that validates our data and throws an exception if the condition is not met using the throw keyword. The IllegalArguementException is from the Exception class in Java. Exception handling is a major topic in Java and many other programming languages, this concept makes our program to be fault-tolerant and ensures that our program terminates gracefully with a soft landing when an exception is encountered. We make use of try, catch, and throw keywords when dealing with Exception handling. The above code snippet helps to prevent our employee object from going into an invalid state whenever a negative value is passed which is less than zero.
Next, we will be introducing a get method that returns the values we passed to the baseSalary set method. This is achieved when a call is made by the employee object in our main class.

public class Employee{
   private int baseSalary;
   private int hourlySalary;
   private int monthlySalary;

public static void setBaseSalary(int baseSalary){
    if (baseSalary < 0)
       throw new IllegalArguementException("Base Salary cannot be blessed than zero");
              this.baseSalary = baseSalary;
}
public int calculateWage(){
    return baseSalary + (hourlySalary * monthlySalary);
}

public static int getBaseSalary(){

        return baseSalary;
}

}

In Java, we make use of setters and getters methods to set and get the values of a field. We also declare our fields with private access modifier.

Short-cut tips: We can auto-generate these setter and getter methods using an Intelli J IDE by hovering over the private instance variables we intend to generate and right-click on the setters/getters generation option from the IDE.
Next, we do the same thing for our hourlySalary and monthlySalary instance variables using setter and getter methods. This concept of information hiding is all there is about encapsulation! Let us now consider another concept in OOP, called Abstraction.

(2) Abstraction: This concept helps to reduce complexity by hiding out unnecessary details. A good analogy is a remote control for our Tv set. This remote control hides all the implementation details and workings behind the scene and only provides the user with an interface for :

  • selecting volume increase/decrease.
  • choice of program or channels.
  • power on/off e.t.c

remoteImages.jpg

Here, the user does not need to bother about "how" they carried the implementation out. All of this is abstracted from the user.
For instance, the image above shows the complexity inside a Tv remote control like an electronic board, a bunch of transistors, and so on. But the users don't directly work with these transistors, all the user wants to do is to change the channel of the TV and they do not care what happens under the hood in getting this done. So, with Abstraction, we just want to hide the implementation details of a class with the use of an Interface image. This way, the user only gets to interact with the interface and is not bored with complex details that might affect user experience (UX).
Remember that, excellent software design is always user-centered and user-focused. This is because the software design should address and solve the problem of the user and should allow for changes on the go since the user's needs are always dynamic. Making a software project to be engineer-focused only usually results in a low outcome.

remotecontrolTransitorBoard (Custom) (1).jfif

This brings us to another concept in OOP known as coupling and de-coupling.

  • Coupling: This simply refers to how much a class is dependent or coupled to another class. E.g when you make use of your phone often, that means you extremely depend on it and if they took away your phone from you, you're going to feel bad.
    Note that, there is no such thing as zero coupling! There is always going to be coupling because all these classes need to interact together to get things done, just the same way you will always need your phone once in a while but shouldn't make it an addiction else you might become less productive as a person...All we can attempt to do is to reduce the dependency amongst these classes.

single_inheritance-Class A depends on class B.png

From the above diagram, we can observe that class B depends on Class A, i.e. Class B uses some methods and attributes of Class A. The arrow above clearly shows that Class A is giving Class B some methods from the arrow direction! This is a form of Single-Inheritance which we are going to look at later in this article. If you make any changes in Class B, then Class A would have to be rectified likewise.
Imagine, we have three or more classes that are dependent on Class A (multi-level Inheritance) as we have in the image below.

multilevel inheritance-class B and C - class A.png

This becomes a big issue in a complex application when we have thousands of classes because any changes made to one class might result in thousands of broken classes. This is the problem with coupling, the more our classes are coupled with each other, the more costly our changes are going to be. But when we reduce the coupling, we reduce the impact of changes in our systems.

public class Account {
     public static void main (String [] args)
 {    Employee employee = new Employee();
       employee.getBaseSalary();      
       employee.getHourlySalary();
       employee.getMonthlySalary();

       int wage = employee.calculateSalary (baseSalary, hourlySalary, monthlySalary);
           System.out.println(wage);
 }

From the code snippet above, we can observe several coupling points in our main class like:

  • Employee employee = new Employee();
  • employee.getBaseSalary()
  • employee.getHourlySalary()
  • employee.getMonthlySalary()
  • employee.calculateSalary()
    Note that, a good software engineering practice is to reduce the number of setters and getters methods we create in our Employee class. We should use a private access modifier or completely delete a method in the Employee class we do not make use of.
    Note that, the more methods that a class has, the greater the number of coupling points in that class. E.g. the more apps we have installed on our phone, the more we are attached or coupled to that phone. We are tempted to visit Facebook, WhatsApp, Instagram, Snapchat, Facetime, Twitter, LinkedIn, HouseParty, Telegram, and WeChat. But I do not need these applications! For me, I am fine with just Twitter, LinkedIn, and Instagram, i.e. I am less coupled with my phone than someone else who has all the apps installed! Remember, our goal is to reduce dependency!
    A better approach in reducing the coupling effect is the use of private access modifier for our setter and getter methods, as shown in the code snippet below:
private static int getBaseSalary(){

        return baseSalary;
}
private static int getHourlySalary(){

        return hourlySalary;
}
private static int getMonthlySalary(){

        return monthlySalary;
}

From the above code snippet, we have successfully hidden the unnecessary implementation details from our users. This is abstraction in action! Here, the number of methods that are exposed outside of the Employee class is less, reducing complexity. We only expose those methods that are needed by the user. Let us now consider another concept in OOP known as Inheritance.

(3)Inheritance: We can simplify the concept of Inheritance in Java using a Parent to Child example. We refer to this type of relationship as is-a relationship, which connotes the Inheritance concept. The word extends is a keyword in Java, which means inherits from. E.g. a Class Animal is referred to as a Superclass in Java or BaseClass in C-based languages. This Animal Superclass would have other child classes that extends from it. We refer to them as subclasses in Java or derived classes in C-languages. The subclass is-a child of the parent (superclass).
Just like we have in real life, where a child usually inherits the genetic makeup of their parents and displays these features through behaviors, characters, looks, and the likes.
This same concept applies in Java because the subclasses derives all the attributes of their superclass and can override the superclass methods as well. Another keyword in Java is override, which means that the subclass has customized a superclass method to suit their implementation style.
Note that Java only supports Single Inheritance, while other languages like Python supports multiple inheritances. This was addressed in Java by introducing Interfaces. We would later consider this concept in this article.
Note that all classes in Java extend the Object superclass. This explains why every class has additional methods like toString(), equals(), household(), notify() and wait() methods.
-Constructors and Inheritance: In Java, we do not inherit constructors or static methods, the subclass calls the superclass constructor by declaring the super keyword inside its own constructor. We show this in the code snippet below:

public CommissionedEmployee extends Employee {
     private int bonus;
    public CommissionedEmployee() { 
        super (baseSalary,hourlySalary,monthlySalary);
           this.bonus = bonus;
   }
}

From the above code snippet, we can observe that the CommisionEmployee constructor has successfully inherited the same attributes of the Employee class by using a super keyword inside its constructor. Also, CommisionEmployee added its own instance variable/attributes like bonus to the constructor. Please note that private methods or fields are not inherited by subclasses i.e whenever you declare a superclass method with a private access modifier, it means the subclass would not have access to them. And they are not accessed outside of that class, instead, we use them to hide the implementation details of a class as we showed earlier under Abstraction. This way, we can change the implementation in the future without having to affect other classes. Other access modifiers include protected, but using them for this purpose is considered bad practice. With a protected access modifier, all classes inside the package have access to the fields/methods. It is almost similar to a public access modifier because it is public inside a package. This is also why we have to import classes whenever they are in a different package. But the use of protected for this purpose should generally be avoided! We can stick to public and private. We use public to expose anything that should be shown outside, and private for hiding.
-Method overriding: In Java, we can override a parent or grand-parent's class i.e direct superclass or indirect superclass using the override keyword.

public CommissionedEmployee extends Employee {
     private int bonus;
    public CommissionedEmployee() { 
        super (baseSalary,hourlySalary,monthlySalary);
           this.bonus = bonus;
   }
   @Override
   public String toString (){
      return  bonus;
}


}

NB: Avoid deep-Inheritance! Inheritance is a good concept but only up to one or two levels. Anything more than two or about three is in the wrong direction. Next, we are going to consider another concept in OOP known as Polymorphism.

(4) Polymorphism: The word Poly means many while morphism refers to forms. Therefore, Polymorphism means to have different forms. In Java, subclasses are given an opportunity to override the methods of their superclass to their own implementation style. Here, we achieve this concept using abstract classes and Interfaces. This is because we need a way to ensure that unrelated classes can still share a common ground or relationship through their parent or superclass. It is upon this singular concept that Java realized the need for Interfaces. Let us now consider the similarities and differences between the Abstract class and an Interface. Many software Engineers usually mistake the differences or similarities.

  • Abstract Classes and Abstract Methods: We make use of Abstract Classes in situations where we declare a class but we do not want to instantiate it i.e we do not want to create an object of that class. We simply do this by using the Abstract keyword in front of the class. An Abstract class has a minimum of one abstract method (that is usually implemented by any subclass that wants to make use of that method). An Abstract Class can have more than one abstract method and it makes use of the keyword extends. The code snippet below shows an abstract class declaration.
public abstract class MonthlyEmployee extends Employee {
private double monthlyPay;
private double monthlyprojects;
      public MonthlyEmployee(int baseSalary, int hourlySalary, int monthlySalary){
         super( baseSalary, hourlySalary,monthlySalary);
           this.monthlyPay = monthlyPay;
           this.monthlyProjects= monthlyProjects;
}

public abstract double earnings();

public abstract double makePayments();
}

From the code snippet above, we can observe that method earnings(); and makePayments(); are both declared using the abstract keyword because they do not have implementation details. Therefore, any subclass that extends this Abstract superclass must agree to implement its methods, otherwise be declared an Abstract subclass.
Any method that has implementation details is known as concrete methods, which is the opposite of abstract methods. The concrete methods are methods that are declared without an abstract keyword and have implementation details. Let us now consider some terms like final classes and methods which has a similar principle with abstract methods.

  • Final Class/Method: We already established that when we declare a class as abstract, we cannot instantiate it, we can only extend it. But with a final class, we cannot extend it. We use final classes in the case where we are sure of the implementation and we do not want any further changes to that class. An example of this is the String class in Java. Strings in Java are immutable but can be modified using uppercase and lowercase methods and can only create new String values when we change to uppercase or lowercase or change them.
    Also, final methods are constants i.e they cannot be changed or changed. Some examples are mathematical values like PIE, EXPONENTIAL that have constant values that are unchangeable. E.g PIE = 3.142
    . In Java, we make use of the final class/methods for the following reasons:
  • Final variables are used to create constant variables
  • Final methods are used to prevent method overriding
  • Final classes are used to prevent Inheritance
    For multiple Inheritance in Java, we make use of Interfaces. We would be considering our next and final topic in this article which is, Interfaces.

Interface: We use Interfaces to build extensible, loosely-coupled, and testable applications. The concept of abstraction attempts to achieve de-coupling by hiding out the implementation details and only exposing what is necessary, but they do not completely achieve that compared to the way Interface does. The Interface enables us to completely de-couple classes by creating a separate Interface.
An Interface is like a Class but differs because of the following reason:

  • it only includes method declaration, no implementation.
  • it has no code, it only defines the capabilities that a class should have.

interface image (Custom).jfif

The above image gives an Industrial application of the concept of Interface in the Automobile Manufacturing Industry. The Interface here is the Vehicle and other subclasses implement its methods as we would see in the code snippet below. To minimize the impact of changes we put an Interface between these classes to de-couple them.
Notice that, if we change the code in class Car, the Truck or Bus class would not be affected, and if we change the Truck, the Car and Bus would not be affected and vice-versa.

The use of Interface is to help us to determine what should be done while Classes helps us to determine how it should be done

Interface Segregation Principle: This principle states that we should divide a big Interface into smaller ones. An Interface should have a single capability and not mixed capabilities in order to enhance de-coupling. A good example is the code snippet below:

public interface Acceleration {
      public void accelerate;
      public void decelerate;
      public void halt;

}

From the above code snippet, we can observe that the public keyword is redundant because all Interface methods are public by default as being public is the only guaranteed way to ensure that subclasses get to implement their methods.
Also, a subclass can choose to implement the Acceleration Interface as we have below:

public class Car implements Acceleration{

@Override
public void accelerate {
system.out.println("can accelerate");
}

}

From the first code snippet of Acceleration Interface above, we observe that any subclass that implements the accelerate method and make changes to it would affect other methods like decelerate and halt, which are not directly linked to it. Therefore, we can see that the more methods we have in an Interface, the more likely it is for such an Interface to be tightly coupled together. The best practice is to create a separate Interface for this purpose. So, in this case, we should adhere to the Interface Segregation Principle which states that different Interfaces should have different capabilities and not one Interface doing multiple tasks.
Observe the code snippet below for more clarity:

public interface Deceleration {
      public void decelerate;
   }
public interface Halt{

   public void halt;

}

This makes our Interface become lightweight with fewer implementable methods (which aligns with the principle of abstraction as stated earlier). Hence, fewer methods in an Interface means that we have fewer coupling points.
Although there could be instances where we need to make use of all the methods of this Interface, then we can remodel our Interface segregation principle to suit the situation at hand.

Note that the point here is not to always segregate any Interface just because it has several methods. Rather, our focus should be on segregating methods and capabilities in an Interface, especially when they are dissimilar (have different methods). We show a better way to go about our Interface example above in the code snippet below:

public class Car implements Acceleration{

@Override
public void accelerate {
system.out.println("can accelerate");
}

}

Note that, we should try to avoid logic/algorithms in Interfaces, because logics deals with how and hows don't belong to Interfaces, they belong to Classes.
Interfaces are contracts! They should not have code and no implementations! No static methods, private methods, or fields! Just method declarations!
Although, many developers might hold a different opinion on this especially those inclined to the functional programming paradigm as they do not need to worry over hiding implementation details and strictly following OOP principles like tightly/loosely couple designs.
This is not to say that functional programming is bad, they both have different use cases and purposes.
Finally, we would conclude this OOP in Java series with a popular interview question: Differences between Abstract Classes and Interfaces.

  • Interfaces are contracts that enable us to build loosely-coupled, extensible, and testable applications while Abstract Classes are partially completed classes used to share codes.
  • Interfaces enable us to achieve Multiple Inheritance in Java as several classes can implement multiple Interfaces but Abstract classes can only be inherited once. But this should be used with caution.
    Other Programming paradigms in Java include Functional Programming, Declarative Programming, Imperative Programming, Event-Driven Programming, Generic Programming, Aspect-Oriented Programming, Structured Programming e.t.c. These Java paradigms are beyond the scope of this course.

References:

  • Mosh Hamedani.

Author bio Linkedin photo resized.jpeg

PETER AIDELOJE is a passionate Solution provider whose interest aligns towards the Software Architecture, DevOps/Cloud Computing, and Cyber Security field. He is currently having fun with Java Programming language (SpringBoot Framework), CSharp(ASP.Net framework) and React framework. Whenever Peter isn't coding, you find him writing/drafting an article about a topic in the Tech space. Peter enjoys engaging and sharing his knowledge with other developers via his social handles.

Kindly reach out to me via any of my socials via the links below: