Last Updated:

Java Encapsulation with Simple Examples

Java: Encapsulation

What is encapsulation?

 

Encapsulation describes the idea of combining data and methods that work with that data in a single module, similar to a Java class. And the concept is quite often used to hide the internal representation or state of the object from the outside.

The general idea of this mechanism is simple. For example, you have an attribute that is invisible from the outside of the object. You associate it with methods that provide read or write access, and encapsulation allows you to hide specific information and control access to the internal state of the object.

If you're familiar with any object-oriented programming language, you probably know these methods as methods of getting and setting – getters and setters. As you can see from the names, the receive method gets the attribute, and the setup method changes it. Depending on which method you use, you decide whether to read or change the attribute. You can also specify whether the attribute is read-only or not at all.

 
 

Without wasting time, we will analyze an example that clearly demonstrates the concept under consideration and how it works in Java.

Encapsulation in Java

This basic concept tells you how to properly design a class in Java to associate a set of attributes that store the current state of an object with a set of methods that use those attributes.

Consider its implementation on an example describing the operation of a coffee machine.

Let's create a Machine class with the configbeansgrinder, and brewingUnit attributes that store the current state of the object. The brewCoffeebrewEspressobrewFilterCoffee, and addBeans methods implement a set of operations on these attributes.

Example 1

import java.util.HashMap;
import java.util.Map;

public class Machine{
    private map config;
    private Map beans;
    private Grinder grinder;
    private BrewingUnit brewingUnit;

    public Machine(Map beans) {
        this.beans = beans;
        this.grinder = new Grinder();
        this.brewingUnit = new BrewingUnit();
        this.config = new HashMap();
        this.config.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28));
        this.config.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480));
    }

    public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException {
        switch (selection) {
            case FILTER_COFFEE:
                return brewFilter();
            case ESPRESSO:
                return brewEspresso();
            default:
                throw new CoffeeException("CoffeeSelection [" + selection + "] not supported!");
        }
    }

    private Coffee brewEspresso() {
        Configuration config = configMap.get(CoffeeSelection.ESPRESSO);

        // grind coffee beans
        GroundCoffee groundCoffee = this.grinder.grind(
            this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee());

        // brew espresso
        return this.brewingUnit.brew(CoffeeSelection.ESPRESSO,
            groundCoffee, config.getQuantityWater());
    }

    private Coffee brewFilter() {
        Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE);

        // grind coffee beans again
        GroundCoffee groundCoffee = this.grinder.grind(
            this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee());

        // mode of single pass of hot water through a layer of coffee
        return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE,
            groundCoffee, config.getQuantityWater());
    }

    public void addBeans(CoffeeSelection sel, CoffeeBean newBeans) throws CoffeeException {
        CoffeeBean existingBeans = this.beans.get(sel);

        if (existingBeans != null) {
            if (existingBeans.getName().equals(newBeans.getName())) {
                existingBeans.setQuantity(existingBeans.getQuantity() + newBeans.getQuantity());
            } else {
                throw new CoffeeException("Only one bean type is supported per mode.");
            }
        } else {
            this.beans.put(sel, newBeans);
        }
    }
}

Hiding information in Java

As we've said before, you can also use the concept of encapsulation to implement a mechanism for hiding information. This approach, like abstraction, is one of the most commonly used mechanisms in Java. Examples of it can be found in almost all well-implemented Java classes. The hiding mechanism makes class attributes inaccessible from the outside.

 

More useful materials you will find on our telegram channel "Library of Javista"
Interesting, go to the channel

Access modifiers

Speaking about this concept, it will not be superfluous to analyze the tools for indicating the availability of elements - modifiers. Java supports four access modifiers used to determine the visibility of classes, methods, and attributes. Each specifies the availability level, and you can use only one for each class, method, or attribute. Let's list them, starting with the most restrictive and ending with the least strict:

  1. private.
  2. no modifier.
  3. protected.
  4. public.

Let's take a closer look at each of them and talk about when you should use them.

Private modifier

The most restrictive and most commonly used private access modifier makes an attribute or method available only within the same class. Subclasses or any other classes in the same or a different package cannot access this attribute or method. We only use it for attributes and methods that we'll never want to call again.

In our example, we use it to restrict access to all the attributes and methods of brewEspresso and brewFilter. These attributes and methods should only be used in the Machine class and are not part of the public API. At first, this may seem a bit confusing. However, it is very useful when the classes in your package implement a well-defined set of logic. This is also useful if you want to manage the APIs available to classes outside of this package. You can then use package visibility to implement a method that is used only by the classes in that package.

Modifier no modifier

The lack of a modifier means that you can access attributes and methods within your class and from all classes in a single package. That's why it's often called batch.

Modifier protected

Attributes and methods with the protected access modifier can be accessed within your class by all classes in a single package, as well as by all subclasses in the same or different packages. This modifier is typically used for internal methods that must be called or overridden by subclasses. You can also use it to allow subclasses to directly access the internal attributes of a superclass.

Public modifier

Methods and attributes that use the public modifier can be accessed in the current class or in all other classes.

Public methods and attributes become part of the public API of your class and any component in which you use them. When a method is publicly available, you need to make sure that it is well documented and handles any input values reliably. Also keep in mind that this method will be used by some part of your app, making it difficult to modify or delete it.

In general, your public API should be as compact as possible and should only include methods that target other parts of your application or external client access.

In our example, the Machine class should be publicly available because it represents the interface of a coffee machine and is intended for use by other classes that do not necessarily have to be part of the same package. The brewCoffee and addBeans constructor and methods are available to other classes for creating a new Machine instance when you add coffee beans to the machine or brew a fresh cup of coffee.

Example 2

It clearly demonstrates the mechanism of hiding information describing the drink prepared with the help of our coffee machine.

public class Coffee {
    private CoffeeSelection selection;
    private double quantity;

    public Coffee (CoffeeSelection selection, double quantity) {
        this.selection = selection;
        this.quantity = quantity;
    }

    public CoffeeSelection getSelection() {
        return selection;
    }

    public double getQuantity() {
        return quantity;
    }

    public void setQuantity(double quantity) throws CoffeeException {
        if (quantity >= 0.0) {  
            this.quantity = quantity;
        } else {
            throw new CoffeeException("Количество должно быть >= 0.0");
        }
    }
}

The Coffee class uses two private attributes to store information: CoffeeSelection and quantity. The private access modifier makes both attributes inaccessible to other classes in the same or different packages. If you want to get information about the current state of an object, you can call one of the public methods.

The getSelection method provides read access to the selection attribute. It represents the type of coffee made by a coffee machine. As you can see from the code snippet, we didn't implement the set method for this attribute because we can't change the coffee variety after it's brewed. The amount of drink available changes over time. After each sip, there is a little less left in your cup. Therefore, we implemented a getter and set method for the quantity attribute.

If you look closely at the setQuantity method, you'll see that we've also implemented additional validation. If the coffee is particularly delicious, you can drink it until your cup is empty. When you do, your coffee will run out and you will no longer be able to drink it. Thus, the amount of coffee should be greater than or equal to zero.

***

Encapsulation describes combining data and methods that work with that data into a single module and is used to implement a mechanism for hiding information. This concept of OOP helps us protect user information from erroneous actions, thereby increasing the efficiency of further work with the code.