Java Modifiers
Modifiers are keywords you attach to classes, methods, and fields to control two things: who can see and use them, and how they behave. Java splits modifiers into two families: access modifiers, which control visibility across classes and packages, and non-access modifiers, which change behavior — whether a member belongs to the class instead of an instance, whether it can be reassigned or overridden, and more. Understanding modifiers is essential to writing encapsulated, maintainable object-oriented code, because they are how you enforce the boundary between a class’s internal implementation and its public contract.
Overview: How Modifiers Work
Every class, field, method, and constructor in Java has an access level, even if you don’t write one explicitly — leaving it blank gives you default (package-private) access. The compiler checks these modifiers at compile time: if code outside the allowed scope tries to touch a member, compilation fails with an error like “has private access in.” This is not a runtime check or a convention — it is enforced by the Java Language Specification and verified by the compiler and the JVM’s bytecode verifier, so it cannot be bypassed by ordinary code (only reflection with `setAccessible(true)` can override it, and even that can be blocked by the module system in newer Java versions).
Access modifiers answer “who can see this?” Non-access modifiers answer “how does this member behave?” A field can carry one access modifier plus any number of compatible non-access modifiers at the same time — for example private static final int MAX = 100; combines all three ideas: hidden from other classes, shared by the whole class rather than per-instance, and unchangeable after initialization.
Access Modifiers
Java has four access levels. Three have keywords; the fourth, package-private, is what you get by omitting a keyword entirely.
| Modifier | Same class | Same package | Subclass (different package) | World |
|---|---|---|---|---|
public |
Yes | Yes | Yes | Yes |
protected |
Yes | Yes | Yes | No |
| (default / package-private) | Yes | Yes | No | No |
private |
Yes | No | No | No |
public members form the API of your class — anyone, anywhere, can use them. private members are implementation details, visible only inside the same class (useful for hiding internal fields behind getters/setters). protected is mainly for inheritance: it lets subclasses in other packages use a member while still hiding it from unrelated code. Default access (no keyword) is for things that should stay internal to one package, like helper classes used only by classes in that package.
Top-level classes can only be public or default — never private or protected, since those only make sense for members nested inside something.
Non-Access Modifiers
| Modifier | Applies to | Effect |
|---|---|---|
static |
fields, methods, nested classes | Belongs to the class itself, not to any instance; shared across all objects. |
final |
classes, methods, variables | Class cannot be subclassed; method cannot be overridden; variable can only be assigned once. |
abstract |
classes, methods | Class cannot be instantiated directly; method has no body and must be implemented by a subclass. |
synchronized |
methods, blocks | Only one thread can execute the guarded code on a given object at a time. |
transient |
fields | Field is skipped by default Java serialization. |
volatile |
fields | Reads/writes go directly to main memory, ensuring visibility across threads. |
native |
methods | Method body is implemented in platform-specific code (e.g. C), not Java. |
Syntax
[access modifier] [non-access modifiers] returnType methodName(parameters) {
// body
}
[access modifier] [non-access modifiers] type fieldName = value;
- access modifier — one of
public,protected,private, or omitted for default. - non-access modifiers — any of
static,final,abstract,synchronized, etc., written before the type. - At most one access modifier is allowed per member, but multiple non-access modifiers can combine (order is flexible but conventionally access comes first).
Examples
Example 1: Access modifiers and encapsulation
class BankAccount {
private double balance;
protected String owner;
String accountType;
public String bankName = "Global Bank";
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
this.accountType = "Savings";
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
}
public class Main {
public static void main(String[] args) {
BankAccount acc = new BankAccount("Alice", 500.0);
acc.deposit(250.0);
System.out.println("Owner: " + acc.owner);
System.out.println("Account type: " + acc.accountType);
System.out.println("Bank: " + acc.bankName);
System.out.println("Balance: " + acc.getBalance());
}
}
Output:
Owner: Alice
Account type: Savings
Bank: Global Bank
Balance: 750.0
Here balance is private, so Main cannot read it directly and must go through the public getBalance() method — that’s encapsulation in action. Because Main and BankAccount live in the same file (same default package), the protected field owner and the package-private field accountType are both reachable directly, while bankName is public and reachable from anywhere.
Example 2: static vs. instance state, and final constants
class Counter {
static int count = 0;
final int id;
Counter() {
count++;
id = count;
}
}
public class Main {
static final double PI_APPROX = 3.14159;
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
System.out.println("c1 id: " + c1.id);
System.out.println("c2 id: " + c2.id);
System.out.println("c3 id: " + c3.id);
System.out.println("Total counters (static): " + Counter.count);
System.out.println("PI_APPROX: " + PI_APPROX);
}
}
Output:
c1 id: 1
c2 id: 2
c3 id: 3
Total counters (static): 3
PI_APPROX: 3.14159
count is static, so there is exactly one copy shared by every Counter instance — each constructor call increments the same memory location. In contrast, id is an instance field marked final: each object gets its own value, assigned once in the constructor and never changed again. PI_APPROX combines static and final to form a true class-wide constant.
Example 3: abstract classes and final methods
abstract class Shape {
abstract double area();
final void describe() {
System.out.println("This shape has area: " + area());
}
}
class Circle extends Shape {
private double radius;
Circle(double radius) {
this.radius = radius;
}
double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
double area() {
return width * height;
}
}
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle(3.0);
Shape s2 = new Rectangle(4.0, 5.0);
s1.describe();
s2.describe();
}
}
Output:
This shape has area: 28.274333882308138
This shape has area: 20.0
Shape is abstract, so it cannot be instantiated with new Shape() — only concrete subclasses like Circle and Rectangle that implement area() can be created. The describe() method is final, meaning every subclass shares the exact same implementation and cannot override it, while still calling each subclass’s own version of area() polymorphically.
Under the Hood
Access and non-access modifiers are stored as flags on each class, field, and method in the compiled .class file’s constant pool and member tables (visible if you run javap -p). When code calls a method or reads a field, the JVM’s bytecode verifier checks these flags before allowing the operation — this is why an illegal access attempt fails at compile time for normal Java code, and would also be rejected by the verifier if you tried to hand-craft bytecode that skipped the check. For static members, the JVM allocates storage once per class, in the method area, when the class is loaded — not per object, which is why all instances observe the same value. For final local variables and fields, the compiler enforces single-assignment through definite-assignment analysis; for final classes and methods, the JVM’s method resolution simply never looks for an overriding version in subclasses, which also lets the JIT compiler inline final methods more aggressively since it knows the implementation can’t change.
Common Mistakes
Mistake 1: Accessing a private field from outside its class.
class Account {
private double balance = 100.0;
}
public class Main {
public static void main(String[] args) {
Account acc = new Account();
System.out.println(acc.balance);
}
}
This fails to compile with “balance has private access in Account.” The fix is to expose a public getter method instead of the raw field, keeping the field private:
class Account {
private double balance = 100.0;
public double getBalance() {
return balance;
}
}
Mistake 2: Reassigning a final variable.
final int MAX_USERS = 100;
MAX_USERS = 200;
Once a final variable is assigned, any later assignment is a compile error (“cannot assign a value to final variable”). If the value genuinely needs to change, don’t mark it final; if it’s meant to be a constant, give it its final value once, typically at declaration.
Mistake 3: Overusing public. Marking every field public to “save time” removes the ability to validate input or change internal representation later without breaking every caller. Default to the most restrictive access level (usually private) and only widen it when there’s a real need — this is the principle of least privilege applied to code.
Best Practices
- Make fields
privateby default and expose behavior through public methods, not raw field access. - Use
protectedonly for members genuinely meant to be extended by subclasses, not as a lazy alternative topublic. - Mark constants
static finaland name them inUPPER_SNAKE_CASE. - Use
finalon classes and methods you never want overridden, especially for security- or correctness-critical logic. - Prefer package-private (default) access for helper classes that only your package should use.
- Reach for
abstractclasses when subclasses share common state or partial implementation; prefer interfaces when you’re only defining a contract. - Use
synchronizedandvolatiledeliberately and sparingly — they solve specific concurrency problems and add overhead if misapplied.
Practice Exercises
- Write a
Personclass with aprivateagefield, a public constructor, and a publicgetAge()method. Try accessingagedirectly from another class and observe the compiler error, then fix it using the getter. - Create an
abstractclassAnimalwith an abstract methodmakeSound(), and two subclassesDogandCatthat implement it differently. Call both through anAnimalreference. - Write a class with a
staticfield that counts how many objects have been created, plus afinalinstance field storing each object’s unique ID assigned from that counter. Create several objects and print both values.
Summary
- Access modifiers (
public,protected, default,private) control visibility across classes and packages, from most to least open. - Non-access modifiers like
static,final, andabstractchange behavior: shared state, immutability, or requiring subclass implementation. privatefields with public getters/setters are the foundation of encapsulation.staticmembers belong to the class and are shared by all instances; instance members belong to each object separately.finalprevents reassignment (variables), overriding (methods), and subclassing (classes).abstractclasses and methods force subclasses to provide concrete implementations before they can be instantiated.- All of this is enforced by the compiler and the JVM’s bytecode verifier, not just convention.
