Java Tutorial
🔍

Java public Modifier

Java public Modifier

public is the widest access modifier in Java. A member or class marked public is accessible from any class in any package anywhere in the program — no restrictions. It is the declaration that says: this is intentionally part of the outward-facing contract. Callers in the same class, same package, different package, subclass, non-subclass — all of them can reach a public member.

Used carelessly, public creates fragile code where any external class can depend on any internal detail, making changes impossible without breaking callers. Used deliberately, public defines a clean, stable API surface that the rest of the codebase depends on with confidence.

What public Can Be Applied To

public can be placed on:

  ✓ Top-level class         → accessible from any package
  ✓ Nested / inner class    → accessible from any package
  ✓ Constructor             → any code can call new ClassName()
  ✓ Method (instance)       → any code can call obj.method()
  ✓ Method (static)         → any code can call ClassName.method()
  ✓ Field (instance)        → any code can read/write obj.field
  ✓ Field (static)          → any code can read/write ClassName.field
  ✓ Interface               → implemented / used from any package
  ✓ Enum                    → used from any package

  Rule for top-level classes:
    — One public class per source file
    — File name must match the public class name exactly
    — class OrderService → must be in OrderService.java

1 — public Class

A public class is visible to every class in every package. It can be imported and instantiated from anywhere.

// File: com/devstackflow/api/ProductCatalogue.java package com.devstackflow.api; import java.util.ArrayList; import java.util.List; import java.util.Optional; public class ProductCatalogue { // public — importable from any package private final List<String> products = new ArrayList<>(); // public constructor — any code can create an instance public ProductCatalogue() { } public void addProduct(String product) { products.add(product); } public Optional<String> findProduct(String name) { return products.stream() .filter(p -> p.equalsIgnoreCase(name)) .findFirst(); } public List<String> getAllProducts() { return List.copyOf(products); // defensive copy — immutable view } public int size() { return products.size(); } }
// File: com/differentteam/ShopFrontend.java package com.differentteam; import com.devstackflow.api.ProductCatalogue; // public class — importable public class ShopFrontend { public static void main(String[] args) { // public class + public constructor = instantiable from anywhere ProductCatalogue catalogue = new ProductCatalogue(); catalogue.addProduct("Laptop"); catalogue.addProduct("Mouse"); catalogue.addProduct("Keyboard"); System.out.println("All products : " + catalogue.getAllProducts()); System.out.println("Found Laptop : " + catalogue.findProduct("laptop").orElse("not found")); System.out.println("Size : " + catalogue.size()); } }
Output:
All products : [Laptop, Mouse, Keyboard]
Found Laptop : Laptop
Size         : 3

2 — public Methods

public methods form the callable API surface of a class. They represent operations that callers are expected to invoke. Everything else — helpers, validators, internal state management — should be private or less visible.

// File: com/devstackflow/service/OrderService.java package com.devstackflow.service; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; public class OrderService { // private state — not accessible from outside private final Map<String, Double> orders = new HashMap<>(); private int orderCounter = 1; // public methods — the intentional API public String createOrder(String customerId, double amount) { validateInput(customerId, amount); // private helper String orderId = generateOrderId(); // private helper orders.put(orderId, amount); System.out.println("Order created: " + orderId + " | Rs." + amount); return orderId; } public boolean cancelOrder(String orderId) { if (!orders.containsKey(orderId)) return false; orders.remove(orderId); System.out.println("Order cancelled: " + orderId); return true; } public double getOrderAmount(String orderId) { return orders.getOrDefault(orderId, -1.0); } public int getActiveOrderCount() { return orders.size(); } // private helpers — implementation detail, not visible outside private void validateInput(String customerId, double amount) { if (customerId == null || customerId.isBlank()) throw new IllegalArgumentException("Customer ID required."); if (amount <= 0) throw new IllegalArgumentException("Amount must be positive."); } private String generateOrderId() { return String.format("ORD-%04d", orderCounter++); } }
// File: OrderServiceDemo.java package com.devstackflow.demo; import com.devstackflow.service.OrderService; public class OrderServiceDemo { public static void main(String[] args) { OrderService service = new OrderService(); String id1 = service.createOrder("CUST-101", 1299.50); String id2 = service.createOrder("CUST-102", 4999.00); String id3 = service.createOrder("CUST-103", 799.00); System.out.println("Active orders: " + service.getActiveOrderCount()); System.out.println("ORD-0001 amt : Rs." + service.getOrderAmount(id1)); service.cancelOrder(id2); System.out.println("After cancel : " + service.getActiveOrderCount()); // service.validateInput(...) ← compile error — private method // service.generateOrderId() ← compile error — private method // service.orders.put(...) ← compile error — private field } }
Output:
Order created: ORD-0001 | Rs.1299.5
Order created: ORD-0002 | Rs.4999.0
Order created: ORD-0003 | Rs.799.0
Active orders: 3
ORD-0001 amt : Rs.1299.5
Order cancelled: ORD-0002
After cancel : 2

