OOP concepts in Java

Published in the Java Developer group
One of Java's greatest strengths is object-oriented programming (OOP). That’s the reason why this language has became so popular and is well suited for projects of any size. OOP concepts in Java - 1 What is object-oriented programming? It's not magic, but it can seem magical if you really get in to it. OOP is about how to build your software. It is a concept, or rather a bunch of concepts, that allow you to create some specific interactions and relationships between Java objects in order to effectively develop and use software. Classical OOP includes 3 + 1 main concepts. Let's start with the classics.

The Object

Java objects as well as real-world objects have two characteristics: state and behavior.

For example, a Human object has state (name, gender, sleeping or not…) and behavior (studies Java, walks, talks...). Any Java object stores its state in fields and exposes its behavior through methods.

Encapsulation

Data encapsulation is hiding internal data from the outside world, and accessing it only through publicly exposed methods. What does that mean? What data? Hiding from whom? Hiding means to restrict direct access to the data members (fields) of a class.

How it works in Java:

  1. Fields are made private
  2. Each field in a class gets two special methods: a getter and a setter. Getter methods return the value of the field. Setter methods let you change the value of the field in an indirect but permissible way.
Example of encapsulation in Java code:

public class Student {
private int age;
private String name;

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

public class Test{
public static void main(String[] args) {
Student firstStudent = new Student();
firstStudent.setName("John");
// The name field is private, so you can no longer do this:  firstStudent.name = "John"; 
}
}

Why should you use encapsulation?

The main reason is to make it easier to change your code. Imagine you have an application for a hockey school and there is a HockeyStudent class with two fields that store the student's name and age when he or she enrolled at the school. Something like this:

public class HockeyStudent {
public String name;
public  int ageOfEnrollment;
}
The ageOfEnrollment field is public, no getters or setters… This class is used by many other classes, and everything was ok until some developer decided that a single int field was not enough. Some hockey players in a cohort are almost one year older than their peers, so it would be more convenient to split them into two groups depending on the month they were born. So the ageOfEnrollment field should be changed to an int array (int[][]): the first number is for full years and the second is for months. Now you need to refactor all code that uses the Student class! But if your ageOfEnrollment field is private and you have getters and setters, then everything is easier. If the requirement for setting the age of a student changes, just update the logic in the setAgeOfEnrollment() setter method and your classes can continue using Student without any problems! This example is somewhat contrived, but I hope it explains why using encapsulation is a great idea.

Inheritance

This principle is easier to understand even without any practical experience. Don't repeat yourself (DRY) could be the motto for the inheritance concept. Inheritance lets you create a child class that inherits the fields and methods of the parent class without redefining them. Sure, you can override the parent class's fields and methods in the child class, but it's not a necessity. What's more, you can add new states and behaviors in the child class. Parent classes are sometimes called superclasses or base classes, and child classes are known as subclasses. Java's extends keyword is used to implement the principle of inheritance in code.

How it works in Java:

  1. Create the parent class.
  2. Create the child class using the extends keyword.
  3. In the Child class's constructor, use the super(parentField1, parentField2, ...) method to set the parent's fields.

A constructor is a special method used to initialize a newly created object. A constructor has the same name as its class name. There are two types of constructors: default (no-arg constructor) and parameterized constructor. A class must have at least one constructor (it has the default constructor if not other constructors have been defined) and it can have a lot of them.

Every time you create a new object, you call its constructor. In the example above, you do this in this line:


Student firstStudent = new Student();

You use the new keyword to call the Student class's default constructor: Student().

Some rules:

  1. One class can have only one parent.
  2. One parent class can have many child classes.
  3. A child class can have its own child classes.
Example of inheritance in Java code Let's create a Phone class.

public class Phone {
    int price;
    double weight;

// Constructor
public Phone(int price, double weight) {
        this.price = price;
        this.weight = weight;
    }

    void orderPhone(){
        System.out.println("Ordering phone...");
    }
}
Of course, there are different type of phones, so let's create two child classes: one for Android phones and a second for iPhones. Then we'll add some fields and methods that the parent doesn't have. And we'll use super() to call constructors to initialize the fields that the parent class does have. Example of inheritance in Java

public class Android extends Phone {

// Some new fields     
String androidVersion;
int screenSize;

    String secretDeviceCode;

// Constructor 
    public Android(int price, double weight, String androidVersion, int screenSize, String secretDeviceCode) {
        super(price, weight); // Android inherits Phone’s fields

        //this - reference to the current object
        //super - reference to the parent object

        this.androidVersion = androidVersion;
        this.screenSize = screenSize;
        this.secretDeviceCode = secretDeviceCode;
    }

	// New Android-specific method, does not exist in the Phone class 
    void installNewAndroidVersion() {
        System.out.println("installNewAndroidVersion invoked...");

    }

}

public class IPhone extends Phone {
   
