Object-oriented Programming: A Simplified explanation

Introduction

why learn oop in java

Are you familiar with Java but want to write code that is reusable and maintainable? Then this post is for you. It describes in simplified language how to use the object-oriented programming model to write code that's secure, scalable and easier to troubleshoot. We will start by exploring what object orientation means and show how it is embedded into the Java language from the first code file you create. Then we will discover how classes, instances and constructors embody the idea of representing objects in code. Okay, ready? Let's dive right in.

What is object orientation

Everything we see, hear and experience in everyday life can be represented in code. It can be represented in a variety of different ways, depending on how the program is designed. Object-oriented programming is a programming model that can help make your code more flexible, reusable and easier to maintain. It is based on writing code that is modeled around objects and data. For example, if we wanted to represent a car in code we could give it a height, diameter, and anything else we wanted to know about the car. These would be attributes or data points of the car. We could also give the car a behavior, the ability to run and this could change the value of some of its attributes. If these attributes and behaviors about the car were contained in a single unit, we could say that it is organized around the car object rather than a specific action or behavior. That's what makes this object-oriented.

Object-oriented programming comes with its own set of four pillars that define object-oriented design;

  • Abstraction

  • Inheritance

  • Encapsulation

  • Polymorphism.

We will explore each of these principles in this article.

What is encapsulation?

Encapsulation is one of the forming principles in object-oriented programming. It refers to the idea of binding an object's state and behavior together into one unit. In other words, encapsulation is about wrapping data and code acting on that data together. One benefit of encapsulation is that we can prevent classes from being tightly coupled. This means we can easily modify one class, either in the data it contains or its behaviors, without affecting the rest of the program. However, this can only happen if we have a clear interface between the class and the rest of the program. Everything can't have direct access. We will need to restrict some of the components of a given class. To make our program more robust to changes, we can make the class's attributes hidden from other classes using encapsulation. The data would still be accessible but only indirectly through the methods of the class. This would allow us to create specific pathways for the classes to communicate without making them heavily dependent on each other. By preventing other classes from having direct access to certain attributes, our programs become more secure and less error-prone because when a given change has to be made, it will result in less code change. Furthermore, with more restricted access, it's less likely that an attribute would be overwritten with an invalid value or null unexpectedly.

Access Modifiers

In Java, we can achieve encapsulation by using access modifiers. Different access modifiers determine where certain variables and methods can be accessed in your code. In other words, they allow you to restrict the scope of specific functionality in your program. The three different access modifiers in Java are private, protected, and public. We use these keywords on pieces of a given class to give it a certain access or visibility level. Private variables and methods are only visible in the class that they live in. Protected variables and methods are visible to the package and all subclasses. Public members are accessible everywhere within the program. If no modifiers are provided, it's only visible to the package it lives in. In the main class, the main method has the public access modifier. This allows it to be invoked by the JVM or Java Virtual Machine to execute a program. Most of the time, each attribute and behavior of the class will have an access modifier that's either private or public. If we want other classes to still have access to this attribute data, we'll need to create an indirect way for them to access it. This will allow us to achieve true encapsulation in our programs.

Implementing encapsulation with access modifiers

With encapsulation, we want to make data hidden from other classes so they cannot use it directly. However, we still want it to be accessed indirectly through a clear pathway. in Java, one way we can achieve encapsulation for our attributes is to declare each attribute as private and then write public methods to get and set the value of each attribute. With this, other classes will still be able to access the hidden data, but they can only do so through a public method of a given class.

What is inheritance?

Another key object-oriented principle is Inheritance. Inheritance allows us to create class hierarchies where classes inherit properties and behaviors from other classes. With Inheritance, we have two main players, the subclass and the superclass. We call the class that inherits the properties the subclass or child class. The class that's being inherited from is called the superclass or the parent class. In Inheritance, the child class inherits properties from the parent class, or the subclass inherits properties from the superclass. For example, let's say we have an Employee class. The class has name, ID, and salary attributes. Now, what if we wanted to represent a salesperson in code? A Salesperson would also have a name, ID, and salary, but they might also have a commission percentage or other attributes and behaviors. While we could create a fresh new class for the salesperson with name, ID, and salary attributes, we could also use Inheritance to help reduce code in our program. Since a salesperson is an employee and will have all of the attributes and behaviors of an employee, we can have the Salesperson class inherit from the employee class. This allows us to avoid duplicating the code from the employee class in the salesperson class. Instead, we would just create a new class with the custom functionality of a salesperson, inheriting the employee functionality from the employee class.

This is beneficial because if there's ever something we want to add to the employee class, it will automatically affect our salesperson. After all, a salesperson is an employee. We call the relationship between the salesperson and the employee a relationship because the salesperson should have all the functionality that the employee has. However, the reverse is not true. Not all employees are salespersons. This is what makes the employee class the superclass and the salesperson class the subclass. Ultimately, inheritance allows for code reusability because we can write the common properties and functionality in one class and have other classes with unique features inherit from it. This also makes our code more scalable because we can write the common functionality once and then have whatever class needs the functionality inherit from it.

Leveraging different types of inheritance

