Java Access Modifiers—Public Field Allows Negative Balances
A public double balance field allowed setting -100 on accounts.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- Java has four access levels: public, protected, default (package-private), private
- public: visible to all classes everywhere
- protected: visible to same package + subclasses (any package)
- default: visible only to same package (no keyword)
- private: visible only within the declaring class
- Golden rule: default to private, open only when needed
Java access modifiers are the language's mechanism for enforcing encapsulation — the principle that hides internal state and implementation details from external code. They exist because without them, any class can reach into another's internals, creating tight coupling that makes refactoring dangerous and bugs like negative bank balances inevitable.
The four modifiers — private, default (package-private), protected, and public — form a graduated access control system, from most restrictive to least. private restricts access to the declaring class only; default (no keyword) opens access to other classes in the same package; protected extends that to subclasses in any package; and public opens the door to everyone. In practice, public fields are almost always a design smell because they expose mutable state directly, bypassing validation logic.
The canonical counterexample is a BankAccount class with a public double balance field — any caller can set it to -500, and you've lost control of your invariants. The fix is to make the field private and expose it through a getBalance() method and a method that checks for sufficient funds.withdraw()
Access modifiers also apply to classes themselves: a top-level class can be public or default (package-private), while inner classes can use all four. The rule of thumb is to start with the most restrictive modifier possible and widen access only when you have a concrete need — this keeps your API surface small, your coupling loose, and your invariants enforceable.
Imagine your house has different rooms. Your front garden is public — anyone walking past can see it. Your living room is protected — only family and close relatives can enter. Your bedroom is private — only you go in there. Your neighbour's shared driveway has no lock, but it's understood only people on that street use it. Java access modifiers work exactly like this: they control who is allowed to 'enter' a class, method, or variable.
Every piece of software you have ever used — your bank app, your favourite game, a weather app — is built on one golden rule: not everything should be accessible to everyone. When a banking app processes your PIN, it doesn't let any random part of the program read or change that number freely. That restriction is enforced in Java using access modifiers, and they are one of the very first tools a Java developer reaches for when designing anything serious.
Without access modifiers, every variable, method, and class in your program would be completely exposed to every other part of the codebase. That sounds harmless on a small project, but on a real-world application with hundreds of classes and multiple developers, it creates chaos. Someone accidentally modifies a value they shouldn't touch, a bug appears, and nobody knows where it came from. Access modifiers solve this by letting you draw clear boundaries — you decide exactly what is visible and what is locked away.
By the end of this article you'll know all four Java access modifiers, understand exactly when and why to use each one, be able to spot access-related compiler errors and fix them instantly, and feel confident answering access modifier questions in a Java interview. Let's build this up from scratch.
Why Public Fields Are a Design Smell
Java access modifiers control visibility at the class, package, subclass, and world levels. The four levels—private, default (package-private), protected, and public—form a strict hierarchy. Private restricts access to the declaring class; default allows access within the same package; protected extends that to subclasses; public opens access to any code. The core mechanic is compile-time enforcement: the compiler rejects illegal access, preventing accidental coupling.
In practice, the default access (no modifier) is the most misunderstood. It is not a synonym for 'internal'—it is package-private, meaning any class in the same package can access the member. Protected is also tricky: it grants access to subclasses and same-package classes, but not to unrelated classes in other packages. Public is the most permissive and should be used sparingly—every public member is a commitment to external consumers.
Use the most restrictive modifier that still allows the intended use. Start with private, then widen only when necessary. Public fields break encapsulation: they expose internal representation, prevent validation, and make refactoring impossible without breaking callers. In production systems, a public field that allows negative balances is a bug waiting to happen—validation logic lives outside the class, scattered across callers.
The Four Access Modifiers and What They Actually Mean
Java gives you four access levels. Think of them as four different lock types on a door. From most open to most locked, they are: public, protected, default (no keyword), and private.
'public' means absolutely anyone can access it — any class, in any package, anywhere in the project. It's the front door of a shop: open to the world.
'protected' means the class itself, any class in the same package, and any subclass (a class that inherits from this one) can access it. Think of a family recipe — you share it with family members and people in your household, but not strangers.
'default' (also called package-private) is what you get when you write NO modifier at all. Only classes inside the same package can access it. It's like an unwritten agreement between neighbours — people on the same street can use the shared path, but outsiders can't.
'private' is the strictest. Only the class that owns the member can access it. Nobody else — not even a subclass — can touch it directly. It's your diary with a lock.
These aren't just theory. They map directly to how Java enforces encapsulation, which is one of the four pillars of object-oriented programming. Getting these right is the difference between code that's easy to maintain and code that becomes a nightmare.
// This file demonstrates all four access modifiers in one place. // Run this as a single file (Java 11+) or create the classes in separate files. // A public class — visible to everyone, everywhere public class AccessModifierDemo { // public field — any code in the entire project can read/change this public String brandName = "TheCodeForge"; // protected field — accessible in this class, same package, and subclasses protected int articleCount = 42; // default (no keyword) field — only accessible within the same package String internalCategory = "Java Basics"; // no modifier = package-private // private field — ONLY this class can read or change this value private String editorPassword = "s3cr3tP@ss"; // public method — anyone can call this public void showBrandName() { // This method is the 'front door' — safe to expose publicly System.out.println("Brand: " + brandName); } // private method — internal helper, no outside class should call this private String encryptPassword(String rawPassword) { // Hiding the implementation detail — callers don't need to know HOW return "[ENCRYPTED]" + rawPassword.hashCode(); } // public method that uses the private method internally public void printEncryptedPassword() { // We expose a safe action publicly, but keep the logic private System.out.println("Encrypted: " + encryptPassword(editorPassword)); } public static void main(String[] args) { AccessModifierDemo demo = new AccessModifierDemo(); // Accessing public field directly — perfectly fine System.out.println("Public field: " + demo.brandName); // Accessing protected field from within the SAME class — fine System.out.println("Protected field: " + demo.articleCount); // Accessing default field from within the SAME package — fine System.out.println("Default field: " + demo.internalCategory); // Accessing private field from within the SAME class — fine System.out.println("Private field: " + demo.editorPassword); // Calling public method — fine from anywhere demo.showBrandName(); // Calling public method that internally uses a private method demo.printEncryptedPassword(); // NOTE: If you tried to call demo.encryptPassword() from OUTSIDE // this class, the compiler would throw an error. Try it and see! } }
private explicitly unless you have a reason to use default.private and public in Practice — The Bank Account Example
The most important pair to master first is private and public — and the classic teaching example is a bank account, because it maps perfectly to the real world.
Imagine a BankAccount class. It has a balance. Should the balance field be public? Absolutely not. If balance is public, any other class in your program can write something like: account.balance = 1000000; — with no checks, no history, no validation. That's terrifying.
Instead, you make balance private. Now nobody outside the class can touch it directly. You then provide public methods — called getters and setters — that control all access. The getter lets someone READ the balance. The setter (or a deposit/withdraw method) lets someone CHANGE it, but only after you've run whatever checks you need. This pattern is called encapsulation, and it's the whole point of private.
Public is the opposite philosophy — use it for things you genuinely want to expose as part of your class's contract with the rest of the world. Method names, constructors, anything another class legitimately needs to call. The golden rule: default to private first. Only make something public if you have a clear reason to.
public class BankAccount { // private — no outside class can directly read or write the balance // This is the single most important use of 'private' you'll encounter private double balance; // private — internal account identifier, nobody outside needs this raw private String accountNumber; // public constructor — anyone who wants to create an account can do so public BankAccount(String accountNumber, double openingBalance) { this.accountNumber = accountNumber; // Validate even at construction time — private data, our rules if (openingBalance < 0) { throw new IllegalArgumentException("Opening balance cannot be negative."); } this.balance = openingBalance; } // public getter — allows READ access to balance, but not direct WRITE access public double getBalance() { return balance; // we return a copy of the value, not the field itself } // public method to deposit money — enforces our business rules public void deposit(double amount) { if (amount <= 0) { System.out.println("Deposit amount must be positive. Ignoring."); return; } balance += amount; // only THIS class modifies balance directly System.out.println("Deposited £" + amount + ". New balance: £" + balance); } // public method to withdraw money — enforces our business rules public void withdraw(double amount) { if (amount <= 0) { System.out.println("Withdrawal amount must be positive. Ignoring."); return; } if (amount > balance) { // private data lets us protect this logic completely System.out.println("Insufficient funds. Balance is only £" + balance); return; } balance -= amount; System.out.println("Withdrew £" + amount + ". New balance: £" + balance); } // private helper — only used internally, no outside class needs to know this exists private boolean isHighValueAccount() { return balance > 10000; } // public method that uses the private helper — safe, controlled exposure public void printAccountSummary() { System.out.println("Account: " + accountNumber); System.out.println("Balance: £" + balance); System.out.println("High-value account: " + isHighValueAccount()); } public static void main(String[] args) { BankAccount myAccount = new BankAccount("ACC-00192", 500.00); // Reading balance through the public getter — correct approach System.out.println("Initial balance: £" + myAccount.getBalance()); myAccount.deposit(250.00); myAccount.withdraw(100.00); myAccount.withdraw(800.00); // should fail — insufficient funds System.out.println(); myAccount.printAccountSummary(); // The line below would cause a COMPILE ERROR if you uncommented it: // myAccount.balance = 999999; // ERROR: balance has private access in BankAccount // myAccount.isHighValueAccount(); // ERROR: isHighValueAccount() has private access } }
protected and Default — When Packages and Inheritance Come In
Once you understand private and public, the next step is understanding when you need something in between. That's where protected and default (package-private) come in — and they're the two most confused access modifiers for beginners.
Default (no keyword) is simpler: if two classes are in the same package — the same folder, essentially — they can see each other's default members. This is useful for utility classes or helper logic that belongs to a module but shouldn't be exposed as part of a public API. Think of it as 'internal' — visible within your team's workspace, invisible to the outside world.
Protected goes one step further: it adds inheritance to the mix. A protected member is visible in the same package AND in any subclass, even if that subclass is in a completely different package. This is specifically designed for the parent-child class relationship in object-oriented programming. You use protected when you're building a base class and you know subclasses will need to access or override something, but you still don't want the whole world touching it.
A good real-world analogy: a protected family recipe. Your children (subclasses) can have it and adapt it. Strangers (unrelated classes outside the package) cannot.
// === FILE 1: Vehicle.java (in package: com.codeforge.vehicles) === // Imagine this is in: com/codeforge/vehicles/Vehicle.java package com.codeforge.vehicles; public class Vehicle { // public — any class anywhere can read the brand public String brand; // protected — this class AND any subclass (even in other packages) can access this // It's the parent sharing something specifically with its children protected int engineCapacityCC; // default (no modifier) — only classes in com.codeforge.vehicles can see this String factoryLocation; // package-private // private — only Vehicle itself can touch this private String internalSerialCode; public Vehicle(String brand, int engineCapacityCC, String factoryLocation, String internalSerialCode) { this.brand = brand; this.engineCapacityCC = engineCapacityCC; this.factoryLocation = factoryLocation; this.internalSerialCode = internalSerialCode; } // protected method — subclasses can call and even override this protected String getEngineDescription() { return brand + " engine: " + engineCapacityCC + "cc"; } // public method — the 'front door' for external callers public void displayInfo() { System.out.println("Brand: " + brand); System.out.println("Engine: " + engineCapacityCC + "cc"); System.out.println("Factory: " + factoryLocation); // Internal serial code stays private — even displayInfo() is careful about it System.out.println("Serial: [RESTRICTED]"); } } // === FILE 2: ElectricCar.java (in package: com.codeforge.electric) === // A DIFFERENT package — notice it can still access the protected member // Imagine this is in: com/codeforge/electric/ElectricCar.java package com.codeforge.electric; import com.codeforge.vehicles.Vehicle; // importing the parent class public class ElectricCar extends Vehicle { // 'extends' means ElectricCar is a subclass of Vehicle private int batteryRangeKm; public ElectricCar(String brand, int batteryRangeKm) { // Calling the parent constructor — electric cars have near-zero CC engine super(brand, 0, "Tesla Gigafactory", "EV-" + brand + "-001"); this.batteryRangeKm = batteryRangeKm; } @Override protected String getEngineDescription() { // Subclass CAN access engineCapacityCC because it's protected // Even though ElectricCar is in a DIFFERENT package, inheritance allows this return brand + " electric motor (no CC), range: " + batteryRangeKm + "km"; } public void showElectricInfo() { // We can call the protected method because we're a subclass System.out.println(getEngineDescription()); // We can access protected field engineCapacityCC directly System.out.println("Engine CC (should be 0 for EV): " + engineCapacityCC); // We CANNOT access factoryLocation here — it's default/package-private // and ElectricCar is in a DIFFERENT package. Uncommenting this would fail: // System.out.println(factoryLocation); // COMPILE ERROR } } // === FILE 3: Main.java (in package: com.codeforge.electric) === package com.codeforge.electric; public class Main { public static void main(String[] args) { ElectricCar tesla = new ElectricCar("Tesla Model 3", 560); // Public method — accessible from anywhere tesla.displayInfo(); System.out.println(); // Calling the subclass-specific method tesla.showElectricInfo(); // Accessing public field directly — fine System.out.println("\nBrand directly: " + tesla.brand); // From OUTSIDE the class hierarchy, protected members are NOT accessible: // System.out.println(tesla.engineCapacityCC); // COMPILE ERROR from here } }
Access Modifiers on Classes — Not Just Fields and Methods
Everything we've covered so far applies to fields and methods inside a class. But access modifiers also control the class itself — and this trips up a lot of beginners.
A top-level class (a class not nested inside another class) can only be public or default (no modifier). You cannot make a top-level class private or protected — the Java compiler will throw an error immediately. This makes sense: a private class that nothing can see would be completely useless.
A public class is visible to everyone. A default class is visible only within its own package. This is useful when you want to create helper classes that are only relevant internally — you don't want other packages to depend on them.
However, inner classes (classes defined inside another class) can use all four access modifiers. A private inner class is a popular pattern for implementation details that are tightly coupled to their outer class.
One important Java rule: if a file contains a public class, the filename MUST match that class name exactly (including case). If your class is 'public class UserProfile', your file must be 'UserProfile.java'. Get this wrong and the compiler will refuse to compile. This is why you'll rarely see more than one public class per file.
// This file demonstrates access modifiers applied at the CLASS level // A single .java file can have only ONE public class // The filename must match: ClassAccessModifierDemo.java public class ClassAccessModifierDemo { // private inner class — completely hidden from outside // Only ClassAccessModifierDemo can create or use a Node // This is a common pattern in data structures (LinkedList nodes, tree nodes, etc.) private class Node { int value; Node nextNode; // points to the next node in a chain Node(int value) { this.value = value; this.nextNode = null; } } // public inner class — anyone can use this if they have a ClassAccessModifierDemo instance public class Statistics { private int totalNodes; public Statistics(int totalNodes) { this.totalNodes = totalNodes; } public void printStats() { System.out.println("Total nodes in chain: " + totalNodes); } } // The outer class uses its private inner class freely private Node firstNode; private int nodeCount; // public method — builds an internal chain using private Node objects public void addValue(int value) { Node newNode = new Node(value); // Node is private, but we're inside the owner class if (firstNode == null) { firstNode = newNode; } else { // Walk to the end of the chain Node currentNode = firstNode; while (currentNode.nextNode != null) { currentNode = currentNode.nextNode; } currentNode.nextNode = newNode; // attach the new node at the end } nodeCount++; } // public method — lets outsiders read values without knowing Node exists public void printAllValues() { System.out.print("Chain: "); Node currentNode = firstNode; while (currentNode != null) { System.out.print(currentNode.value); if (currentNode.nextNode != null) System.out.print(" -> "); currentNode = currentNode.nextNode; } System.out.println(); } public Statistics getStatistics() { return new Statistics(nodeCount); // returns a public inner class instance } public static void main(String[] args) { ClassAccessModifierDemo chain = new ClassAccessModifierDemo(); chain.addValue(10); chain.addValue(25); chain.addValue(37); chain.printAllValues(); Statistics stats = chain.getStatistics(); stats.printStats(); // The line below would cause a COMPILE ERROR: // ClassAccessModifierDemo.Node n = new ClassAccessModifierDemo.Node(5); // ERROR: Node has private access in ClassAccessModifierDemo } }
Best Practices and Common Pitfalls with Access Modifiers
After understanding the mechanics, the real skill is applying them wisely. Here are patterns that separate clean code from fragile code.
First, the golden rule: minimize visibility. Start every member as private. Only increase visibility when there's a concrete need. This is called the Principle of Least Privilege applied to code.
Second, design interfaces, not innards. Public methods are your contract with the world. They should be stable. Keep implementation details private so you can change them without breaking callers.
Third, use protected sparingly. Protected creates a coupling between a parent class and its subclasses. That's intentional, but it should be a deliberate design choice, not a default. If you're writing a class that isn't meant to be extended, mark it final and keep everything private.
Fourth, be explicit about default access. Many bugs come from forgetting to add a modifier. A field that should be private but has no modifier is accidentally visible to the entire package. When you see a field without a modifier, you should be able to explain why it's package-private.
Finally, use access modifiers in tests. Unit tests in the same package can access default members. That's useful for testing package-private methods. But don't rely on it for production code — prefer testing through public APIs.
Why the Default Modifier Is a Silent Maintenance Bomb
Default access (package-private) feels safe because you don't type a keyword. That's exactly what makes it dangerous. Teams inherit code where package-private members spread through a package like a slow leak. The problem? Package-private treats the whole package as one leaky module. One careless coworker adds a class in your package and suddenly your carefully hidden utility methods are fair game. I've seen production systems where package-private grew into tangled spaghetti because nobody drew hard boundaries. The rule is simple: if a method or field isn't explicitly protected, public, or private, then every class in the package holds a reference to it. That's not encapsulation. That's organized chaos. The JVM enforces nothing beyond the package boundary. Use default only for package-internal contracts that must cross classes but never cross packages. Anything else deserves private or protected. Don't let the absence of a keyword become an absence of discipline.
package com.thecodeforge.internal; public class UserService { String calculateHash(String raw) { // default access — hidden trap return raw.hashCode() + ""; } } class RogueService { public void exploit() { UserService svc = new UserService(); String hash = svc.calculateHash("password123"); // access granted! System.out.println(hash); } }
Protected Without Inheritance Is Just Fancy Default Access
Protected is the most misunderstood modifier. Developers treat it as 'slightly more open than default.' That's dangerous. Protected grants access to subclasses in other packages — that's its whole purpose. But if a class is never subclassed outside its package, protected behaves identically to default. I've reviewed codebases where protected methods sat on non-inheritable classes for years. They added zero security and zero flexibility. They just confused readers. The real power of protected is in framework design — think Spring's protected init methods or template method patterns. If you don't have a concrete inheritance chain, pick private or package-private. Don't add protected because 'someday someone might extend this.' That day rarely comes. When it does, you refactor. Java's protected is a contract for subclass authors. If you're not writing for subclass authors, you're just adding noise. Keep your API surface honest.
package com.thecodeforge.base; public class TemplateProcessor { protected void preProcess() { // intended for subclasses System.out.println("Template: pre-processing..."); } public final void execute() { preProcess(); System.out.println("Template: processing..."); } } // Subclass in a different package: package com.thecodeforge.extensions; import com.thecodeforge.base.TemplateProcessor; public class CustomProcessor extends TemplateProcessor { @Override protected void preProcess() { System.out.println("Custom: extended pre-processing!"); } } public class Main { public static void main(String[] args) { new CustomProcessor().execute(); } }
Top-Level Class Access: The Two-Modifier Rule That Catches Everyone
Most tutorials cover member-level modifiers but gloss over the hard constraint on top-level classes. A top-level class in Java can only be public or package-private (default). Protected and private don't apply — they're illegal on outer classes. Why? Because protected on a class makes no semantic sense. Protected means 'accessible to subclasses.' But a class isn't inherited from — its instances are. Private on a class would make it invisible to the entire package, which defeats the purpose of a top-level declaration. I still see junior devs tacking 'protected class Foo' and getting compile errors. They waste 20 minutes on something a two-line rule would fix. The exception is nested (inner) classes — those can use any modifier because they exist inside the visibility scope of the enclosing class. Memorize this: top-level classes get two choices. Public means global. Default means package-only. Pick wisely. Your architecture depends on it.
// Valid top-level classes: package com.thecodeforge.model; public class PublicClass { } // Accessible everywhere class PackagePrivateClass { } // Only in this package // Invalid — won't compile: // protected class ProtectedClass { } // private class PrivateClass { } // Valid nested classes: public class OuterClass { protected class NestedProtected { } // OK — nested private class NestedPrivate { } // OK — nested }
The Case of the Exposed Account Balance
balance field in Account was declared public double balance; instead of private. Any class in the project could read and write it. The third-party library's malicious code (or an internal misconfiguration) set it to -100.private, added a getBalance() getter, and enforced all writes through deposit() and withdraw() methods with validation. Ran a full data integrity check to correct corrupted records.- Always start with
privatefor fields — never expose raw data. - Use controlled public methods (getters/setters) to enforce invariants.
- Code review should flag any public field that isn't a constant (static final).
javap -p ParentClass.class (shows all members, including private)If private, add protected getter. If default, either move subclass to same package or change to protected.Check the import statement and package declaration.If the method is default, either move the calling class into the same package or change modifier to public.ls -la *.java (list files and check names)Rename the file or the class so they match.| Access Level | Same Class | Same Package | Subclass (diff. package) | Any Class (anywhere) |
|---|---|---|---|---|
| public | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| protected | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No |
| default (no keyword) | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
| private | ✅ Yes | ❌ No | ❌ No | ❌ No |
Key takeaways
Common mistakes to avoid
4 patternsMaking all fields public for convenience
Confusing protected with private when it comes to subclasses
Forgetting that default access is NOT 'no restriction'
Using protected on fields when you should use public getters
Interview Questions on This Topic
What is the difference between protected and default (package-private) access in Java? Can a subclass in a different package access a protected member? What about a default member?
Why would you ever use private access for a field when a getter method that returns the same value is also public — isn't that the same thing?
If a class has no access modifier on its constructor, can you instantiate it from a different package? What error would you see, and how would you fix it?
Can you make a top-level class private in Java? Explain.
Explain the impact of access modifiers on the Java Module system (JPMS). How does 'public' differ in a module context?
Frequently Asked Questions
The default access level in Java is called package-private. You get it by writing NO modifier at all before the field, method, or class. It means only classes within the same package can access that member. It's NOT a keyword — writing 'default int count = 0;' is actually a syntax error.
No. Private members are strictly limited to the class they're declared in — not even subclasses can access them directly. If a parent class has a private field that subclasses need to work with, the parent should provide a protected getter method or use protected access on the field itself.
Private restricts access to the single class where the member is declared — no other class can touch it, including subclasses. Protected is less restrictive: it allows access from the same class, any class in the same package, AND any subclass regardless of package. Use private when you want to hide implementation details completely, and protected when you're designing a class that's meant to be extended.
No. 'default' is a reserved keyword used only in switch expressions and default methods in interfaces. To make a member package-private, simply omit any access modifier. Writing 'default int x;' will result in a compiler error.
You cannot declare a top-level class as private — it's a compile error. Inner classes (nested classes) can be private, meaning they are only accessible within the enclosing class. This is useful for implementation details like tree nodes.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Java Basics. Mark it forged?
9 min read · try the examples if you haven't