    boolean fingerPrint;

    public IPhone(int price, double weight, boolean fingerPrint) {
        super(price, weight);
        System.out.println("IPhone constructor was invoked...");
        this.fingerPrint = fingerPrint;
    }

    void deleteIPhoneFromDb() {
        System.out.println("deleteIPhoneFromDb invoked...");
    }

@Override // This is about polymorphism, see below
void orderPhone(){
        System.out.println("Ordering my new iPhone and deleting the old one...");
    }
}
So, to repeat: in Java, inheritance lets you extend a class with child classes that inherit the fields and methods of the parent class. It's an excellent way to achieve code reusability.

Polymorphism

Polymorphism is an object's ability to morph, taking on different forms or rather acting in different ways. In Java, polymorphism usually happens when a parent class reference is used to refer to a child class object.

What that means and how it works in Java:

What is polymorphism in Java? In general, that means you can use the same method name for different purposes. There are two types of polymorphism in Java: method overriding (dynamic polymorphism) and method overloading (static polymorphism).

Method overriding

You can override a parent class's method in a child class, forcing it to work in different way. Let's create a Musician parent class with a play() method. Example of polymorphism in Java code

   public class Musician {
    String name;
    int age;

    // Default constructor
    public Musician() {
    }

    // Parameterized constructor
    public Musician(String name, int age) {
        this.name = name;
        this.age = age;
    }

    void play() {
        System.out.println("I am playing my instrument...");
    }
}
Different musicians use different instruments. Let's create two child classes: Pianist and Violinist. Thanks to polymorphism, each will execute its own version of the play() method. When overriding, you can use the @Override annotation, but it is not necessary.

public class Pianist extends Musician {
    
    String favoritePianoType;

    public Pianist(String name, int age, String favoritePianoType) {
        super(name, age);
        this.favoritePianoType = favoritePianoType;
    }


    @Override
void play(){
        System.out.println("I am playing the piano...");
    }
}
The violin player could be a soloist or a member of an orchestra. Let's take that into consideration when overriding our play() method.

public class Violinist extends Musician { 
    boolean isSoloist; 

public Violinist(String name, int age, boolean isSoloist) {
            super(name, age);
            this.isSoloist = isSoloist;
        }


    @Override
void play(){
if (isSoloist) 
        System.out.println("I am playing the violin solo...");
else 
System.out.println("I am playing the violin in an orchestra...");

    }
}
Let's create a Demo class, in which we will create three objects, one instance of each of the previously created classes. We'll see what results we get.

public class Demo {
  public static void main(String[] args) {
  Musician musician = new Musician();
  Violinist violinist = new Violinist("John", 32, true);
  Pianist pianist = new Pianist("Glen", 30, "Acoustic"); 

  System.out.println("Musician said:");
  musician.play();
  System.out.println("Violinist said:");
  violinist.play();
  System.out.println("Pianist said:");
  pianist.play();
    }
}
Here's what we get:

Musician said:
I am playing my instrument...
Violinist said:
I am playing the violin solo…
Pianist said:
I am playing the piano...
Every violinist and pianist is a musician, but not every musician is a violist or pianist. That means that you can use the musician's play method if you don't need to create a new one. Or you can call the parent's method from the child using the super keyword. Let's do that in Pianist's code:

public class Pianist extends Musician {

    String favoritePianoType;
    
    @Override
    void play(){
        super.play();
        System.out.println("I am playing the piano...");
    }
}
Now let's call our main() method in the Demo class. Here's the result:

Musician said:
I am playing my instrument...
Violinist said:
I am playing the violin solo...
Pianist said:
I am playing my instrument...
I am playing the piano...

Method overloading

Method overloading means using various methods with the same name in the same class. They must be different in terms of the number, order, or types of their parameters. Suppose that a pianist can play an acoustic piano and an electric piano. To play an electric, the musician needs electricity. Let's create two different play() methods. The first one without parameters, for an acoustic piano, and the second with a parameter that indicates whether electricity is available.

public class Pianist extends Musician {

    String name;
    int age;
    String favoritePianoType;

    @Override
    void play(){
        super.play();
        System.out.println("I am playing the piano...");
    }
    void play(boolean isElectricity){
        if (isElectricity) {
            System.out.println("The electricity is on.");
            System.out.println("I am playing the piano...");
        }
        else System.out.println("I can't play this without electricity.");
    }
}
By the way, you can use the first play() method inside the second play(boolean) method in this manner:

void play(boolean isElectricity){
        if (isElectricity) {
            System.out.println("The electricity is on.");
            play();
        }
        else System.out.println("I can't play this without electricity.");
    }
Let's add some lines to our Demo class to demonstrate our overloading:

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

