Encapsulation in OOP: Definition and Examples

Programming is all about solving problems. An essential aspect of solving these problems through software is modeling and embodying real-life entities and manipulating these abstractions in a structured way to simulate operations and get things done. Different programming paradigms approach this facet of writing code differently. One of the most popular of these paradigms is object-oriented programming (OOP) that deals with class templates and their object instantiations.

A core principle of this OOP regime is encapsulation – the mechanism of binding data (attributes) and methods (operations) together into objects and limiting the direct access to its internals. In this post, we will do a deep dive into this topic and understand what makes it so significant in the world of programming.

Here’s an outline of what we’ll be covering so you can easily navigate or skip ahead in the post – 

What is Encapsulation in Object-Oriented Programming?

Encapsulation, in general, is nothing but a fancy word for packaging or enclosing things of interest into one entity.

en · cap · su · la · tion

the action of enclosing something in or as if in a capsule.

Used most commonly in the realms of object-oriented programming, encapsulation refers to the packaging of data and functions that represent an embodiable (real world) entity into a programmable enclosure (commonly classes/objects). 

The data (variables) here represent the attributes and properties that define the unit, whereas the functions symbolize its possible behaviors and operations. So, in other words, encapsulation is the binding of data and methods into this one entity to define itself and the scope of its operations. The most common example of such a unit would be a class/object setting, which we’ll cover in detail in the next section.

For example, suppose we were to encapsulate aspects of a human being into a programmable entity. In that case, the data would distinguish one human from another, and its methods would define possible operations (behavior/actions). Below is a reduced representation of the same

undefined

Encapsulating the notion of a human being

This binding gives meaning to the entity and semantically resembles aspects of the real-world object it is trying to model.

Additionally, this bundling of data and methods into individual entities hides complexity by providing abstracted interfaces (the objects) to program and interact with; instead of directly with lower-level constituents. More importantly, this protects your object’s internal state from undesirable external access and modification through a concept popularly known as information hiding. This proves to be a critical aspect for applications dealing with sensitive data and also prevents failures in one part of your code to bleed into the rest. We’ll dive deeper into this in a dedicated section later in this post.

As one might know, encapsulation is a core principle of the object-oriented programming (OOP) paradigm (along with inheritance, abstraction, and polymorphism). Interestingly, it has its description inextricably intertwined with how it reflects in the context of objects and classes. Let’s step away from the theory of encapsulation and look at constructs that can serve as these capsule-like “entities” for holding your data and functions together. 

Object-Oriented Programming: Classes and Objects

It is much easier to understand encapsulation once you have a good grasp of OOP concepts (and vice versa).

OOP principles revolve around the class/object way of doing things. Here, a class is a template or a blueprint of the real-world entity you want to model. For example, you could have a class for a person, a car, a country, an animal, or for pretty much any other category. This class serves as a template with (placeholder) data entries for defining class attributes and functions representing behaviors that define the class or type.

An object, however, is a manifestation of a class, i.e., an instantiation of it. For example, an object of a person class can be “Person A” or “Person B”. Similarly, an object of the animal class can be a dog, a cat, a cow, etc.

Person class and its objects

Objects of the animal class

How you choose to define your class abstraction is up to you and your use case. For example, you can have an animal class with a dog as an object, or a dog class with “dog a”, “dog b” (or “breed a,” “breed b”) objects, or even both hierarchies simultaneously.

Dog class and its objects (different breeds)

You can also follow the common noun / proper noun analogy and think of a class as a common noun and an object as a proper noun. If you are interested in learning more about object-oriented programming and the other programming paradigms out there (like functional and procedural programming), check out our OOPS vs Functional vs Procedural Programming post.

Bundling Data and Methods

Now that we have established classes and objects, encapsulation in OOP can package the entity’s characteristics (attributes and operations) into these classes (as templates) or their objects. 

Let’s draw a broad outline of a dog class example and instantiate its objects and use that to discuss encapsulation.

Dog class: {
data:
            Breed name:
            Colour:
            Average weight (kg):
      functions:
            func sit() {}
            func bark() {}
            func walk() {}

            func run() {}
}

Here, as you can see, the Dog class constitutes information about the breed’s name, its color, and average weight through (what are known as) class variables. More attributes define a dog’s breed, but we can stick to these for simplicity.

These data attributes will define the objects instantiated from this class, i.e., each object of this class will have its own values for each of the attributes and help distinguish them from their counterparts. Let’s look at two arbitrary objects of this class below —