3 — public static final Fields — Constants

The one case where public fields are appropriate: constants. A public static final field is immutable — its value can never change — so there is no risk of external code corrupting state through direct access.

// File: com/devstackflow/config/AppConstants.java package com.devstackflow.config; public final class AppConstants { // Prevent instantiation — this class is purely a constant holder private AppConstants() { } // public static final — constants safe for direct access public static final String APP_NAME = "DevStackFlow"; public static final String APP_VERSION = "2.1.0"; public static final int MAX_RETRY_COUNT = 3; public static final long SESSION_TIMEOUT = 30 * 60 * 1000L; // 30 minutes in ms public static final double GST_RATE = 0.18; public static final String BASE_CURRENCY = "INR"; // HTTP Status codes public static final int HTTP_OK = 200; public static final int HTTP_CREATED = 201; public static final int HTTP_BAD_REQUEST = 400; public static final int HTTP_UNAUTHORIZED = 401; public static final int HTTP_NOT_FOUND = 404; public static final int HTTP_SERVER_ERROR = 500; }
// File: ConstantsDemo.java package com.devstackflow.demo; import com.devstackflow.config.AppConstants; import static com.devstackflow.config.AppConstants.MAX_RETRY_COUNT; import static com.devstackflow.config.AppConstants.GST_RATE; public class ConstantsDemo { public static void main(String[] args) { // Accessed via class name — clear and unambiguous System.out.println("App : " + AppConstants.APP_NAME + " v" + AppConstants.APP_VERSION); System.out.println("Session : " + AppConstants.SESSION_TIMEOUT / 60000 + " mins"); System.out.println("Base curr : " + AppConstants.BASE_CURRENCY); System.out.println("HTTP OK : " + AppConstants.HTTP_OK); // Accessed via static import — clean in math-heavy methods double price = 2499.0; double total = price + (price * GST_RATE); System.out.printf("Price + GST: Rs.%.2f%n", total); System.out.println("Max retries: " + MAX_RETRY_COUNT); // AppConstants.APP_NAME = "Hacked"; ← compile error — final field // new AppConstants(); ← compile error — private constructor } }
Output:
App        : DevStackFlow v2.1.0
Session    : 30 mins
Base curr  : INR
HTTP OK    : 200
Price + GST: Rs.2948.82
Max retries: 3

4 — public Constructors vs Restricted Constructors

public constructors allow unrestricted instantiation. Sometimes you want to control how objects are created — use private or protected constructors combined with static factory methods.

