OOP Concept for Beginners: What is Encapsulation

By: Thorben
  |  February 28, 2024
OOP Concept for Beginners: What is Encapsulation

Encapsulation is one of the fundamental concepts in object-oriented programming (OOP).  Let’s see how we can implement encapsulation using Java.

By definition, encapsulation describes bundling data and methods that work on that data within one unit, like a class in Java. We often often use this concept to hide an object’s internal representation or state from the outside. This is called information hiding.

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

If you’re familiar with any object-oriented programming language, you probably know these methods as getter and setter methods. As the names indicate, a getter method retrieves an attribute and a setter method changes it. Depending on the methods that you implement, you can decide if an attribute can be read and changed. You may also control if the attribute is read-only or not visible at all. Later, we’ll show you how you can also use the setter method to implement additional validation rules to ensure that your object always has a valid state.

Let’s take a look at an example that shows the concept of encapsulation in Java. This example implements information hiding and applies additional validation before changing the values of your object attributes.

It’s a basic concept that most Java developers use without a lot of thought. In essence, it’s simply how you design a Java class

Encapsulation in Java

If you’ve read our previous post about abstraction, you already saw several examples for encapsulation. It’s a basic concept that most Java developers use without a lot of thought. In essence, it’s simply how you design a Java class. You bundle a set of attributes that store the current state of the object with a set of methods using these attributes.

The CoffeeMachine Example

We’ll use encapsulation when creating the CoffeeMachine class example. The attributes configMapbeansgrinder and brewingUnit store the current state of the CoffeeMachine object. The methods brewCoffeebrewEspressobrewFilterCoffee and addBeans implement a set of operations on these attributes.

You can clone this and all other classes of the CoffeeMachine example project at https://github.com/thjanssen/Stackify-OopAbstraction.

import java.util.HashMap;
import java.util.Map;
public class CoffeeMachine {
    private Map configMap;
    private Map beans;
    private Grinder grinder;
    private BrewingUnit brewingUnit;
    public CoffeeMachine(Map beans) {
        this.beans = beans;
        this.grinder = new Grinder();
        this.brewingUnit = new BrewingUnit();
        this.configMap = new HashMap();
        this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28));
        this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480));
    }
    public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException {
        switch (selection) {
            case FILTER_COFFEE:
                return brewFilterCoffee();
            case ESPRESSO:
                return brewEspresso();
            default:
                throw new CoffeeException("CoffeeSelection [" + selection + "] not supported!");
        }
    }
    private Coffee brewEspresso() {
        Configuration config = configMap.get(CoffeeSelection.ESPRESSO);
        // grind the coffee beans
        GroundCoffee groundCoffee = this.grinder.grind(
            this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee());
        // brew an espresso
        return this.brewingUnit.brew(CoffeeSelection.ESPRESSO, 
            groundCoffee, config.getQuantityWater());
    }
    private Coffee brewFilterCoffee() {
        Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE);
        // grind the coffee beans
        GroundCoffee groundCoffee = this.grinder.grind(
            this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee());
        // brew a filter 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 kind of beans supported for each CoffeeSelection.");
            }
        } else {
            this.beans.put(sel, newBeans);
        }
    }
}

Information Hiding in Java

As explained earlier, you can use the encapsulation concept to implement an information-hiding mechanism. Similar to the abstraction concept, this is one of Java’s most commonly used mechanisms. You’ll find examples in almost all well-implemented Java classes.

You implement an information-hiding mechanism by making your class attributes inaccessible from the outside. You can also provide getter and/or setter methods for attributes to be readable or updatable by other classes.

Access Modifiers

Java supports four access modifiers that you can use to define the visibility of classes, methods and attributes. Each modifier specifies a different level of accessibility, and you can only use one modifier per class, method or attribute. As a rule of thumb, you should always use the most restrictive modifier that still allows you to implement your business logic.

Starting from the most to the least restrictive, these modifiers are:

  • private
  • no modifier
  • protected
  • public

Let’s take a closer look at each of these modifiers and discuss when you should use them.

Private

The most restrictive and most commonly used access modifier, the private modifier makes an attribute or method only accessible within the same class. Subclasses or any other classes within the same or a different package can’t access this attribute or method.

For all attributes and internal methods that shouldn’t be called from external classes, choose the private modifier by default. You might need to make an exception to this rule when you’re using inheritance. Also, exempt some of the subclasses that need direct access to an attribute or internal method. In that case, you should use the protected modifier instead of private.

No Modifier

No modifiers mean you can access attributes or methods within your class and from all classes within the same package. That’s why it’s often called package-private.