Dog (breed) A object: new Dog (
            Breed name: “Breed A”
            Colour: “black”
            Average weight (kg): 20
)
Dog (breed) B object: new Dog (
            Breed name: “Breed B”
            Colour: “brown”
            Average weight (kg): 15
)

b_object.run() or b_object -> run ()Here, we create two instances of the Dog class and with different properties that define two arbitrary breeds. Both these instances have access to the same functions we defined in the Dog class (i.e., sit, bark, walk, and run), and depending on the programming language, can be invoked as —

Note: For multiple objects of the same class, the functions are common for all objects, whereas values of data properties can be common across all objects or unique for each.

Encapsulation in the context of our example is the capturing of a bunch of defining attributes and behaviors of a dog into a class enclosure — which can further manifest as multiple objects. This allows us to effectively model a real-world entity — a dog in our example — into our code and further programmatically manipulate the abstraction without worrying too much about the lower-level data.

Encapsulation is mostly always used in the context of OOP but doesn’t have to be limited to OOP realms. One of the most famous examples of encapsulation in software technologies is containerization technology, which has taken the industry by storm. We will plunge into this real-world example a little later in the post

Before that, let’s look at another integral merit of encapsulation — the data security it provides.

Information Hiding through Encapsulation

So far, we have been focusing on the bundling aspect of encapsulation — how it ties down attributes and behaviors of entities into one unit. Another vital aspect of encapsulation is information hiding.

Binding data and functions to specific constructs instead of letting them hang around in your program’s global state also allow you to exercise control over their accessibility to external sources. You can restrict access to your object’s internal state through encapsulation and protect sensitive data from unauthorized or accidental access or interference. This virtue of encapsulation is popularly referred to as information hiding — because of how it hides and prevents direct access to your object’s state and methods. Programming languages allow this through access modifiers when declaring class variables and functions. 

There are three primary types of access modifiers across most programming languages —

How Do You Use Encapsulation?

In programming languages like Java, the access modifiers are specified explicitly when declaring the class variables (or methods) — through keywords. For example, here’s a Java class with three variables made accessible to the rest of the program using different access modifiers —

class MyClass { 
    public int myPublicVar = 20;
    protected int myProtectedVar = 10;
    private int myPrivateVar = 30; 
}  

In Python, however, access modifiers are specified through underscores at the beginning of a variable’s or function’s name.

class Student:
   
    _name = "John" # protected data (single underscore before name)
    __roll = 10 # private data (double underscore before name)
   
    # constructor
    def __init__(self, name, roll, school):
          self._name = name
          self.__roll = roll
          self.school = school # public data

Note: Private variables in Python are not entirely private (hidden). They can be accessed outside the class using some unconventional ways. Regardless, Python has superficially still given this option to allow programmers to enforce some restrictions. For more context and discussion about Python’s philosophy on private class variables, We recommend you check out this Stack Overflow forum thread.

Coming back on track here, based on the modifier rules we discussed above, one can choose which variables to protect and which to keep publicly available. This lets you control how much of your capsule’s contents you want to reveal to the external components. The overall idea here, as mentioned before, is to protect unwanted access to sensitive data.

Now, you could be wondering — how then do we access private variables of a class at all? If you noticed, we briefly hinted at how functions of the same class can access private data. We’ll get to full-fledged examples of encapsulation and comprehensively cover all these access modifiers, but before that, let’s get a brief overview of how we can allow safe access to private variables without putting them at risk.

Getter and Setter Methods

In programming languages like Java, this takes place using (what are known as) getter and setter methods. These are essentially class functions that act as intermediary interfaces to fetch or manipulate the state of your object. The below diagram presents an overview of accessing private variables through getter functions.

Getters can allow (read) access to private variables.

As the name suggests, a getter function can fetch (read) an instance variable, whereas a setter function enables updating it. As per Java naming conventions, getter and setter function names need to start with “get”/”set” and followed by the variable’s name (that we want to read or update) in a camel-case setting, as shown in the example below —

class MyClass {
  private int var1;
 
  // Getter function
  public int getVar1() {
    return var1;
  }
 
  // Setter function
  public void setVar1(int v) {
    this.var1 = v;
  }
}
public class HelloWorld {
   
    public static void main(String []args){
        MyClass myObject = new MyClass(); // creating class's object
        myObject.setVar1(20); // invoking setter to set value
        System.out.println(myObject.getVar1()); // get private variable value through getter function       
    }
}

Output:

20

This worked as expected. However, if we tried to print the private variable directly (using myObject.var1), this would have resulted in an error because our variable is private.

If used correctly, this can regulate access to your application’s sensitive data, preserve its integrity, and prevent unwanted modifications. Therefore, to summarise things, this level of control over access to your object’s state can allow you to exercise more control over what aspects of your object you want to reveal to the outside. This can be plenty helpful for building secure, robust real-world applications.

