Index

Encapsulation

Java Object-Oriented Design

3.1 Access Modifiers (private, public, protected, default)

Access modifiers in Java are fundamental to the concept of encapsulation, one of the core principles of object-oriented programming. They control the visibility of classes, fields, methods, and constructors, defining which parts of a program can access or modify them. Proper use of access modifiers helps create secure, maintainable, and well-structured code by restricting direct access to internal implementation details.

The Four Access Levels in Java

Java provides four access levels: private, public, protected, and the default (also called package-private or no modifier). Each modifier controls visibility differently:

private

Example:

public class BankAccount {
    private double balance; // hidden from other classes

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    public double getBalance() {
        return balance;
    }
}

Here, balance cannot be accessed directly outside BankAccount; it’s only modifiable through controlled methods, protecting the integrity of the data.

public

Example:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

Anyone can call add() because it is public.

protected

Example:

public class Animal {
    protected String species;

    protected void makeSound() {
        System.out.println("Some generic sound");
    }
}

public class Dog extends Animal {
    public void bark() {
        makeSound();  // Allowed because of protected access
        System.out.println("Bark!");
    }
}

Here, species and makeSound() are accessible to Dog, even if it’s in a different package.

Default (Package-Private)

Example:

class Helper {
    void assist() {
        System.out.println("Assisting...");
    }
}

public class Service {
    public void execute() {
        Helper helper = new Helper();
        helper.assist();  // Allowed, same package access
    }
}

Note: The Helper class and its assist() method have no explicit modifier, so they are package-private.

Why Access Control Is Vital for Encapsulation

Encapsulation is about hiding internal details and exposing only what is necessary. Access modifiers enforce this by:

Package-Level Access and Inheritance

Java’s package system groups related classes, and access modifiers work closely with it:

Summary Example Demonstrating All Modifiers

package com.example;

public class Vehicle {
    private String engineNumber;     // Accessible only inside Vehicle
    protected int maxSpeed;          // Accessible in subclasses and package
    String model;                    // Package-private: accessible in same package
    public String brand;             // Accessible everywhere

    private void startEngine() {
        System.out.println("Engine started.");
    }

    protected void accelerate() {
        System.out.println("Accelerating...");
    }

    void displayModel() {
        System.out.println("Model: " + model);
    }

    public void showBrand() {
        System.out.println("Brand: " + brand);
    }
}

Conclusion

Access modifiers are a vital part of Java’s approach to encapsulation. By carefully choosing private, protected, public, or default access, you can design classes that hide their internal workings, expose clear interfaces, and support flexible, secure, and maintainable codebases. Mastery of these modifiers sets the foundation for building professional-grade Java applications.

Index

3.2 Getters and Setters

In Java, getters and setters are methods that provide controlled access to the private fields of a class. They are essential tools for encapsulation, allowing you to hide the internal data of an object while still providing a way to read or modify it safely.

The Role of Getters and Setters

When you declare fields as private to protect the object’s internal state, other classes cannot access or modify those fields directly. To enable controlled interaction, you create getter methods to retrieve the field values and setter methods to update them.

This approach ensures:

Standard Syntax and Naming Conventions

By convention, getter and setter methods follow a simple naming pattern:

Field type Getter method name Setter method name
Any type (except boolean) getFieldName() setFieldName(Type value)
Boolean isFieldName() or getFieldName() setFieldName(boolean value)

Example: Basic Getter and Setter

public class Person {
    private String name;
    private int age;

    // Getter for name
    public String getName() {
        return name;
    }

    // Setter for name
    public void setName(String name) {
        this.name = name;
    }

    // Getter for age
    public int getAge() {
        return age;
    }

    // Setter for age
    public void setAge(int age) {
        this.age = age;
    }
}

Adding Validation Logic in Setters

One of the biggest benefits of using setters is the ability to validate data before updating a field. This ensures the object remains in a consistent, valid state.

