Java is a general-purpose programming language. One of its core features that we encounter in our HelloWorld.java
program is classes
which suggests the support for Object-Oriented Programming (OOP) in Java.
class HelloWorld{
public static void main(String[] args){
HelloWorld h = new HelloWorld(); // instantiates HelloWorld class
h.print("hello world!");
}
public void print(String str){
System.out.println(str);
}
}
The above code decoupled the functionality that prints our string, hello world!
. Notice the need to instantiate our class HelloWorld
and use the .
(dot) operator to access our methods. The very moment you encounter a program in Java, the nuances of OOP are already waving at you! (Technically speaking, the main reason why there is a need to instantiate our class to access the print
method is the absence of the static
keyword, but let us set that aside for now.)
OOP is great for organizing concepts instead of leaving out pieces of code in the open, we can group them in a coherent entity. The main advantage we can leverage in this paradigm is code reusability and organization. Indeed, OOP seems more natural to us when we express certain ideas that insights the relationship of entities. OOP encourages us to think visually. Because of this, OOP logic can be presented in a Unified Modeling Language -- but this deserves a separate discussion on its own.
This article focus on getting you started with OOP in Java. We will discuss the first two core concepts of OOP:
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism
Encapsulation
OOP binds data and functions in one coherent object. In precise terms, we call functions inside object methods(or behaviors) and data as attributes. We can interact with this object based on what is permissible to us. Encapsulation is a concept that draws boundaries between what can be accessed and what remains hidden. It provides us the capability to hide implementation details that constitute the internal mechanism of our objects. In some cases, this is necessary to maintain an object's properties secured.
Java allows us to control the level of access for methods and attributes inside our object. It effectively provides program safety because we do not want changes in the external world to affect all the pieces of our code without any reason. For that, we have to set restrictions and define boundaries: we have to make it so that whenever we want to alter internal states, we express our intentions clearly. Consider the following snippet of code:
class Employee{
static String companyName = "xpb";
String email;
String name;
int age;
Employee(String fname, String lname, int age){
this.name = name + " " + lname;
this.age = age;
this.email = lname.toLowerCase() + "_" +
fname.toLowerCase()+ "@" +
companyName.toLowerCase()+".com";
}
public String getEmail(){
return this.email;
}
//.
//.
//.
}
Suppose we want to generate the company's email address per employee and have it formatted with @<companyName>.com
extension.
Because there is no restriction in terms of access to our key variables for email generation, we can openly modify emails.
public class Main{
public static void main(String[] args) {
Employee emp1 = new Employee("Ryan", "Bryan", 23);
Employee emp2 = new Employee("Kevin", "James", 33);
System.out.println(emp1.getEmail());
emp.companyName = "potato";
System.out.println(emp2.getEmail());
}
}
This will mess up all of the existing entities as lname_fname@potato.com
. Communication channels could be corrupted.
Instead, what we want is to have a restriction for crucial variables we don't want to be changed in our object model.
We simply append private
for these entities to prevent external events unreasonably alter the values of our object.
class Employee{
private static String companyName = "xpb";
private String email;
private String name;
private int age;
Employee(String fname, String lname, int age){
this.name = name + " " + lname;
this.age = age;
this.email = lname.toLowerCase() + "_" +
fname.toLowerCase()+ "@" +
companyName.toLowerCase()+".com";
}
public String getEmail(){
return this.email;
}
//.
//.
//.
}
Having an internal and external presence is crucial for defining an object. We want to make our model safe and secure. Therefore, we should craft our models in a way that maintains a coherent structure. One way to maintain this is by separating public
and private
entities. But Java extends our access control with our objects. Below is a summary of different levels of access in Java.
Modifier | Class | Package | Subclass | World |
---|---|---|---|---|
public |
+ |
+ |
+ |
+ |
protected |
+ |
+ |
+ |
- |
<default> |
+ |
+ |
- |
- |
private |
+ |
- |
- |
- |
Key terms:
- Object - is a user-defined data type created by the developer to capture a representation of an entity. It may include a composition of attributes, methods, and other objects.
- Class - is a construct that specifies a prototype of an object.
- Subclass - A class that inherits properties of another class (called a superclass).
- Package - is a construct that is used to group Java classes. It may be a folder in a file directory that consists of a collection of classes.
- Encapsulation - pertains to the process of hiding the implementation details of an object
Abstraction
One of the key benefits of OOP is the notion of generalization, also known as abstraction. Abstraction is the process of extracting common features that exist in a set of ideas, where we can group them and extend them however we like. There are two perspectives we can discuss abstraction in Java. First comes from the hierarchical design of class relationships, and second is from the perspective of an abstract class.
From a design perspective, abstraction can be achieved by extending an object's properties to capture the concept of another entity. Let's consider modeling the class of Fish, Bird, and Mammals. We know that these concepts are Animals, so we can define base class that contains all the common properties of an animal. To make it simpler, we know that all animals sleep
, eat
, and move
so we begin by defining these methods in our Animal class.
class Animal{
private String name = "Animal";
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public void eat(){
System.out.println(this.name + " is eating");
}
public void sleep(){
System.out.println(this.name + " is sleeping");
}
public void move(){
System.out.println(this.name + " is moving");
}
}
Now that we have defined a general structure that holds for the common features of an Animal, we can extend this to our classes as follows:
class Mammal extends Animal{
private String name;
Mammal(String name){
super.setName(name);
this.name = name;
}
}
class Bird extends Animal{
private String name;
Bird(String name){
super.setName(name);
this.name = name;
}
public void fly(){
System.out.println(this.name + " is flying.");
}
}
class Fish extends Animal{
private String name;
Fish(String name){
super.setName(name);
this.name = name;
}
public void swim(){
System.out.println(this.name + " is swimming.");
}
}
Notice that we have no restriction to instantiate the Animal class, we are only concerned with defining Fish
, Bird
, and Mammals
. Just as with our earlier program in Encapsulation, we should clearly express the intent otherwise our codebase will be error-prone. That said, we should only be able to instantiate the concepts relevant to our objective. In this situation, we can benefit from the notion of abstract classes in Java. Abstract classes effectively restrict direct instantiation of it i.e. if we set abstract class Animal
, we can no longer instantiate Animal
.
An abstract class is a class that can only interact with classes i.e. an abstract class cannot be instantiated, but it can be subclassed. We can set a default method in an abstract class or an abstract method that we ought to implement once we extend our abstract class to a concrete class. In other words, when an abstract class is subclassed, the subclass must provide implementations for all the abstract methods (if there are any) in its superclass. However, if it does not, then the subclass must also be declared abstract i.e. we can extend an abstract class to another abstract class.
Let's look at how this concept can be implemented in Java:
abstract class Abstract{
public Abstract(){
System.out.println("Abstract class constructor is called.");
}
public void concreteMethod(){
System.out.println("concreteMethod is defined in Abstract class.");
}
public abstract void abstractMethod();
}
class Concrete extends Abstract{
public Concrete(){
System.out.println("Concrete class constructor is called.");
}
@Override
public void abstractMethod(){
System.out.println("abstractMethod is defined in Concrete class.");
}
}
public class Main{
public static void main(String[] args) {
Concrete c = new Concrete();
c.abstractMethod();
}
}
As an exercise try playing with our code in the REPL below:
- Make an attempt to instantiate the Animal class without the
abstract
keyword and with anabstract
keyword. - Redefine
eat()
as an abstract method,abstract void eat()
and implement them for each class that we extended our abstract class. This should represent what fishes, mammals, and birds eat.
Summary
We discussed the first two concepts of OOP and implement these concepts in Java. We noted that the control of restriction for access is crucial in designing a safe implementation of a concept. More so, we wanted to express clear intentions for controlling the internal state of our objects: this is done to reduce the chance of errors.
OOP allows us to express higher-level concepts that stack on top of each other via abstraction. Here we passively introduced the notion of subclassing which we will discuss in more detail for the second part of this article.