Encapsulation in Java is one of the four pillars of OOPS, along with Abstraction, Inheritance, and Polymorphism. While the term encapsulation has several meanings in general English, such as enclosing something within a capsule, wrapping, or packaging, in computer science, it refers to the mechanism of binding data and the methods that operate on that data into a single unit, while restricting direct access to the internal state of the object.
In object-oriented programming, encapsulation ensures that a class’s internal implementation details 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…
Index
What is Encapsulation in Java?
Encapsulation in Java is the process of tying methods and data together inside a class while limiting direct access to the object’s internal state through access modifiers.
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 has been declared with a private access modifier, which cannot be accessed directly from outside the class.
This ensures authentic, authorised, secured and controlled communication between all the methods and variables in the class. This concept of hiding internal data from outside classes is called data hiding.
Data Hiding
It is a process of hiding internal data from outside classes by preventing its direct access using the private access modifier, ensuring that it can only be accessed either from within the same class or from controlled 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.
Example Time !!!
Now that we have understood encapsulation from a technical point of view, let us understand it using a real-world analogy.
Consider a medicine capsule that we take when we fall sick.
A capsule can be compared to a class in Java. It contains a mixture of different chemical components sealed inside it, and we have no idea which chemicals are inside and in what proportion, yet we swallow it as it is the only way to consume the medicine.
We cannot open the capsule, modify its contents, or interact with the chemicals directly. We simply trust that it will produce the desired effect once consumed.

In this analogy:
- The capsule represents the class.
- The chemical mixture inside represents the data (variables).
- The effect of the medicine represents the methods (behaviour).
- The outer protective layer represents data hiding.
We interact with the capsule as a whole, without knowing or interfering with its internal composition. Similarly, in Java, we interact with an object through its public methods without directly accessing or modifying its internal data.
This controlled way of interacting with an object, while keeping its internal details hidden, is what encapsulation is all about.
Understanding Encapsulation in Java with the help of the code.
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.
The following code contains a Human class with two variables, age and name, which are initialised but not declared, and a class Demo in which we have initialised an objectobj that will give the two parameters their values. 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");
}
}
Output

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");
}
}
Output

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 inside the same 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");
}
}
Output

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());
}
}
Output

Once again, we are getting the same error, but it’s not the same. As previously, there were four errors – two of the Human class and two of the Demo class, but now the Demo class errors have been rectified. But how and why?
Before answering these questions, let’s understand 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");
}
}
Output

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 –
- Declaring and initialising the variables at the same time
- Declaring the variables in the Human class and initialising them in the Demo class.
Declaring and initialising the variables at the same time
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.
Output

Declaring the variables in the Human class and initialising them in the Demo class.
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");
}
}
Output

If you ask 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.
Getter and Setter Public Methods for Encapsulation in Java
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 extract the values, while the setter method is used to set or modify the value of the variable. It is not compulsory to write the getor 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");
}
}
Output

In the above code, we have replaced –
- getAge() with AAA()
- setAge() with GGG()
- getName() with BBB()
- setName() with HHH()
With these replacements, the code is also working fine, and we are getting the same output. This means we can name our methods anything, but it’s not a good habit to give some random names, as this might confuse anyone who is reading the code and, in some cases, even us, so it is better to name them as per the type of code we are writing, like getAge() and setAge().
Why do we need Encapsulation in Java?
Now that we have understood what encapsulation 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
Data Protection
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.
Controlled Access
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:
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.
Understanding Encapsulation in Java, the professional way
Now, let’s increase the bar of this blog to industry standards. The theory and code examples we have seen and understood above were just for understanding what encapsulation is and how to apply it.
Now it’s time to understand when to apply it, whether it is necessary to apply it, and various other questions. Understand it this way, in the above sections, we only understood encapsulation like – private access modifier + getters + setters, this was school-level stuff, just to briefly explain what encapsulation is. But the real encapsulation is much more vast than that. In real-world software engineering, encapsulation means:
- Protecting invariants
- Designing behaviour, not data containers
- Preventing invalid states
- Reducing coupling
- Making objects responsible for their own logic
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());
}
}
Output