        Musician musician = new Musician();
        Violinist violinist = new Violinist("John", 23, true);
        Pianist pianist = new Pianist("Glen", 30, "Acoustic"); 

        System.out.println("Musician said:");
        musician.play();
        System.out.println("Violinist said:");
        violinist.play();
        System.out.println("Pianist said:");
        pianist.play();
        System.out.println("The pianist will now try the electric piano:");
        pianist.play(true);
        System.out.println("The electricity has been shut off. Now when trying the electric piano, the pianist says:");
        pianist.play(false);
    }
}
Here is the result:

Musician said:
I am playing my instrument...
Violinist said:
I am playing the violin solo...
Pianist said:
I am playing my instrument...
I am playing the piano...
The pianist will now try the electric piano:
The electricity is on.
I am playing my instrument...
I am playing the piano...
The electricity has been shut off. Now when trying the electric piano, the pianist says:
I can't play this without electricity.
Java knows which method should be used based on its parameters and the object type. That's polymorphism.

Abstraction

When we define a class, we are trying to build a model of something. For example, suppose we're writing a video game called MyRacer with different race cars. A player can choose one of them and then later update it or buy a different one. So… What is a car? A car is a pretty complicated thing, but if we are trying to create an racing video game (as opposed to a driving simulator), then we don't need to describe the all thousands of gears and gaskets it contains. We need its model, top speed, maneuverability characteristics, price, color… And maybe that's enough. That's the model of a car for our game. Later in MyRacer 2, suppose we decide to add tires that affect handling on the road. Here the model is different, because we added more details. Let's define data abstraction as the process of identifying only the important (or necessary) characteristics of an object and ignoring any irrelevant details. There are different levels of abstraction. For example, if you are a passenger on a bus, you need to know what your bus looks like and where it is going, but you don't need to know how to drive it. If you are a bus driver, you don't need to know how to create a new bus — you only need to know how to drive it. But if you are a bus manufacturer, you need to go to a lower level of abstraction, because the details of the bus design are very important to you. I hope you understand what I mean. OOP concepts in Java - 2

How it works in Java:

Let's build four levels of abstraction in Java, or rather in OOP — from the lowest (the most specific) to the highest (the most abstract).
  1. The lowest level of abstraction is a specific object. It is an entity with a set of characteristics that belong to a specific class. It has specific field values

  2. A template for creating objects is a class. It's a description of a set of objects with similar properties and internal structure.

  3. An abstract class is an abstract description of the characteristics of a set of classes (it acts as a template for inheritance by other classes). It has a high level of abstraction, so it is impossible to create objects directly from an abstract class. Only child classes of abstract classes can be used to create objects. An abstract class may include methods with an implementation, but this is not a requirement.

  4. An interface is a construct of the Java programming language construct that contains only abstract public methods and static constant fields (final static). In other words, neither abstract classes nor interfaces can be used to generate objects.

BTW, in Java 8 or later, interfaces can have not only abstract methods and constants, but also default and static methods. In Java, an interface defines a behavior, while an abstract class is used to create a hierarchy. One interface can be implemented by mutiple classes. Example of an interface in Java code

interface Human {
	public void struggle();
	public void protect();
}

interface Vulcan {
	int angleOfPointyEars; 
	public void turnOffEmotions(boolean isOn);
	public void telepathy();
}
You can implement more than one interface

The Spock class implements Human and Vulcan {
public void struggle() {
System.out.println("I am struggling...");
}
	public void protect() {
System.out.println("You are under my protection!”);
}
public void turnOffEmotions(boolean isOn){
If (isOn) {
System.out.println("I am turning off my emotions.");
isOn= !isOn;
}
}
	public void telepathy() {
System.out.println("Connecting to your brain...");
}

}
For beginning students, that covers all the main concepts of object-oriented programming in Java. Besides the 4 main OOP principles, Java also has association, aggregation, and composition. You can call them "additional OOP principles". They deserve their own separate article.

More reading:

Comments (9)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Adam Odoj Level 15, Poland
10 May 2022
"The Spock class implements Human and Vulcan { " in the first line of code. AND is the right way to add various interfaces??? Looks strange...
Ashish RajAnand Level 13, Bhilai , India
6 May 2020
nice artical
Pandora Level 17, Stockholm, Sweden
11 February 2020
I am confused by this method, can someone read it to me in plain English (what it means)?

public void emotionOff(boolean isOn){
If (isOn) {
System.out.println(“I am turning off my emotions”);
isOn= !isOn;
}
}
Jay Level 17, Washington, United States
9 August 2019
Good stuff!
Xavier Level 3, Durban, South Africa
6 March 2019
nice Article...
Ahmed Khalil Level 1
6 February 2019
nice article
Kaushal Level 5, Guwahati, India
30 January 2019
Really nice article...Thx