Now let’s look at a few examples to understand better the concepts we covered and observe encapsulation in action. Encapsul-action!

Encapsulation Examples

Basic Example in Java — Playing with Different Access Modifiers

This first example will focus on accessing variables with different access modifiers in the Java programming language. For this, let’s create an arbitrary class and declare variables with the various access specifiers. We’ll reuse the same variables we saw above.

class MyClass { 
    public int myPublicVar = 20;
    protected int myProtectedVar = 10;
    private int myPrivateVar = 30; 
}  

Next, let us create another class that will house our main method. We’ll also inherit the previous class here to be able to work with our protected variable.

public class HelloWorld extends MyClass {
    public static void main (String []args){
        MyClass myObject = new MyClass(); // create class's object
        System.out.println(myObject.myPublicVar); // printingpublic variable
        System.out.println(myObject.myProtectedVar); // printing protected variable
    }
}

Output:

20

10

This worked just fine because myPublicVar is public. The protected variable (myProtectedVar)  will also come through because we inherited the other class (and both classes are in the same package). Now let’s see how things change when we directly access our private variable (myPrivateVar).

public class HelloWorld extends MyClass { // extending MyClass -- to be able to access protected variables
   
    public static void main(String []args){
                MyClass myObject = new MyClass();
                System.out.println(myObject.myPrivateVar); // trying to directly access private variable -- will result in error
    }
}

Output:

Error: myPrivateVar has private access in MyClass
                System.out.println(myObject.myPrivateVar); // trying to directly access private variable -- will result in error
                                          ^
1 error

As expected, this fails. This is because only elements of the class itself can access private variables. Therefore, it’s time to add a getter function to MyClass (like we did before) and invoke that through the object (myObject) to get the private variable’s value.

class MyClass { 
    public int myPublicVar = 20;
    protected int myProtectedVar = 10;
    private int myPrivateVar = 30; 
   
    public int getMyPrivateVar (){ // added (public) getter function to access private var
        return this.myPrivateVar;
    } 
}

Note the naming convention here —  the getter function for myPrivateVar becomes getMyPrivateVar.

Now in our main class, we can invoke this function (because it’s public) and therefore read the private variable, as shown below —

public class HelloWorld extends MyClass {
   
    public static void main(String []args){
        MyClass myObject = new MyClass();
        System.out.println(myObject.getMyPrivateVar()); // accessing private variable through getter function       
    }
}

Output:

30

Note how this implementation only lets us read the variable and therefore still prevents modification. 

Now let’s do one last example — a more realistic one this time.

Basic User Database Example in Java

For this example, we’ll try to simulate a dummy user database through a bunch of user objects, and the idea here, too, would be to restrict the direct access to instance data. This becomes even more critical in settings where users’ personal data is at stake.

class User { 
    private String firstName; // private data
    private String lastName;
    private int age;
    public String userID; // assuming IDs can be public
 
    User (String firstName, String lastName, int age, String userID) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.userID = userID;
    }
 
      // Getter function
      public String getFirstName() {
        return this.firstName;
      }
     
      // Getter function
      public String getLastName() {
        return this.lastName;
      }
     
      // Getter function
      public int getAge() {
        return this.age;
      }
     
      // Getter function
      public String getUserID() {
        return this.userID;
      }
     
      // Setter function
      public void setAge(int age) {
        this.age = age;
        System.out.println("User #" + this.userID + "'s age updated to " + age); // printing update information
      }
   
}

Here we create a user class for our application with four attributes and a constructor function to populate the instance data right off the bat. We’ve kept the ID public for some variety and assume that it might be helpful for the organisation’s inner workings. As for the getter and setter functions, it is common to have getter and setter functions for each variable, but for our dummy use case, we’ll assume that only the age variable is updatable (through a setter). In contrast, others can only be read (through their own getters). 

Now let’s instantiate this class in our main method and fetch and update stuff to test our code.

public class HelloWorld {
   
    public static void main(String []args){
        User user1 = new User("John", "Doe", 28, "ABC123"); // creating new user
        User user2 = new User("Jane", "Doe", 31, "ABC121");
        User user3 = new User("Shawn", "Doe", 27, "ABC122");
       
        System.out.println("User info -- Name: " + user1.getFirstName() + " " + user1.getLastName() + "; ID: " + user1.getUserID() + "; Age: " + user1.getAge());
        System.out.println("User info -- Name: " + user2.getFirstName() + " " + user2.getLastName() + "; ID: " + user2.getUserID() + "; Age: " + user2.getAge());
        System.out.println("User info -- Name: " + user3.getFirstName() + " " + user3.getLastName() + "; ID: " + user3.getUserID() + "; Age: " + user3.getAge());
       
        user1.setAge(29); // updating age through setter
       
        System.out.println("User info -- Name: " + user1.getFirstName() + " " + user1.getLastName() + "; ID: " + user1.getUserID() + "; Age: " + user1.getAge()); // checking update
    }
}