In the above code
- There is no setAge method, but it was there in the previous code
- The value of age cannot be set to anything; it has to be a positive number
- Object controls its own logic
- In the previous code, the data could be changed very easily, as we are not using the getters and setters properly. The above code is like a read-only data holder, as there are no setters to update the value.
Now, what if we need to update the variable without setters? Check the code below.
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());
}
}
Output

Now, in this code, we have added a behaviour through which we can update the value of age. Previously, anyone who has the access to the code could update the value of age through external classes, as we had the setAge method in place. But now, only through the behaviour can the value of the age be updated.
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.
Industrial best practices for Encapsulation in Java
In the above section, we understood the basic difference between understanding encapsulation 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 was 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.
Use constructors to enforce a valid object state
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 –
- Incorrect Data
- Hard-to-trace bugs
- NullPointerExceptions
- Inconsistent system state
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 –
- Forces values at object creation.
- Validates inputs before assignment.
- Prevents invalid objects from ever existing.
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;
}
}
Output

Now, 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.
Guarding Object Integrity
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 age cannot be more than 100 and less than 0. If it throws an exception 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;
}
}
Restrict Setter Visibility When needed
We have learnt about many ways in which we can enforce some rules on state change, like
- The value of age cannot be negative
- The value of the name cannot be either null or empty
- Using behaviour instead of a setter
And many more, but what if there is an easier way to handle situations like these (edge cases)? Our main aim is to restrict access to the variable, and for that, we have declared it with a private access modifier, but we have also given the public methods getters and setters to read and update the value whenever possible.
With the current setter method, anyone can update the value of the variables. Can we declare these public methods with the private access modifier? The answer is yes, we can. See the image below.

Now,
- Not everyone will be able to see or access the setter method
- Only the class can modify the field
- We have reduced accidental corruption significantly
But again, we think removing the setter logic completely will be the best choice, as it will make our code thread-safe as well.
Use encapsulation to reduce coupling
Before understanding this, let’s first understand what coupling is. Coupling means how much one class depends on the internal details of another class. So when we use the phrase tight coupling, it generally means that
- A class knows too much about another class
- A class depends upon the implementation details rather than the behaviour.
- A small change in one class will also be reflected in others.
So now, in our initial code, the one which was the textbook version of the ideal code, the Demo class knows that the Human class has age and name variables which have been declared with a private access modifier and also knows how to combine them in a sentence. Isn’t it too many details shared between classes?
In any normal code, it would have been normal to share data, but when we are talking about encapsulation, it is not a good sign, as any change made in the Demo class (external class) will be reflected in the Human class (internal class).
So the updated code is as follows –
class Human {
private int age;
private String name;
public Human(String n, int a) {
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());
}
}
Output

Notice Something -:
- We have used a custom constructor
- There are no getters, setters and behaviour in the above meaning; we cannot change the value of the age and name variables. Once they are set thay are set.
We can make it such that the state also updates by adding a behaviour, but for now, let’s leave it at that, as our main aim was to create code that reduces coupling.
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.
Conclusion
At the end, we would just like to say that encapsulation is a process of not only hiding the data but also protecting it from outside access. For that, the best way is to declare the field (method, variable, etc) with a private access modifier. This will ensure that the fields can only be accessed by the other members of the same class.
The only way to access these variables is by using the public getters and setters methods, as shown in the blog. But that’s the textbook way of encapsulating our app and will lead to many edge cases and loopholes.
Like anyone can access and update the value of a field through getters and setters, and set or update the value to something that can potentially break the code. For that, there are best practices that must be followed, though not strictly, as different situations might need different approaches to handle edge cases, but yeah, more or less, at least they should be kept in mind.
Frequently Asked Question
Q1. What is the difference between encapsulation and abstraction?
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.
Q2. What happens if we don’t use encapsulation in a Java program?
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.
Q3. Is it possible to encapsulate a method in Java?
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.
Q4. What are the design patterns that heavily rely on encapsulation?
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.
Q5. Is it possible to overuse the Getter and Setter methods?
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

