1. Definition of encapsulation
Encapsulation is one of the fundamental principles of object-oriented programming (OOP). In simple terms, encapsulation is the ability to hide an object’s internals (its “guts”) and give access to them only through specially provided “doors” — public methods.
Imagine a modern coffee machine. The user sees only the buttons and the display — they don’t need to know how the boiler, pump, and tubes are arranged inside. They press “Cappuccino” — and get the result. Everything inside is hidden. That’s encapsulation!
In Java (and other OOP languages), encapsulation is achieved by:
- Data hiding — class fields are declared as private (or at least not public).
- Public interface — only those methods that are really needed by the object’s user are “exposed”.
Diagram: what encapsulation looks like
+-------------------------------+
| Class Student |
|-------------------------------|
| - name: String | // private field
| - age: int | // private field
|-------------------------------|
| + getName(): String | // public method
| + setName(String): void | // public method
| + getAge(): int | // public method
| + setAge(int): void | // public method
+-------------------------------+
Here the sign - means private (hidden), and + means public (accessible from outside).
What are getters and setters?
Before we figure out why encapsulation is needed, let’s quickly get acquainted with getters and setters — these are special methods that help us “communicate” with private fields of a class.
Getter — a method that retrieves the value of a private field. Typically called getNameField().
Setter — a method that sets the value of a private field. Typically called setNameField(value).
Simple example:
public class Student {
private String name; // private field — not visible from outside
// Getter — "give me the student's name"
public String getName() {
return name;
}
// Setter — "set the student's name"
public void setName(String name) {
this.name = name;
}
}
How it works:
Student student = new Student();
student.setName("John"); // set the name via the setter
String name = student.getName(); // get the name via the getter
Think of getters and setters as “polite requests” to an object: instead of reaching into its pockets (student.name = "John"), we politely ask: “Please set the name” (student.setName("John")).
Looking ahead: in a couple of lectures we’ll study getters and setters in detail, learn their secrets, and master them. For now, it’s enough to understand the core idea!
2. Why do we need encapsulation?
Protecting data from incorrect use
If all fields of a class were public (public), any external code could change their values directly, however it pleased:
Student s = new Student();
s.age = -1000; // Oops, a vampire student!
This is dangerous! Your program may start behaving unpredictably, and bugs will appear in the most unexpected places.
The ability to change the internal implementation without affecting external code
Encapsulation allows you to change a class’s internal structure without breaking the code that uses it. For example, you can change how data is stored or add validation in methods, and users of the class won’t notice — they still call the same methods.
Improved readability and maintainability
When all internal details are hidden, the external interface becomes cleaner and easier to understand. A developer using your class doesn’t need to understand how it works inside — they just need to know which methods are available and what they do.
Real-life example
Think about how you use a smartphone. You don’t think about exactly how touch events are processed, how the battery is built, or how the radio module works. You simply call the functions you need through a clear interface (icons, buttons). If the manufacturer changes the internal implementation, you won’t even notice.
Real example in code
Imagine we have a BankAccount class. In the old version of the program, the balance was stored as a string with dot separators, for example "1.000.50". Then the developers decided to store the balance as a double. If the field were public, all the old code that directly accessed account.balance would break.
But if we use encapsulation and hide the field, providing only deposit() and getBalance() methods, external code won’t even notice the changes:
public class BankAccount {
private double balance; // field is hidden
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
Now if tomorrow we want to store the balance, say, in cents (long), we just change the class’s internal implementation, and all the other code that calls deposit() and getBalance() will keep working as before.
3. Examples of bad and good encapsulation
Bad example: public fields
public class Student {
public String name;
public int age;
}
Problems with this approach:
- Any code can assign any values to the fields, even invalid ones.
- No ability to add data validation.
- If you decide to change the type or structure of a field, you’ll have to change all the code that uses it.
Good example: private fields and public methods
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
// You can add validation!
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// Check that age is non-negative
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.age = age;
}
}
Advantages:
- External code cannot change fields directly — only through methods.
- You can add checks, logs, and automatic actions (for example, updating statistics).
- If the internal representation changes (for example, age is stored in a different format), the external interface remains the same.
What it looks like in use
Student s = new Student();
s.setName("Alice");
s.setAge(20);
System.out.println(s.getName() + ", age: " + s.getAge());
Try assigning a negative age — you’ll get an error at runtime! Your program is protected from silly mistakes.
4. Relation to other OOP principles
Encapsulation is the “mother” of all other OOP principles. Without it, there would be no inheritance, polymorphism, or abstraction. We’ll study them a bit later, but for now we’ll briefly mention them:
- Inheritance (extends) allows you to create new classes based on existing ones, extending or changing their behavior. If a class’s internals were open, a subclass could accidentally break something important.
- Polymorphism (the ability of objects of different classes to respond to the same messages in different ways) is impossible without a clear separation between internal implementation and external interface.
- Abstraction is highlighting only the essential characteristics of an object and hiding the details. Encapsulation helps implement abstraction in practice.
Analogy
Imagine a car. The driver has access only to the steering wheel, pedals, and levers — that’s the interface. Everything else (engine, transmission, electronics) is hidden under the hood. If the driver could directly control every screw of the engine, accidents would be much more common!
5. Practical example: encapsulation in our app
Let’s continue developing our learning app — for example, an “Address book”. Suppose we have a Contact class that stores a name and a phone number.
Without encapsulation (anti-example):
public class Contact {
public String name;
public String phone;
}
Usage:
Contact contact = new Contact();
contact.name = ""; // Oops! The name is empty
contact.phone = null; // Phone number is not set
With encapsulation (the right approach):
public class Contact {
private String name;
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Contact name cannot be empty");
}
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
if (phone == null || phone.isBlank()) {
throw new IllegalArgumentException("Phone number cannot be empty");
}
this.phone = phone;
}
}
Now external code won’t be able to leave the name or the phone number empty:
Contact contact = new Contact();
contact.setName("Ivan");
contact.setPhone("+1-999-123-45-67");
If you try to set an empty name, the program will throw an error.
6. Useful nuances
Encapsulation and long-term code maintenance
When you’re working on a small learning project, it might seem you can do everything “on a handshake”: who would assign a negative age or an empty name, right? But as soon as the project grows, other developers join, and even you yourself forget the implementation details after a couple of months — that’s when encapsulation saves you from chaos.
- It’s easy to change a class’s internals — if you need to store the phone as an object of type PhoneNumber instead of a string, you just change the implementation and don’t touch the external code.
- Easier to test — if all changes happen only through methods, it’s easy to track which data changes and when.
- Fewer bugs — protection from invalid values and accidental changes.
Question: do we always need getters and setters?
Beginners often think: “Since encapsulation is private fields and public getters/setters, we must create a getter and setter for every field!” That’s not quite right.
- Sometimes a field should be read-only (for example, a unique object identifier). Then provide only a getter.
- Sometimes a field doesn’t need to be “exposed” at all — then provide neither a getter nor a setter.
- A setter can be made private if a field’s value should change only inside the class.
Golden rule: expose only those data and methods that are truly needed by external code.
Visualization: comparing approaches
| Approach | Example of field access | Ability to control | Security |
|---|---|---|---|
| public fields | |
No | Low |
| private fields + methods | |
Yes | High |
7. Common mistakes when working with encapsulation
Mistake #1: All class fields are declared public. This is the most common beginner mistake. Such code quickly becomes unmanageable: anyone can change any data without your knowledge. Don’t do this — even if you really want to save time!
Mistake #2: Getters and setters without checks or logic. If you create accessors, use them for validation: don’t allow assigning invalid values. Simply “copying” a value from a parameter to a field isn’t always the best option.
Mistake #3: Premature exposure of internal structure. If you create getters/setters for all fields “just in case” in advance, you risk exposing too many details that will be hard to change later.
Mistake #4: Returning mutable objects directly. If a field is a mutable object (for example, a list), don’t return it directly from a getter. It’s better to return a copy or make it immutable.
GO TO FULL VERSION