Output:

User info -- Name: John Doe; ID: ABC123; Age: 28
User info -- Name: Jane Doe; ID: ABC121; Age: 31
User info -- Name: Shawn Doe; ID: ABC122; Age: 27
User #ABC123's age updated to 29
User info -- Name: John Doe; ID: ABC123; Age: 29

This worked like a charm. As you can see, we successfully created new users, displayed their information through getter functions, and updated one of their ages using a setter function. This should give you a good idea of how we can associate data and methods to models of real-world entities in our code and protect them from unwanted external interference.

What are the Advantages of Encapsulation?

Through these examples we discussed above, you must have noticed several advantages of working with encapsulation. Let’s list them all here —

Now let’s clear a common confusion among the popular OOP concepts.

Encapsulation, Inheritance, and Abstraction

Encapsulation is often confused with other object-oriented programming principles such as inheritance and abstraction. Before we contrast them against encapsulation one by one, let’s briefly describe these

Encapsulation: It is the bundling of data and methods into programmable units and preventing unwanted access.

Inheritance: Allows creating a hierarchy among classes in a way that enables “children” (sub) classes to inherit and share variables and methods from “parent” classes.

Abstraction: It is a general concept, not restricted to the confines of programming paradigms, that refers to the generalization drawn out of the details of reality.

Encapsulation vs. Inheritance

This is an easy one. Inheritance is the OOP principle that enables programmers to model hierarchies in their code — letting new classes inherit data and methods from existing ones by extending them and their functionality. For example, a Car class can extend (inherit from) a Vehicle class; a Cat class can extend an Animal class, etc.

On the other hand, encapsulation is the data/method packaging aspect of the OOP classes discussed so far.

Encapsulation vs. Abstraction

Encapsulation and abstraction, however, are more complementary in this regard.

In programming, abstraction is the hiding of the lower-level components and dealing with higher-level entities whose behaviors and interactions are easier to program and manage. Abstractions help application development on two fronts — 1) making it easier for end-users to interact with the application’s logic using clean user interfaces, and 2) enabling developers to model real-world entities (through classes/objects) and efficiently manipulate them as complete units.

Interestingly, the bundling and information hiding aspects of encapsulation in OOP constructs promote abstraction. It would even be reasonably accurate to say that abstraction is indeed a result of this OOP ability to encapsulate.

Before we close, let’s discuss a popular real-world application where you can see encapsulation at play but at a higher level.

How Encapsulation Relates to Containerization

So far in this post, our discussions have been more at the code (class/object/function/variable) level — the lower-level encapsulation that binds functions and variables to a class-like template.

Let’s talk about things on an application level. The principles of encapsulation are reflected in the container technology that, thanks to Docker, has taken the software and IT industries by storm.

Encapsulation in containers relates to packaging processes, services, and full-fledged applications into lightweight, portable, secure containers. These containers pack all the code, dependencies, and configurations required by your application into standalone executables that can effortlessly and reliably operate across different server environments. 

These independent, isolated environments consume minimal resources and prevent any setup issues otherwise caused by dependency version mismatches across different environments and with other applications. The containerization technology also follows the encapsulation principle in how it provides an abstraction over fully functional applications and can securely hide lower-level implementations.

You can learn more about Docker and its containers by reading the following posts on our blog —

Recap and Closing Thought

In this post, we plunged into the encapsulation principle of object-oriented programming. We started by defining the concept in the context of OOP constructs, discussed its bundling and information hiding aspects, and talked about access modifiers and getter/setter functions. We then looked at a couple of examples in Java to see encapsulation in action, followed by enlisting the advantages that it brings to the table. Towards the end, we compared it with other core principles of OOP and finally discussed a popular real-world application that benefits from encapsulation.

If you work with OOP languages, you have likely been using encapsulation all along in one way or another (you might not have known what it’s called). And if this is the first time you are coming across this, what are you waiting for? Go ahead and encapsulate everything you learned in this post into a pet project. Happy coding!

We have encapsulated all your application performance monitoring needs into Scout APM so you can spend more time building and less time debugging. Get started with a 14-day free trial (no credit card needed!).