public class Person {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age < 0) {
            System.out.println("Age cannot be negative.");
        } else {
            this.age = age;
        }
    }
}

Now, setting a negative age will not corrupt the object:

Person p = new Person();
p.setAge(-5);  // Outputs: Age cannot be negative.

Read-Only and Write-Only Properties

Depending on design requirements, some fields should only be readable or writable:

public class Employee {
    private final String employeeId;

    public Employee(String id) {
        this.employeeId = id;
    }

    public String getEmployeeId() {
        return employeeId;
    }
}
public class SecureData {
    private String password;

    public void setPassword(String password) {
        this.password = password;
    }
}
Click to view full runnable Code

public class Main {
    public static void main(String[] args) {
        // Example: Basic Person with validation
        Person person = new Person();
        person.setName("Alice");
        person.setAge(30);
        System.out.println("Name: " + person.getName());
        System.out.println("Age: " + person.getAge());

        person.setAge(-5); // Triggers validation warning

        // Example: Read-only employee ID
        Employee emp = new Employee("EMP123");
        System.out.println("Employee ID: " + emp.getEmployeeId());

        // Example: Write-only secure data
        SecureData data = new SecureData();
        data.setPassword("s3cr3t");  // No way to read it from outside
    }
}

class Person {
    private String name;
    private int age;

    // Getter and Setter with validation
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age < 0) {
            System.out.println("Age cannot be negative.");
        } else {
            this.age = age;
        }
    }
}

class Employee {
    private final String employeeId;

    public Employee(String id) {
        this.employeeId = id;
    }

    public String getEmployeeId() {
        return employeeId;
    }
}

class SecureData {
    private String password;

    public void setPassword(String password) {
        this.password = password;
    }
}

Summary

Getters and setters are vital for controlling access to class fields, allowing you to:

Mastering getters and setters lays the groundwork for writing safe, encapsulated Java classes that promote robust software design. In the next section, we will explore how to create immutable objects, which take encapsulation even further by preventing changes to the object's state after creation.

Index

3.3 Immutable Objects

What Is Immutability?

In Java and object-oriented design, an immutable object is an object whose state cannot be changed after it is created. Once you set the values of its fields, those values remain constant throughout the object's lifetime.

Immutability is a powerful design concept that promotes simplicity, safety, and predictability in software development.

Advantages of Immutability

How to Create Immutable Classes in Java

To make a class immutable, follow these guidelines:

  1. Declare the class as final (optional but recommended): Prevents subclassing which might alter immutability.
  2. Make all fields private and final: Fields cannot be reassigned after initialization.
  3. Do not provide setter methods: No way to modify fields after construction.
  4. Initialize all fields via constructor: Set all field values once at object creation.
  5. If fields reference mutable objects, ensure defensive copies: Return copies or immutable views instead of the original references.
  6. Provide only getters: Methods that expose field values without allowing modifications.

Example: The Immutable String Class

Java’s built-in String class is a classic example of an immutable class. Once a String object is created, its value cannot be changed.

String greeting = "Hello";
greeting.toUpperCase(); // Returns a new String; original is unchanged
System.out.println(greeting); // Prints "Hello"

Calling methods like toUpperCase() returns new String instances rather than modifying the original, ensuring immutability.

Creating a Custom Immutable Class

Here’s an example of a simple immutable Person class:

public final class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

Because the class and fields are final, and no setters are provided, once a Person object is created, its name and age cannot be changed.

Person p = new Person("Alice", 30);
// p.name = "Bob"; // Compile error: cannot assign a value to final field

Defensive Copies for Mutable Fields

If your class contains fields that refer to mutable objects (like arrays or collections), you should return copies to maintain immutability:

public final class Team {
    private final List<String> members;

    public Team(List<String> members) {
        // Create a new list to prevent external modification
        this.members = new ArrayList<>(members);
    }

    public List<String> getMembers() {
        // Return a copy to prevent caller from modifying internal list
        return new ArrayList<>(members);
    }
}
Click to view full runnable Code