// File: com/devstackflow/model/DatabaseConnection.java package com.devstackflow.model; public class DatabaseConnection { private static DatabaseConnection instance; // singleton — only one instance ever private final String host; private final int port; private boolean connected; // private constructor — callers cannot use new DatabaseConnection(...) private DatabaseConnection(String host, int port) { this.host = host; this.port = port; this.connected = false; } // public static factory — the controlled way to get an instance public static DatabaseConnection getInstance(String host, int port) { if (instance == null) { instance = new DatabaseConnection(host, port); System.out.println("New connection created: " + host + ":" + port); } else { System.out.println("Returning existing connection."); } return instance; } // public methods — the API public void connect() { connected = true; System.out.println("Connected to " + host + ":" + port); } public void disconnect() { connected = false; System.out.println("Disconnected from " + host + ":" + port); } public boolean isConnected() { return connected; } public String getHost() { return host; } public int getPort() { return port; } }
// File: DbConnectionDemo.java package com.devstackflow.demo; import com.devstackflow.model.DatabaseConnection; public class DbConnectionDemo { public static void main(String[] args) { // Cannot use new — private constructor // DatabaseConnection conn = new DatabaseConnection("localhost", 5432); ← error // Must use the public static factory method DatabaseConnection conn1 = DatabaseConnection.getInstance("db.meesho.in", 5432); conn1.connect(); System.out.println("Connected : " + conn1.isConnected()); // Second call — returns the same instance DatabaseConnection conn2 = DatabaseConnection.getInstance("db.meesho.in", 5432); System.out.println("Same instance: " + (conn1 == conn2)); // true — singleton conn1.disconnect(); } }
Output:
New connection created: db.meesho.in:5432
Connected to db.meesho.in:5432
Connected : true
Returning existing connection.
Same instance: true
Disconnected from db.meesho.in:5432

5 — public Methods in Interfaces

Every method declared in an interface is implicitly public and abstract (unless it is default or static). When a class implements an interface, its implementations must be public.

// File: com/devstackflow/contract/Payable.java package com.devstackflow.contract; public interface Payable { // implicitly public and abstract — no keyword needed String initiatePayment(String orderId, double amount); boolean verifyPayment(String transactionId); boolean refundPayment(String transactionId, double amount); // default method — has implementation, still implicitly public default String getPaymentSummary(String orderId, double amount) { return String.format("Payment for order %s: Rs.%.2f", orderId, amount); } // static method — utility, still implicitly public static boolean isValidAmount(double amount) { return amount > 0 && amount <= 100_000; } }
// File: com/devstackflow/payment/UpiPayment.java package com.devstackflow.payment; import com.devstackflow.contract.Payable; public class UpiPayment implements Payable { private final String upiId; public UpiPayment(String upiId) { this.upiId = upiId; } // Must be public — interface contract requires it @Override public String initiatePayment(String orderId, double amount) { if (!Payable.isValidAmount(amount)) { return "FAILED: invalid amount"; } String txnId = "UPI-" + System.currentTimeMillis(); System.out.println("UPI payment initiated: " + txnId + " | " + upiId + " | Rs." + amount); return txnId; } @Override public boolean verifyPayment(String transactionId) { System.out.println("Verified: " + transactionId); return transactionId.startsWith("UPI-"); } @Override public boolean refundPayment(String transactionId, double amount) { System.out.println("Refund Rs." + amount + " for: " + transactionId); return true; } }
// File: PayableDemo.java package com.devstackflow.demo; import com.devstackflow.contract.Payable; import com.devstackflow.payment.UpiPayment; public class PayableDemo { public static void main(String[] args) { Payable payment = new UpiPayment("priya@meesho"); // Static interface method — called on the interface itself System.out.println("Valid Rs.500 : " + Payable.isValidAmount(500.0)); System.out.println("Valid Rs.-1 : " + Payable.isValidAmount(-1.0)); System.out.println(); String txnId = payment.initiatePayment("ORD-001", 1299.0); System.out.println("TxnId : " + txnId); System.out.println("Verify: " + payment.verifyPayment(txnId)); System.out.println("Summary: " + payment.getPaymentSummary("ORD-001", 1299.0)); payment.refundPayment(txnId, 1299.0); } }
Output:
Valid Rs.500  : true
Valid Rs.-1   : false

UPI payment initiated: UPI-1705312200000 | priya@meesho | Rs.1299.0
TxnId : UPI-1705312200000
Verify: true
Summary: Payment for order ORD-001: Rs.1299.00
Refund Rs.1299.0 for: UPI-1705312200000

public — What to Expose vs What to Hide

