Skip to main content

Access Modifiers & Visibility

In Java, access modifiers are keywords that set the accessibility (visibility) of classes, constructors, methods, and fields. They are a fundamental aspect of encapsulation, an object-oriented programming principle that allows you to hide internal implementation details and control how other parts of the code can interact with your classes and their members.

There are four types of access modifiers in Java:

  1. public
  2. protected
  3. default (or package-private)
  4. private

These modifiers specify who can access a member: the class itself, other classes in the same package, subclasses (even in different packages), or all classes.


1. public

The public access modifier is the most permissive. When a class, method, or field is declared public, it means it is accessible from anywhere in the application.

  • Classes: A public class can be accessed by any other class.
  • Methods/Fields/Constructors: public members can be accessed from any class, regardless of the package.

Visibility: Accessible everywhere.

Example:

// package com.example.model
package com.example.model;

public class PublicClass { // Public class
public int publicField = 10; // Public field

public PublicClass() { // Public constructor
System.out.println("PublicClass constructor called.");
}

public void publicMethod() { // Public method
System.out.println("This is a public method.");
}
}

// package com.example.app
package com.example.app;

import com.example.model.PublicClass;

public class AccessPublic {
public static void main(String[] args) {
PublicClass obj = new PublicClass(); // Accessible
System.out.println(obj.publicField); // Accessible
obj.publicMethod(); // Accessible
}
}

2. protected

The protected access modifier allows members to be accessed within:

  1. The same class.
  2. Classes in the same package.
  3. Subclasses (children classes) in any package.

It's primarily used when you want to allow subclasses to access a member, but prevent unrelated classes from doing so.

Visibility:

  • Within the same class.
  • Within the same package.
  • By subclasses (even if in a different package).

Example:

// package com.example.model
package com.example.model;

public class Vehicle {
protected String type = "Generic Vehicle";

protected void startEngine() {
System.out.println("Engine started for " + type);
}
}

// package com.example.model (same package)
package com.example.model;

class Garage {
public void testVehicle() {
Vehicle car = new Vehicle();
car.startEngine(); // Accessible, same package
System.out.println(car.type); // Accessible, same package
}
}

// package com.example.app (different package, subclass)
package com.example.app;

import com.example.model.Vehicle; // Import needed for subclassing

public class Car extends Vehicle { // Car is a subclass of Vehicle
public Car() {
this.type = "Car"; // Accessible, as Car is a subclass
}

public void drive() {
startEngine(); // Accessible, as Car is a subclass
System.out.println(type + " is driving.");
}

public static void main(String[] args) {
Car myCar = new Car();
myCar.drive(); // Output: Engine started for Car, Car is driving.
}
}

// package com.example.app (different package, NOT a subclass)
package com.example.app;

// import com.example.model.Vehicle; // Uncomment if needed

public class Mechanic {
public static void main(String[] args) {
// Vehicle someVehicle = new Vehicle(); // Might need import
// someVehicle.startEngine(); // COMPILE ERROR: Cannot access protected member outside package unless subclass
// System.out.println(someVehicle.type); // COMPILE ERROR
}
}

3. default (Package-Private)

When no access modifier is explicitly specified for a class, method, or field, it gets the default access level. This is also known as "package-private" access.

default members are accessible only within:

  1. The same class.
  2. Other classes within the same package.

They are not accessible by subclasses in different packages.

Visibility:

  • Within the same class.
  • Within the same package.

Example:

// package com.example.data
package com.example.data;

class UserProfile { // Default access class (only accessible within com.example.data)
String userName = "Guest"; // Default access field

void displayProfile() { // Default access method
System.out.println("User: " + userName);
}
}

// package com.example.data (same package)
package com.example.data;

public class ProfileManager {
public void showUserProfile() {
UserProfile user = new UserProfile(); // Accessible (same package)
System.out.println("Manager accessing: " + user.userName); // Accessible
user.displayProfile(); // Accessible
}
}

// package com.example.app (different package)
package com.example.app;

// import com.example.data.UserProfile; // COMPILE ERROR: UserProfile is not public in com.example.data; cannot be accessed from outside package

public class MainApp {
public static void main(String[] args) {
// UserProfile user = new UserProfile(); // COMPILE ERROR (UserProfile is default access)
// new com.example.data.UserProfile(); // COMPILE ERROR (even with fully qualified name)

// However, if ProfileManager (public) exposes UserProfile, it's fine for public members
// ProfileManager manager = new ProfileManager();
// manager.showUserProfile(); // This would work if ProfileManager were imported and public
}
}

4. private

The private access modifier is the most restrictive. private members (fields, methods, constructors) are accessible only from within the same class in which they are declared.

  • private classes are not directly supported (except for nested classes).
  • private is fundamental for encapsulation, allowing classes to hide their internal state and implementation details from the outside world.

Visibility: Accessible only within the same class.

Example:

// package com.example.core
package com.example.core;

public class BankAccount {
private double balance; // Private field

public BankAccount(double initialBalance) { // Public constructor for external access
this.balance = initialBalance;
}

private void logTransaction(String message) { // Private method
System.out.println("Transaction Log: " + message);
}

public void deposit(double amount) { // Public method to interact with balance
if (amount > 0) {
this.balance += amount;
logTransaction("Deposited " + amount + ". New balance: " + balance); // Accessible within class
}
}

public double getBalance() { // Public getter
return balance;
}
}

// package com.example.app
package com.example.app;

import com.example.core.BankAccount;

public class BankingApp {
public static void main(String[] args) {
BankAccount myAccount = new BankAccount(1000.0);
myAccount.deposit(500.0); // Accessible

System.out.println("Current balance: " + myAccount.getBalance()); // Accessible

// myAccount.balance = 2000.0; // COMPILE ERROR: balance has private access
// myAccount.logTransaction("Unauthorized access"); // COMPILE ERROR: logTransaction has private access
}
}

Summary Table

ModifierSame ClassSame PackageSubclass (different package)Anywhere (different package, not subclass)
privateYesNoNoNo
defaultYesYesNoNo
protectedYesYesYesNo
publicYesYesYesYes

Key Takeaways

  • Encapsulation: Access modifiers are crucial for enforcing encapsulation. They allow you to hide the internal workings of a class, exposing only what is necessary, which makes code more maintainable and less prone to errors.
  • API Design: When designing APIs, you typically expose public methods and classes that define the public contract of your component, while keeping internal state and helper methods private or protected.
  • Flexibility vs. Control: Each modifier offers a different balance between flexibility (how widely accessible a member is) and control (how much you restrict its usage).
  • Class Level Access: Top-level classes can only be public or default. Inner/nested classes can use all four access modifiers.