Encapsulation in Java is one of the four pillars of OOP, along with Abstraction, Inheritance, and Polymorphism. While the term encapsulation has several meanings in general English, in computer science, it refers to the mechanism of binding data and the methods that operate on that data into a single unit called a class, while restricting direct access to the internal state of the object.
In object-oriented programming, encapsulation ensures that the internal implementation details of a class are hidden from the outside world. This is achieved using access modifiers and controlled access methods such as getters and setters.
By doing so, we protect the integrity of the object and prevent unauthorised or unintended modifications.
In this blog, we will explore everything in depth, using common examples, code snippets, and diagrams to make things as simple as possible without compromising on technical details.
Let’s start…
The process of tying methods and data together inside a class while limiting their direct access to the object’s internal state through access modifiers is called encapsulation in java.
In simple terms, encapsulation safeguards the internal data of an object by preventing uncontrolled external access.
But how?
As discussed in the introduction, one way to achieve Encapsulation in Java is by using access modifiers, as they can change the visibility and accessibility of data and members.
And the access modifier that does this thing the best is the private access modifier. When a variable or method is declared as private, it can only be accessed within the same class. No other class can directly access it. This restriction prevents unauthorised or unintended modification of data.
In the above image, the variable name and the method have been declared with a private access modifier, which makes them inaccessible from outside the class.
By using the private modifier, a class creates a secure boundary that ensures all internal variables are accessed only through authorised and controlled methods.
This prevents outside classes from directly tampering with the object’s state, maintaining its internal integrity. This concept of hiding internal data from outside classes is called data hiding.
IData hiding is a process of using private access modifiers to shield the internal state from external classes, enforcing access through controlled, authorised methods.
But encapsulation is not only about declaring variables as private; it is about designing classes in a way that their internal representation remains hidden and interaction happens only through a well-defined public interface.
Let’s understand encapsulation through a very common example – a pill and then we will move on to code examples.
Think of Encapsulation in Java as the “black box” of Java. To understand how it works, let’s look at a real-world classic: the medicine pill.
When we are sick, we swallow a pill that has a mix of many different chemicals tightly sealed in a plastic shell.
That little plastic shell isn’t just for show; it’s a functional barrier that will get dissolved only in our stomach, and the constituents get mixed up in the bloodstream and go where the problem is.
In Java terms, a class is that capsule. It wraps multiple “ingredients” (data and logic) into a single, easy-to-use unit. We can’t interact with the raw chemical powder directly; we can only interact with the capsule as a whole.
In this analogy:
By hiding the “how” (the chemicals) and only exposing the “what” (the medicine’s effect), we can protect our data from being corrupted.
In Java, this means users can only interact with the public methods rather than poking around in the private data of the code.
In the above sections, we have understood encapsulation in java in theory and real life analogy. Let’s understand it through code as well.
Till now, we have understood the concept of encapsulation in theory and with the help of a real-world example, it is time to understand this concept with code as well.
For this section of the blog, we have taken a scenario in which we will print the name and age of a human,
The following code contains a Human class with two variables, age and name which are declared but not initialised, and a Demo class in which we have initialised an object, obj, followed by the declaration of name and age variables using the obj reference. Finally, we have printed the result.
class Human {
int age;
String name;
}
public class Demo {
public static void main (String args[]) {
Human obj = new Human();
obj.age = 54;
obj.name = "Ateev";
System.out.println(obj.name + " is " + obj.age + " years old");
}
}
Let’s begin encapsulation by making the two variables private to prevent outside access to the Human class.
class Human {
private int age;
private String name;
}
public class Demo {
public static void main (String args[]) {
Human obj = new Human();
obj.age = 54;
obj.name = "Ateev";
System.out.println(obj.name + " is " + obj.age + " years old");
}
}
As expected, we are getting an error saying that the age and name variables have private access, meaning they can’t be accessed from the Demo class.
So, how to access them?
To access them, we will be creating some public methods – getAge() and getName() inside the Human class and print the result.
class Human {
private int age;
private String name;
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
public class Demo {
public static void main (String args[]) {
Human obj = new Human();
obj.age = 54;
obj.name = "Ateev";
System.out.println(obj.name + " is " + obj.age + " years old");
}
}
But why is the result still the same? That’s because now to access the variables, we have to use the method name instead of the variable names.
class Human {
private int age;
private String name;
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
public class Demo {
public static void main(String args[]) {
Human obj = new Human();
obj.age = 54;
obj.name = "Ateev";
System.out.print(obj.getName() + ": " + obj.getAge());
}
}
Once again we are getting the same error, but it’s not the same. Previously there were four errors – two of Human class and two of Demo class, but now the Demo class errors have been rectified.
Before understanding that lets first see what we did in the above code that resulted in error reduction. We have just replaced the variable names with the public method names in our print statement.
So instead of
System.out.println(obj.name + " : " + obj.age);
We have written
System.out.println(obj.getName() + " : " + obj.getAge());
By doing this we can maintain the integrity of the object’s state and ensure that the data is accessed and modified through a controlled and authorised interface.
And in future, we can change this part of the code without affecting the original code. This flexibility is essential for maintaining a robust and scalable application.
Now we were getting the two errors from our Demo class as we have initialised the two variables there, and they were private to begin with, which means we cannot change their value outside of the Human class. Let’s see what happens if we remove them from our code.
class Human {
private int age;
private String name;
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
public class Demo {
public static void main (String args[]) {
Human obj = new Human();
System.out.println(obj.getName() + " is " + obj.getAge() + " years old");
}
}
Hurray, the code is working, but the values we are getting are the default values of int and String data type as we have not initialized age and name variables. Let’s do that next.
There are two ways in which we can do that –
Let’s give them the same values as before. Name – Ateev and age – 54
class Human {
private int age = 54;
private String name = "Ateev";
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
public class Demo {
public static void main (String args[]) {
Human obj = new Human();
System.out.println(obj.getName() + " is " + obj.getAge() + " years old");
}
}
Now the values we are getting are not default.
But what if we somehow want to declare values like we usually do in any programming language – in the Demo or Main class? We can do that with slight modifications to our code.
For that, we will introduce two new public methods, setAge() and setName(), in the same class. These two methods will be responsible for updating the values of our variables, as we cannot do that from outside the class.
class Human {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}
public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
}
public class Demo {
public static void main(String args[]) {
Human obj = new Human();
obj.setAge(54);
obj.setName("Ateev");
System.out.print(obj.getName() + " is " + obj.getAge() + " years old");
}
}
According to us, this method is much easier and will be more useful in the future, as the variables have been declared outside the class and we can still access them.
So, we can modify the code to a greater extent than what we can in the earlier method where to change the values of the variables we have to go into the class which will alter some parts of the code. But with the latter way, that possibility is also gone.
These public methods are called getter and setter methods and play a crucial role in encapsulation, as we have just seen from the code. Let’s understand them in an easy-to-understand language.
These are a type of helper methods which are used to access and modify values of the data members that have been declared private.
The getter method is used to retrieve values, while the setter method is used to set or modify, or validate, the value of the variable.
In most of the codes we have seen above, in which we have introduced these getter and setter methods, we have used the get and the set words as the naming convention for getter and setter methods.
But, it is not compulsory to write the get or set word; we can use any name as long as we are following the method naming rules. Check the code below to understand.
class Human {
private int age;
private String name;
public int AAA() {
return age;
}
public void GGG(int a) {
age = a;
}
public String BBB() {
return name;
}
public void HHH(String n) {
name = n;
}
}
public class Demo {
public static void main(String args[]) {
Human obj = new Human();
obj.GGG(54);
obj.HHH("Ateev");
System.out.print(obj.AAA() + " is " + obj.BBB() + " years old");
}
}
In the above code, we have replaced –
With these replacements, the code is also working fine, and we are getting the same output. This means we can name our methods anything we want.
However, giving them random names is not a good habit because it can confuse anyone reading the code and, in some cases, even us. It is best to name them based on the type of code we are writing, such as getAge() and setAge().
Now that we have understood what encapsulation in java is and how it works with the help of code, it’s time to understand why we need it.
We need encapsulation because of the following points
We have already understood the concept of Data Hiding. It is used to restrict outside access using the private access modifiers, and only allows authorised and controlled access for those methods and variables that are of the same class.
By using getters and setters, we can specify exactly how the data in our class can be accessed or modified. Thus giving access only to a selected few
Code maintenance through encapsulation becomes much easier, as we can modify any part of the code without affecting the rest. Thus making our code flexible.
Now, let’s increase the difficulty bar of this blog to industry standards. The theory and code examples we have seen and understood above were just for understanding what encapsulation in java is and how to apply it.
This marks the end for our blog as in theory part. Let’s first have a small recap before moving on to the next section.
In all the above sections, we have understood encapsulation in java as: private access modifier + getters + setters
This was the school-level or textbook way of using encapsulation in java. But the real encapsulation is much more vast than that. In real-world software engineering, encapsulation means:
Let’s take an example from our above sections and understand it again (mainly where we fell short). The code is
class Human {
private int age;
private String name;
public int getAge() { return age; }
public void setAge(int a) { age = a; }
public String getName() { return name; }
public void setName(String n) { name = n; }
}
The above code will compile and give us output, but will the output always be the one we want? What if someone updates the value of age to -500? Can a person’s age be negative? No, so even when the code is compiling and giving us an output, it’s not always correct.
Check out the code below.
class Human {
private final String name;
private int age;
public Human(String name, int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
// Create a Human object
Human person = new Human("Alice", 25);
// Print initial details
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
}
}
In the above code
Now, what if we need to update the age variable? One way is by using setters, but that will give access to everyone to make changes as they see fit.
Another way is to update the value without the use of setters. Check the code below to understand this method.
class Human {
private String name;
private int age;
public Human(String name, int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public void celebrateBirthday() {
age++;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
// Create a Human object
Human person = new Human("Alice", 25);
// Print initial details
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
// Celebrate birthday
person.celebrateBirthday();
// Print updated age
System.out.println("After birthday, Age: " + person.getAge());
}
}
In the above code, we have added a behaviour through which we can update the value of the age variable without the setter method.
As the age variable has been declared private in the code, this behaviour must belong to the same class but can be called from outside classes. This behaviour does nothing special; it just increases the value of age by one when called.
So it’s like, instead of letting other code directly change internal variables, the class provides meaningful methods that control how state changes. And this is industry-level code, design and use of encapsulation in Java.
If we have to give this entire scenario a heading, we will go with Avoiding Unnecessary Setters,, as sometimes they act as a gateway through which anyone can update our private variables externally.
Just like this, there are many industry-level best practices that we must follow while writing code for encapsulation. Let’s see some of them.
Remember, there is no one way of writing code for strict encapsulation; one should adjust and adapt according to the needs of the project.
In the above section, we understood the basic difference between understanding encapsulation in java and applying it to make our app robust using the very example we used to understand encapsulation in the above sections.
The code that we used for understanding encapsulation is shown below.
class Human {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}
public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
}
public class Demo {
public static void main(String args[]) {
Human obj = new Human();
obj.setAge(54);
obj.setName("Ateev");
System.out.print(obj.getName() + " is " + obj.getAge() + " years old");
}
}
This code has too many loopholes, and we are literally telling the third party or outside class to come and make internal changes using external means.
One loophole that we fixed in our previous code was the usage of the unnecessary setter (setAge) that we have removed and used a behaviour instead to make the code updation complex, as now it won’t be that easy and is only possible through behaviour.
Let’s discuss some more best practices like this.
Currently, the code does not have a custom constructor; it’s working on the default constructor made by the compiler during compilation.
This allows the object to exist in an invalid or incomplete state, which can create a new edge case in which the object is broken and in the worst case scenario, we might have –
So, it’s important to create a custom constructor when we are dealing with fields that are required. Let’s add a custom constructor and update the code.
The custom constructor –
Now this line of code will –
public class Main {
public static void main(String[] args) {
Human person = new Human("Ateev", 54);
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
}
}
class Human {
private String name;
private int age;
public Human(String nameValue, int ageValue) {
if (nameValue == null || nameValue.isEmpty()) {
throw new IllegalArgumentException("Invalid name");
}
if (ageValue < 0) {
throw new IllegalArgumentException("Invalid age");
}
name = nameValue;
age = ageValue;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
In the above code, we have also covered any edge cases that we might encounter with the name variable, as the name can be set, or null, or the field can itself be left empty.
Our previous would have accepted the output with no name at all or the default value of the String data type, but now it won’t.
This is industry-level code, as in this we are not only hiding data but also controlling object lifecycle, which is very important in order to avoid any mishap later on.
In the above section, we have seen how to avoid unnecessary use of setters. In this section, we will use setters, as in some situations, it becomes necessary or unavoidable. But still, the edge case remains – what if someone says
Now age cannot be negative. So what’s the solution? The solution is very simple – let’s use an if statement saying that the value of the age variable must be between 0 and 100.
If for any reason it is either more than 100 or less than 0, we will get an error saying “Invalid age”. See the code below
public class Main {
public static void main(String[] args) {
Human person = new Human("Ateev", 54);
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
}
}
class Human {
private String name;
private int age;
public Human(String nameValue, int ageValue) {
if (nameValue == null || nameValue.isEmpty()) {
throw new IllegalArgumentException("Invalid name");
}
if (ageValue < 0 || ageValue > 100) {
throw new IllegalArgumentException("Invalid age");
}
name = nameValue;
age = ageValue;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int value) {
if (value < 0 || value > 100) {
throw new IllegalArgumentException("Invalid age");
}
age = value;
}
}
The current value of age lies between 0 and 100. But what if it was negative or beyond 100? Let’s check if the if statement we have written is working/
So, our if statement is working as expected. But we have also written the same logic inside the setAge() method. Why/
The if statement that was inside the Human() method was to check and validate whether the initial value we have given to the age variable fulfils the condition.
While the if condition in the setAge() method checks whether the value of the age variable still lies in the range, if we modify it, as if it doesn’t, the object would now contain invalid data. Let’s see if that really does happen.
Let’s add a line in our Main method just below the constructor – person.setAge(1000);
And try to run the code.
We have received a runtime exception, the same error we received above. But check out the line number that threw the error. In this case, it’s line number 39, the same line in which our setAge() logic is written, while in the above error message, it was line number 4 – the line in which our constructor was declared.
The IllegalArgumentException is a subclass of java.lang.RuntimeException, and happens when a method is passed illegal or unsuitable arguments, as in our code.
Meaning that the constructor triggered the previous error, and this error has been triggered from our setAge() method, which proves that the code will even throw an error if the modified value of the age variable is less than 0 or more than 100.
Before understanding this, let’s first understand what coupling is. Coupling means how much one class depends on the internal details of another class.
In Java, tight coupling occurs when classes are so deeply intertwined that they lose their independence. Simply said, when classes are overly dependent on each other’s internal details, they are called tightly coupled classes.
Imagine two gears welded together; if one gets stuck, the whole machine breaks.
Technically, this happens when:
In our initial example, the `Human` class only contains two variables: `name` and `age`. These variables are declared as `private`, which means other classes cannot access them directly.
At the moment, this seems simple because there are only two pieces of data. However, in real programs, a class may contain many variables. Other classes may need to read or change some of this information in order to perform their tasks.
We know from the above paragraph that when classes depend too heavily on each other’s internal data, the program becomes tightly coupled.
This means that one class relies strongly on the internal structure of another class. If the internal data changes, other parts of the program may also need to be changed.
Encapsulation helps reduce this problem by controlling how data is accessed. Instead of allowing direct access to variables, we provide controlled methods (such as getters and setters) to read or modify the data.
This creates looser coupling between classes, because they interact through well-defined methods rather than relying on the internal details of the class.
So the updated code is as follows –
class Human {
private int age;
private String name;
public Human(String n, int a) {
if (n == null || n.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
if (a < 0 || a > 130) {
throw new IllegalArgumentException("Age must be between 0 and 130");
}
name = n;
age = a;
}
public String introduce() {
return name + " is " + age + " years old";
}
}
public class Demo {
public static void main(String[] args) {
Human person = new Human("Ateev", 54);
System.out.println(person.introduce());
}
}
Notice Something -:
Now, how to read or modify the variables through a behaviour.
class Human {
private int age;
private String name;
public Human(String n, int a) {
setName(n);
setAge(a);
}
public String introduce() {
return name + " is " + age + " years old";
}
public void setName(String n) {
if (n == null || n.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
name = n;
}
public void setAge(int a) {
if (a < 0 || a > 130) {
throw new IllegalArgumentException("Age must be between 0 and 130");
}
age = a;
}
}
public class Demo {
public static void main(String[] args) {
Human person = new Human("Ateev", 54);
System.out.println(person.introduce());
person.setName("Tarun");
person.setAge(32);
System.out.println(person.introduce());
}
}
In the above code, in order to change the value of the name and age variable is using, the setAge() and setName() public methods and not through the internal structure.
There are many more best practices when it comes to encapsulation, but again, no one method should be used everywhere. Which method or practice will be used and where will be decided by us after reviewing the code and what we are building.
Finally, encapsulation is a concept that not only hides data but also protects an object’s internal state from uncontrolled external access.
A common approach to implementing encapsulation in Java is to declare fields as private, which ensures that they cannot be accessed directly from outside the class. This allows the class to control how its internal data is read or modified.
Accessing these fields is another challenge for us, as it is typically done through controlled methods, such as getters, setters, constructors, or other behaviour-oriented methods defined by the class.
While getters and setters are widely used, relying only on them may weaken encapsulation. Unrestricted setters can allow external code to modify an object’s state in unintended ways, potentially introducing invalid data or inconsistent states.
Therefore, it is important to follow good design practices such as
So that the class can maintain control over its internal state while remaining flexible and maintainable.
Ans. Encapsulation hides the internal details of an object by bundling data and methods together, while abstraction hides complexity by showing only the essential features and hiding the background details.
Ans. Without encapsulation, data can be easily accessed or modified from outside the class, leading to security risks, less control over data, and potential errors that are harder to trace.
Ans. Yes, methods can be encapsulated by making them private, so they can only be accessed within the class, or protected, so they are accessible only within the package or by subclasses.
Ans. Design patterns like Singleton, Factory, and Builder rely heavily on encapsulation to manage object creation and ensure that the internal object state is not exposed or modified directly, while APM solutions like Retrace use encapsulation for error detection.
Ans. Yes, it is entirely possible, and it will break the whole purpose of encapsulation. According to one post on Stack Exchange, the point of encapsulation is not that you should not be able to know or to change the object’s state from outside the object, but that you should have a reasonable policy for doing it.
Having getters and setters does not break encapsulation; what breaks encapsulation is automatically adding a getter and a setter for every data member (every field, in Java lingo), without giving it any thought. While this is better than making all data members public, it is only a small step away
Inheritance in Java is a mechanism of creating a new class or interface from an…
In this blog, we will not only understand the concepts of OOPS in Java in…
Object-Oriented Programming System (OOPS) is a programming paradigm built around the concept of objects —…
Abstraction in Java is one of the four pillars of OOPs which is used to…
In this blog, we will learn How to Detect a Click Outside of a React…
learn How to Use Hooks to Create Infinite Scrolling in React by making a custom…