Inheritance can be applied in many different ways, and it's important to choose the right form for your use case. With the employee-salesperson example, we were using single-level inheritance. The employee was the superclass and the salesperson was its only subclass. We can extend this inheritance example by adding additional types of employees. We could add an analyst class. Instead of receiving a commission on sales, an analyst receives bonuses each year depending on their performance. An analyst is also an employee, but an analyst is not a salesperson. This type of inheritance is called hierarchical inheritance, where one parent class has many subclasses, and this is also supported in Java. Another type of inheritance we have is multi-level inheritance. A class can inherit from one class, but also be the parent of another class. Let's say we add a person class to the mix an employee is a person, an analyst is a person, and a salesperson is a person. We can have the employee class inherit from the person class, and in turn, the analyst and salesperson will also inherit from the person class indirectly. Since the name attribute is more of a person attribute rather than an employee attribute, we can move that over to the person class. With this structure, attributes and behaviors dealing with a person live in the person class, while details associated with the employee live in the employee class. There are a few other types of inheritance, including multiple inheritance and hybrid inheritance, but they are not supported in Java.

Multiple inheritances can cause unnecessary complexity, especially in casting, constructor chaining, and other Java operations. There are very few scenarios where multiple or hybrid inheritances are needed, so it's been removed entirely for simplicity. Ultimately, in Java, a class can only have one superclass, but it can have multiple subclasses. If you want a class to have several superclasses, one option is what we've shown here with multi-level inheritance, where a given class inherits from multiple classes indirectly. Understanding the different forms of inheritance available to you in Java can help you design better systems that reduce code for your use case.

What is polymorphism?

Polymorphism is the ability of an object or function to take many forms. Depending on the context and situation, the form may be different, making your code more flexible and reducing complexity. The idea that an object can use functionality from different classes depending on the context is a key idea that's essential to understanding polymorphism. Java supports two types of polymorphism, runtime polymorphism and compile-time polymorphism. Polymorphism will not only help us reduce complexity in our Java programs, making them easier to understand, but it'll also help us write more reusable code.

Runtime polymorphism

Let's explore how to implement polymorphism in Java. We will create an OddArrayList that will only contain odd numbers. We will want it to have all of the functionality of an ArrayList. So we will extends ArrayList. We also know it will only contain odd whole numbers, so we can have it extended with the integer data type. To make it so that only odd numbers can be added to the list, we'll need to override some of the ArrayList functionality. Methods like the constructor, add, addAll, set, and replaceAll will need to be modified so that only odd numbers are added. To override the function implementations, we'll need to match the method signatures from the ArrayList exactly. We can still use the original functionality with the keyword super to access the original implementation. Let's take a look at how this works. To override the add method, we'll use the @Override annotation. Then we will write out the signature for the add method. This comes from the ArrayList class. From here, we can implement the modified add functionality. To start, we will check to see if the item is odd, using the mod operator %.

If the absolute value of the element has a remainder one when divided by two, then the item must be odd and can be added to our list. The actual implementation of adding an item already lives in the ArrayList class. So we will access its implementation with the super keyword and then add the element. We will continue to do this for all of the other methods. First, check if the item is odd and then leverage the already implemented functionality from the ArrayList class. Since the odd check functionality will be the same for all of these methods, we can separate it into its public static function. Then we'll use it in each function we're overriding. Next, we will override the addAll methods, which lets you add collections to your ArrayList. Next, we will modify the set functionality so that only valid elements can be inserted. If the item is not valid, we will return the minimum value for an integer. For the replaceAll method, we will run the operation and then immediately remove the invalid elements. The last method we need to create is the constructor. We will have it take in a series of numbers and this will become an array for us to use in our implementation. Then we will use the ArrayList constructor with the keyword super and then pass in the array with the odd numbers only. With inheritance, we were able to reuse the functionality of a given class. Then we added to it by creating new methods in the new class. With runtime polymorphism, we not only reused the functionality of a given class but we overrode it with new functionality as needed while leveraging the superclass's implementation.

Compile-time polymorphism

Java also supports compile-time polymorphism to make your code faster and cleaner. With compile-time polymorphism, we can have more than one method with the same name. So how does Java decide which method to use? It is based on the input type and the number of parameters used at compile time, hence compile-time polymorphism.

What is abstraction?

In software development, we use abstraction to hide implementation complexity. This complexity could be from an algorithm, API or design. And the goal of abstraction is to generalize the features of a given system. If a software product uses abstraction it should be able to provide a user with an example input, and output in a broad description of what the system does without going into the technical details. Consider someone making coffee with a pod coffee machine. The person knows they need to provide the machine with water and the specific coffee pod but they don't know exactly how the coffee is made. They don't need to know how the coffee machine works internally to brew a fresh cup of coffee. Someone else created the pod coffee machine to hide all the details of exactly how the coffee is made. If you want to make a cup of coffee, you just interact with the simple interface, providing the inputs and you get your output, a cup of coffee. You can do all of this without having any knowledge of the coffee machine's internal implementation. Looking at abstraction and Java specifically, we have abstract classes and interfaces and we will be looking at these in more detail in another article.

When systems are abstract, it's easier for engineers to add code contributions because they don't have to know every single detail of each system they're working with. They just need to know the inputs, the outputs and a general idea of what the system does. This allows engineers to focus on what features they want to add to the application without getting bogged down in the details.

conclusion

I hope the knowledge you've gained from this article comes in handy whenever you're building Java programs.