Java Class Declaration: Syntax and Structure
java class declaration: Learn Java class declaration syntax, access modifiers, inheritance, interfaces, nested classes, and common mistakes with practical code examples.
A Java class declaration defines the type's name, its visibility, its relationship to other types, and the structure of its instances. The declaration is the first thing the compiler reads about a type, and it determines what code elsewhere in the program is allowed to do with that type. Getting the declaration right matters because changing it later often forces changes in every file that references the class.
The Core Declaration Syntax
The minimal class declaration in Java is:
class Account { }
This declares a package-private class named Account with no explicit parent class and no interfaces. The compiler treats it as extending java.lang.Object implicitly. The class body is empty, so instances of Account carry no state and expose no behavior beyond what Object provides.
The full declaration form is:
[access modifier] [class modifiers] class Name [extends Parent] [implements Interface1, Interface2] { // body }
Each part is optional except class Name and the body braces. A class can have at most one extends clause, because Java supports single inheritance for classes. The implements clause can list multiple interfaces separated by commas.
Access Modifiers on the Declaration
Java gives a top-level class exactly two visibility options: public or package-private. There is no protected or private for top-level classes.
public class PublicAccount { } class PackageAccount { }
public makes the class visible to code in any package that imports it. Package-private (no modifier) restricts visibility to classes in the same package. The choice affects the public API surface of your codebase. A package-private class is a reasonable default for implementation details that should not leak into other packages.
For nested classes, the rules differ. A nested class can be private, protected, or public, because its visibility is scoped relative to the enclosing class.
Class Modifiers: abstract, final, and sealed
Three modifiers change what other code can do with the class itself.
An abstract class cannot be instantiated. It exists to be extended:
public abstract class PaymentProcessor { public abstract boolean process(Payment payment); }
Any concrete subclass must implement process. If a subclass does not, it must also be declared abstract.
A final class cannot be extended at all:
public final class OrderId { private final String value; public OrderId(String value) { this.value = value; } }
This is useful for value types where subclassing would break invariants, such as immutability or equality semantics.
A sealed class (Java 17 and later) restricts which classes may extend it. The permitted subclasses are listed explicitly:
public sealed class Shape permits Circle, Rectangle, Triangle { }
Every permitted subclass must be in the same module or package, and each must declare itself final, sealed, or non-sealed. Sealed classes give you exhaustive pattern matching over a known set of subtypes, which is useful in domain modeling.
Declaring Fields, Constructors, and Methods
The class body contains the members that define state and behavior. Field declarations establish the instance state:
public class BankAccount { private final String accountNumber; private double balance; public BankAccount(String accountNumber, double initialBalance) { this.accountNumber = accountNumber; this.balance = initialBalance; } public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive"); } this.balance += amount; } public double getBalance() { return balance; } }
The constructor runs when an instance is created with new. It must initialize every field that has no default value, or the compiler will reject the code. Fields of primitive type default to zero or false, and reference fields default to null, but relying on those defaults for non-final fields is usually a design smell.
Methods declared in the class body define the behavior available to callers. Access modifiers on methods (private, package-private, protected, public) control which code can invoke them, independently of the class-level visibility.
Inheritance and Interface Implementation
The extends clause names the direct parent class. Java allows only one parent, but the parent can itself extend another class, forming an inheritance chain.
public class SavingsAccount extends BankAccount { private final double interestRate; public SavingsAccount(String accountNumber, double initialBalance, double interestRate) { super(accountNumber, initialBalance); this.interestRate = interestRate; } }
The super(...) call in the constructor must be the first statement. If the parent has no no-argument constructor, the child must call a specific parent constructor explicitly.
The implements clause attaches interfaces. A class can implement any number of them:
public class AuditedAccount extends BankAccount implements Auditable, Serializable { // implementation }
Interfaces define a contract without implementation details, though default methods can provide shared behavior. Implementing an interface requires the class to provide concrete implementations of all abstract interface methods, unless the class is abstract.
Nested Class Declarations
A class declared inside another class is a nested class. Java supports four forms, and the declaration syntax determines which form you get.
A static nested class is declared with the static modifier:
public class Order { public static class LineItem { private final String sku; private final int quantity; public LineItem(String sku, int quantity) { this.sku = sku; this.quantity = quantity; } } }
A static nested class has no reference to the enclosing instance. It behaves like a top-level class grouped under the outer class's name.
An inner class is declared without static:
public class ShoppingCart { private final List<Item> items = new ArrayList<>(); public class CartIterator { public int size() { return items.size(); } } }
An inner class instance holds an implicit reference to the enclosing instance, so it can access the outer class's fields and methods. Creating one requires an enclosing instance first: cart.new CartIterator().
Local classes are declared inside a method body, and anonymous classes are declared inline with new. Both have restricted scope and are used when the class is needed only within that method.
Common Declaration Mistakes
One frequent mistake is declaring a class final when it should be open for extension. This surfaces later when a test needs to subclass the class to inject a fake dependency. If the class is not designed for inheritance, final is a reasonable choice, but it should be deliberate.
Another mistake is omitting the implements clause and relying on reflection or casting to treat a class as an interface type. If a class is meant to satisfy an interface contract, declare it explicitly. Reflection-based discovery is brittle and fails at runtime rather than at compile time.
A third issue is declaring fields as public in the class body. This exposes mutable state to every caller and makes it impossible to enforce invariants. Prefer private fields with controlled accessor methods.
Runtime Behavior of Class Declarations
The declaration also affects runtime behavior. The JVM loads a class lazily, on first active use. Static initializers and static field assignments run when the class is initialized, which happens once per class loader:
public class Config { public static final String ENV = System.getenv("APP_ENV"); }
If Config is referenced in many places, the static initializer runs only once, on the first reference that triggers initialization. This matters for classes that read configuration or establish connections in static blocks, because the timing of that work depends on where the class is first used.
Class declarations that participate in inheritance also affect method dispatch. When a method is invoked on an instance, the JVM resolves the method based on the runtime type, not the declared type. This is why a BankAccount reference can call an overridden method on a SavingsAccount instance. Understanding this behavior helps when designing class hierarchies, because the declared type determines what callers can invoke, while the runtime type determines which implementation runs.