Design rule — the public API should be:

  ✓ Stable   — callers depend on it; changing it breaks them
  ✓ Minimal  — expose only what callers genuinely need
  ✓ Safe     — public methods should validate before acting on state
  ✓ Named intentionally — method names should describe WHAT, not HOW

  What should NOT be public:
  ✗ Implementation details (how something works internally)
  ✗ Mutable fields (callers could corrupt state)
  ✗ Internal helpers (generateId(), validateInput(), etc.)
  ✗ Partially-built state (methods that only make sense mid-construction)

  The iceberg principle:
  ┌─────────────────────────────────┐
  │       PUBLIC API SURFACE        │  ← small, stable, intentional
  │  createOrder()  cancelOrder()   │
  │  getStatus()    getTotal()      │
  ├─────────────────────────────────┤
  │     (below the waterline)       │
  │  private fields                 │  ← large, hidden, free to change
  │  private helpers                │
  │  default package utilities      │
  │  protected extension hooks      │
  └─────────────────────────────────┘

Basic Example

public class Main { public static void main(String[] args) { Counter counter = new Counter(); counter.increment(); counter.increment(); counter.increment(); // public field is directly readable from another class System.out.println("Direct field access: " + counter.count); // public method is directly callable from another class System.out.println("Count: " + counter.getCount()); } } // not public — but its public members are still reachable from Main class Counter { public int count = 0; // public field — visible outside this class public void increment() { // public method — visible outside this class count++; } public int getCount() { return count; } }

Output

Direct field access: 3
Count: 3

How it works:

