Introduction to Object-Oriented Programming in Java
E.P.I.C concept (Encapsulation, Polymorphism, Inheritance, Composition)
Table of contents
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
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.
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.
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.
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 ofsetters
andgetters
methods we create in ourEmployee
class. We should use aprivate
access modifier or completely delete a method in theEmployee
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 ofprivate
access modifier for oursetter
andgetter
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 oneabstract
method (that is usually implemented by any subclass that wants to make use of that method). AnAbstract
Class can have more than oneabstract
method and it makes use of the keywordextends.
The code snippet below shows anabstract
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 onlyextend
it. But with afinal
class, we cannotextend
it. We usefinal
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 theString
class in Java.Strings
in Java are immutable but can be modified usinguppercase
andlowercase
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 likePIE
,EXPONENTIAL
that have constant values that are unchangeable. E.gPIE
= 3.142
. In Java, we make use of the final class/methods for the following reasons: Final
variables are used to create constant variablesFinal
methods are used to prevent method overridingFinal
classes are used to prevent Inheritance
For multiple Inheritance in Java, we make use ofInterfaces
. 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.
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
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: