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.
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.
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.
Encapsulation is about hiding internal details and exposing only what is necessary. Access modifiers enforce this by:
private
), you prevent unwanted changes that could corrupt the object state.Java’s package system groups related classes, and access modifiers work closely with it:
protected
members and can access them even across packages, while default members are inaccessible outside the package.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);
}
}
engineNumber
is tightly guarded to protect critical data.maxSpeed
can be used or overridden by subclasses.model
is package-private, so only classes in com.example
can see it.brand
is public for anyone to access.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.
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.
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:
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) |
void
and accept one parameter to assign to the field.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;
}
}
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.
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;
}
}
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;
}
}
Getters and setters are vital for controlling access to class fields, allowing you to:
private
fields.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.
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.
To make a class immutable, follow these guidelines:
final
(optional but recommended): Prevents subclassing which might alter immutability.private
and final
: Fields cannot be reassigned after initialization.String
ClassJava’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.
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
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);
}
}
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());
}
}
Immutable objects are naturally thread-safe because:
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:
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.
A JavaBean class typically follows these rules:
getXxx()
/ setXxx()
pattern for accessing and modifying values.Serializable
interface to allow objects to be converted into a byte stream for storage, communication, or caching.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.
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
.
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:
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.