  1. ›Counter declares count, increment(), and getCount() as public, so any other class in the file can reach them directly.
  2. ›Main creates a Counter object and calls increment() three times — each call is allowed only because increment() is public.
  3. ›Main also reads counter.count directly — this is legal only because count is a public field; if it were private, that line would fail to compile.
  4. ›getCount() returns the same value, showing that both direct field access and method access work identically once a member is public.

Best Practices

Design the public API before writing the implementation. Write the public method signatures first — name them clearly, define their parameters and return types. Then implement them privately. This API-first approach produces stable, well-named public surfaces and forces separation between contract and implementation from the start.

Keep the public surface as small as possible. Every public method you add is a commitment — callers will depend on it, and changing or removing it later requires updating every caller. A method that is public today but should be private can be made private if no callers exist yet. Once callers exist, changing visibility is a breaking change. Err towards narrower visibility and widen when genuinely needed.

Never make mutable instance fields public. public double price lets any caller write product.price = -1000 without any validation. Always make instance fields private and provide controlled access through getter/setter methods that enforce business rules. The only safe public fields are static final constants — immutable, so direct access cannot corrupt state.

Document every public method with its contract. public methods are promises to callers. Document what the method expects (preconditions), what it guarantees (postconditions), what exceptions it throws, and whether it is thread-safe. Private methods are implementation details that only you maintain — they need less formal documentation.

Common Mistakes

Mistake 1 — Accidental public on Internal Helpers

// WRONG — generateId is an implementation detail public class OrderService { public String generateId() { // accidental public — now callers depend on this return "ORD-" + System.currentTimeMillis(); } public String createOrder(...) { String id = generateId(); // callers now bypass createOrder and call this directly ... } } // CORRECT — generateId is private — callers use createOrder only public class OrderService { public String createOrder(...) { ... } // only this is public private String generateId() { ... } // hidden helper }

Mistake 2 — public Mutable Fields

// WRONG — any caller can corrupt state public class Product { public double price; // product.price = -999 is valid — no validation public int stockCount; // product.stockCount = -50 is valid — wrong } // CORRECT — all state changes go through validated methods public class Product { private double price; private int stockCount; public void setPrice(double price) { if (price < 0) throw new IllegalArgumentException("Price cannot be negative."); this.price = price; } public double getPrice() { return price; } }

Mistake 3 — Two public Classes in One File

// WRONG — file is named OrderService.java public class OrderService { } // matches filename — OK public class OrderHelper { } // compile error: class OrderHelper is public, // should be declared in a file named OrderHelper.java // Only ONE public class per .java file // The file name must match the public class name // CORRECT — move OrderHelper to its own file, or make it package-private class OrderHelper { } // no public — same file is fine

Mistake 4 — Implementing Interface Method as Non-public

interface Printable { void print(); // implicitly public } class Document implements Printable { // @Override // void print() { } // compile error — cannot reduce to package-private // // interface method is public; impl must be public @Override public void print() { // must be public System.out.println("Printing..."); } }

Interview Questions

Q1. What does the public access modifier mean in Java?

public is the least restrictive access modifier. A public class, method, field, or constructor is accessible from any class in any package in the entire program. There are no package, inheritance, or proximity restrictions — any code that can see the class can call its public members. On top-level classes, public means the class is importable from any package. On members, public means any caller that holds an instance or knows the class name can access the member directly.

Q2. What is the difference between a public method and a public static method?

A public instance method requires an object instance to be called — obj.method(). It has access to this and can operate on the object's fields. A public static method belongs to the class itself — ClassName.method() — and does not need an instance. It cannot access instance fields or this. Static methods are used for utilities, factory methods, and operations that don't depend on object state. Math.max(), String.valueOf(), and Arrays.sort() are all public static methods.

Q3. Can a public method be overridden in a subclass?

Yes. A public method can be overridden by any subclass — in the same package or in a different package. The overriding method must also be public (or remain public — it cannot be narrowed). When overriding, the @Override annotation makes the intent explicit and causes a compile error if the method signature does not actually match any parent method. Polymorphism uses the overridden method when the subclass instance is accessed through a parent-type reference.

Q4. Why are all interface methods implicitly public?

An interface defines a contract that implementing classes must fulfil for all callers. If interface methods could be private or default, callers would not know which methods they could actually invoke — the contract would be inconsistent. Making all abstract interface methods implicitly public ensures that every implementation class exposes the full interface contract to all callers, maintaining the fundamental premise of interface-based programming.

Q5. Is it good practice to have public fields in Java?

For mutable instance fields — no. A public instance field bypasses encapsulation: any caller can read and write it without restriction, making validation impossible, breaking invariants, and coupling external code to the internal representation. Changing the field name or type later becomes a breaking change for all callers. The only appropriate use of public fields is public static final constants — which are immutable and safe for direct access.

Q6. What happens when a top-level class does not have public access?

A top-level class without public gets default (package-private) access. It is only visible within its own package — it cannot be imported by classes in other packages, and those other-package classes cannot use it at all. This is appropriate for internal helper classes that are only needed within a single package. One file can have only one public top-level class, and the file name must match that class's name exactly.

FAQs

Can you have a public method inside a private nested class?

Yes. A public method inside a private nested class is technically public relative to the nested class, but the enclosing constraint wins — since the nested class itself is private, only code inside the enclosing outer class can access the nested class and therefore its methods. The public on the method has no practical effect from outside the outer class.

Can you call a public method without creating an object?

Only if the method is also static. public static methods are called on the class: ClassName.method(). A public instance method always requires an object reference: obj.method(). Attempting to call an instance method without an object — ClassName.instanceMethod() — causes a compile error.

Does making a method public affect performance?

No. The public modifier has no effect on bytecode or runtime performance. The JVM does not execute any visibility check at runtime for public members — the access control check was already performed and resolved by the compiler. public, private, protected, and default methods all execute at identical speed.

Can you have a public class in the default (unnamed) package?

Yes, but it creates a problem. A public class in the default package cannot be imported by named-package classes — the import statement requires a package prefix that the default package does not have. So while the class is technically public, it is only usable within the same default-package context. This is another reason to avoid the default package in production code.

What is the purpose of a public constructor versus a public static factory method?

A public constructor is the standard way to create instances — new ClassName(args). A public static factory method — like getInstance(), of(), or create() — controls how and whether new instances are created. Factory methods can enforce singleton patterns, return subtype instances, return cached instances, validate parameters before creation, and have meaningful names that new cannot express. Optional.of(), List.of(), and LocalDate.of() are all factory methods.

Summary

public is the declaration that says: this is part of the outward-facing contract. It grants unrestricted access — any class, any package, any context. Used on a class it means importable everywhere. Used on a method it means callable by anyone. Used on a constant field (static final) it means readable by anyone without risk.

The discipline of public is minimal exposure. Every public method is a commitment that callers will depend on. Every unnecessary public member is a future maintenance liability. The right practice: start with private, expose through public only what external callers genuinely need, and document every public method as a stable contract.

What to Read Next