import java.util.ArrayList;
import java.util.List;

public final class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }

    public int getAge() { return age; }
}

final class Team {
    private final List<String> members;

    public Team(List<String> members) {
        // Defensive copy to protect internal state
        this.members = new ArrayList<>(members);
    }

    public List<String> getMembers() {
        // Return a copy to maintain immutability
        return new ArrayList<>(members);
    }
}

public class Main {
    public static void main(String[] args) {
        // Immutable Person
        Person p = new Person("Alice", 30);
        System.out.println(p.getName() + ", age " + p.getAge());

        // Mutable list outside
        List<String> list = new ArrayList<>();
        list.add("Bob");
        list.add("Carol");

        Team team = new Team(list);
        System.out.println("Team members: " + team.getMembers());

        // Modify external list after creation
        list.add("Dave");
        System.out.println("After modifying original list:");
        System.out.println("Team members: " + team.getMembers());

        // Try modifying the returned list from getter
        List<String> membersFromGetter = team.getMembers();
        membersFromGetter.add("Eve");
        System.out.println("After modifying getter's list:");
        System.out.println("Team members: " + team.getMembers());
    }
}

Thread-Safety Benefits

Immutable objects are naturally thread-safe because:

Summary

Immutability is a key design strategy that enhances reliability and safety in Java applications. By creating immutable classes with final fields, no setters, and proper encapsulation, you gain benefits such as:

Index

3.4 JavaBeans and Encapsulation in Practice

What Are JavaBeans?

JavaBeans are a widely used convention in Java programming that defines a standard way to create reusable software components. The JavaBeans specification sets guidelines on how classes should be designed to work smoothly with development tools, frameworks, and libraries. Understanding JavaBeans is important because many Java technologies—like GUI builders, enterprise frameworks, and persistence libraries—expect components to follow these conventions.

Key JavaBeans Conventions

A JavaBean class typically follows these rules:

  1. Private fields: All properties are declared as private to enforce encapsulation.
  2. Public getter and setter methods: Each property has public methods named following a getXxx() / setXxx() pattern for accessing and modifying values.
  3. No-argument constructor: The class provides a public default constructor without parameters, allowing tools to instantiate the bean easily.
  4. Serializable: Implements the Serializable interface to allow objects to be converted into a byte stream for storage, communication, or caching.

Properties in JavaBeans

A property represents a logical attribute or characteristic of a JavaBean. The property name corresponds to the field name, and access is provided by getters and setters. For example, a field named age would have:

public int getAge() { return age; }
public void setAge(int age) { this.age = age; }

This standardized naming convention enables automated tools and frameworks to manipulate bean properties without needing to know their internal implementation.

Example JavaBean Class

Here’s a simple example of a JavaBean called Student:

import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private int grade;

    // No-argument constructor
    public Student() {
    }

    // Getter for name property
    public String getName() {
        return name;
    }

    // Setter for name property
    public void setName(String name) {
        this.name = name;
    }

    // Getter for grade property
    public int getGrade() {
        return grade;
    }

    // Setter for grade property
    public void setGrade(int grade) {
        if (grade >= 0 && grade <= 100) {
            this.grade = grade;
        }
    }
}

This class meets the JavaBeans standards: it has private fields, public getters/setters, a no-arg constructor, and implements Serializable.

How JavaBeans Support Encapsulation and Practical Development

JavaBeans put encapsulation into practice by enforcing private fields and controlled access through getters and setters. This hides internal data and allows validation or logic to be inserted in setters, helping maintain object integrity.

Moreover, the conventions facilitate:

Summary

JavaBeans offer a practical embodiment of encapsulation principles with standardized patterns for fields, constructors, and methods. They are an integral part of professional Java development, ensuring your classes work well with many tools and frameworks while promoting safe, maintainable, and reusable code. As you progress, you’ll frequently encounter JavaBeans, especially in enterprise and GUI applications, making mastery of this pattern invaluable.

Index