We use the private modifier to restrict access to all attributes and the brewEspresso and brewFilterCoffee methods in the CoffeeMachine example. We can only use these attributes and methods within the CoffeeMachine class and are not part of the public API.

That might seem a bit confusing in the beginning. However, it’s very useful when the classes in your package implement a well-defined set of logic. It’s also practical if you want to control the API that’s available to classes outside of this package. You can then use package visibility to implement a method only used by classes within this package. That allows you to create a package internal and an external API.

Protected

Attributes and methods with the access modifier protected can be accessed within your class, by all classes within the same package and by all subclasses within the same or other packages.

The protected modifier gets mostly used for internal methods that need to be called or overridden by subclasses. You can also use the protected modifier to allow subclasses to access internal attributes of a superclass directly.

Public

This is the least restrictive access modifier. Methods and attributes that use the public modifier can be accessed within your current class and by all other classes.

Public methods and attributes become part of the public API of your class and of any component in which you include them. That is almost never a good idea for any attribute, so you should think twice before using this modifier on a method.

When a method is publicly available, you need to ensure that it’s well documented and robustly handles any input values. Also keep in mind that some part of your application will use this method, which will make it hard to change or remove it.

Generally, your public API should be as lean as possible. Public APIs should only include the methods intended for other parts of the application or access by external clients.

That’s the case for the CoffeeMachine class, its constructor and the brewCoffee and addBeans methods. The CoffeeMachine class has to be public because it represents the interface of the coffee machine. The CoffeeMachine class is intended to be used by other classes that don’t have to be part of the same package. The constructor and the brewCoffee and addBeans methods are accessible by other classes to create a new instance of CoffeeMachine. These methods also interact with it by adding coffee beans or by brewing a fresh cup of coffee.

The brewCoffee method shows another benefit of the different access modifiers. You can not only use it to hide information, but you can also use it to support abstraction. The public brewCoffee method abstracts the internal details of the brewFilterCoffee and brewEspresso methods, which are both private. The access modifiers ensure that an external class can only call the abstraction provided by the brewCoffee method, but not the internal methods.

Accessibility Matrix

Here you can see an overview of the different access modifiers and the accessibility of the attributes or methods.

Here you can see an overview of the different access modifiers and the accessibility of the attributes or methods | oops concept for beginners

The Coffee Example

The Coffee class provides a good example of the information-hiding mechanism and represents a drink brewed by the CoffeeMachine.

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("Quantity has to be >= 0.0.");
        }
    }
}

The class uses two private attributes to store information about the CoffeeSelection and the quantity of the drink. The access modifier private makes both attributes inaccessible for other classes within the same or other packages. If you want to get information about an object’s current state, you may call one of the public methods.

The getSelection method provides read access to the selection attribute. It represents the kind of coffee the CoffeeMachine brewed, e.g. a filter coffee or an espresso. As you can see in the code snippet, we didn’t implement a setter method for this attribute, because you can’t change the kind of coffee after it is brewed (at least we don’t know how to change a boring filter coffee into a strong and tasty espresso!).

The available quantity of a drink changes over time. After every sip you take, your cup contains a little bit less. Therefore, we implemented a getter and setter method for the quantity attribute.

If you take a closer look at the setQuantity method, you can see that we also implemented an additional validation. If the coffee is especially delicious, you might drink it until your cup is empty. When you do that, your coffee is gone, and you can’t drink any more. So the quantity of the Coffee has to be greater or equal to zero.

OOP makes programs easier to write and debug since all objects have a common structure and behavior

Other OOP Concepts

OOP makes programs easier to write and debug since all objects have a common structure and behavior. Encapsulation is part of four OOP concepts, with the rest being abstraction, polymorphism, and inheritance. With abstraction, you define an object and remove unnecessary details. Polymorphism allows you to define an object that can change its behavior depending on context. Finally, inheritance is the process through which an individual object gains all of the characteristics of another object(i.e a child object inherits attributes and methods from the parent object).

Summary

Encapsulation is one of the core concepts in object-oriented programming and describes the bundling of data and methods operating on this data into one unit.

You can use it to implement an information-hiding mechanism. This mechanism reduces the accessibility of attributes to the current class and uses public getter and setter methods to control and restrict external access to these attributes. These methods allow you to define which attributes to read or update and enable you to validate the new value before changing the attribute.

Encapsulation provides the basic property to hide data, thereby providing security to user data. Performing encapsulation is a great OOP practice, though it’s best when paired with a robust APM solution like Retrace for error monitoring.

Try your free, 14-day Retrace trial today!

Also, try Stackify’s free code profiler, Prefix, to write better code on your workstation. Prefix works with .NET, Java, PHP, Node.js, Ruby, and Python.

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]