☕ Java

Classes

A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.

Class Anatomy

A class declaration begins with optional modifiers (public, final, abstract), the class keyword, the class name, optional extends and implements clauses, and then the class body enclosed in braces. Inside the body the class can declare fields (data), constructors (initialisation logic), methods (behaviour), nested classes, and static initialisers. Java enforces one rule about top-level class declarations: only one public class may exist per source file, and the file name must exactly match that class name including capitalisation. Non-public classes can appear in the same file but are rarely declared there in practice — each class typically lives in its own file. By convention, class names use UpperCamelCase (also called PascalCase): each word begins with a capital letter, with no underscores. This convention is universal in the Java ecosystem and its violation is immediately visible to any Java developer reading the code.
Java
// ── Class anatomy ────────────────────────────────────────────────────
//
// [modifiers] class ClassName [extends SuperClass]
//                             [implements Interface1, Interface2] {
//
//     // 1. Static constants / fields
//     // 2. Instance fields
//     // 3. Static initialiser blocks
//     // 4. Constructors
//     // 5. Instance methods
//     // 6. Static methods
//     // 7. Nested classes
// }

public class BankAccount {

    // ── 1. Static constants ───────────────────────────────────────────
    private static final BigDecimal OVERDRAFT_LIMIT =
        new BigDecimal("-500.00");
    private static final int ACCOUNT_NUMBER_LENGTH = 8;

    // ── 2. Instance fields ────────────────────────────────────────────
    private final String accountNumber;
    private final String ownerName;
    private       BigDecimal balance;
    private       int transactionCount;

    // ── 3. Static initialiser (runs once when class is loaded) ────────
    static {
        System.out.println("BankAccount class loaded");
    }

    // ── 4. Constructor ─────────────────────────────────────────────────
    public BankAccount(String ownerName, BigDecimal initialBalance) {
        this.accountNumber    = generateAccountNumber();
        this.ownerName        = ownerName;
        this.balance          = initialBalance;
        this.transactionCount = 0;
    }

    // ── 5. Instance methods ───────────────────────────────────────────
    public void deposit(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException(
                "Deposit amount must be positive");
        }
        balance = balance.add(amount);
        transactionCount++;
    }

    public void withdraw(BigDecimal amount) {
        BigDecimal projected = balance.subtract(amount);
        if (projected.compareTo(OVERDRAFT_LIMIT) < 0) {
            throw new InsufficientFundsException(
                "Withdrawal would exceed overdraft limit");
        }
        balance = balance.subtract(amount);
        transactionCount++;
    }

    public BigDecimal getBalance()        { return balance;           }
    public String     getAccountNumber()  { return accountNumber;     }
    public String     getOwnerName()      { return ownerName;         }
    public int        getTransactionCount(){ return transactionCount; }

    // ── 6. Static methods ─────────────────────────────────────────────
    private static String generateAccountNumber() {
        return String.format("%0" + ACCOUNT_NUMBER_LENGTH + "d",
            (long)(Math.random() * Math.pow(10, ACCOUNT_NUMBER_LENGTH)));
    }
}

Fields — Instance and Static

Fields store the state of an object (instance fields) or the state shared by all objects of the class (static fields). An instance field has a separate copy in every object — each BankAccount has its own balance. A static field has exactly one copy in the class, shared by all instances — a counter of how many accounts have been created. Fields should almost always be declared private. This is not pedantry — it is the enforcement of encapsulation. When a field is private, its class has complete control over every read and write. If a validation rule must be applied when the balance changes, the class can enforce it in the setBalance method and trust that no external code bypasses it. If the field were public, any code anywhere in the program could write any value to it, making invariants impossible to maintain. The final modifier on a field means it can be assigned exactly once — either in the declaration or in every constructor. Final instance fields communicate to readers that the value is set at construction and never changes, which simplifies reasoning about object state.
Java
public class Counter {

    // ── Static field — ONE copy, shared by ALL Counter instances ──────
    private static int totalCreated = 0;

    // ── Instance fields — separate copy in EVERY Counter object ───────
    private final String name;    // final: assigned once, never changes
    private       int    count;   // mutable: changes throughout object life

    public Counter(String name) {
        this.name  = name;
        this.count = 0;
        totalCreated++;           // shared field updated by every constructor
    }

    public void increment() { count++;  }
    public void decrement() { count--;  }
    public void reset()     { count = 0; }

    public int    getCount()        { return count;        }
    public String getName()         { return name;         }

    // ── Static getter — access without an instance ────────────────────
    public static int getTotalCreated() { return totalCreated; }
}

// ── Static vs instance demonstration ─────────────────────────────────
Counter c1 = new Counter("visits");
Counter c2 = new Counter("errors");
Counter c3 = new Counter("logins");

c1.increment();
c1.increment();
c2.increment();

System.out.println(c1.getCount());            // 2 — c1's own field
System.out.println(c2.getCount());            // 1 — c2's own field
System.out.println(Counter.getTotalCreated()); // 3 — shared static field

// ── Field initialisation options ──────────────────────────────────────
public class Config {

    // Direct initialisation
    private String host = "localhost";
    private int    port = 8080;

    // Initialisation via static initialiser block
    private static final Map<String, String> DEFAULTS;
    static {
        DEFAULTS = new HashMap<>();
        DEFAULTS.put("host", "localhost");
        DEFAULTS.put("port", "8080");
        DEFAULTS.put("timeout", "30");
    }

    // Initialisation deferred to constructor
    private final String environment;
    public Config(String environment) {
        this.environment = environment;
        // port could also be set here based on environment
        if ("prod".equals(environment)) this.port = 443;
    }
}

Access Modifiers

Java has four access levels, controlled by three keywords. public means accessible from anywhere. protected means accessible from within the package and from subclasses in any package. The default (package-private, no keyword) means accessible only within the same package. private means accessible only within the declaring class itself. Access modifiers are not just about security — they define the public API of a class. Public members are a contract with the outside world: changing them may break other code. Private members are implementation details that can be changed freely without affecting any external code. Choosing access modifiers carefully is therefore a design decision about what should be stable and what should be flexible. The principle of least privilege applies to class design: grant the minimum access required. Start with private. Widen to package-private if the class is part of a collaborating group. Widen to protected only when subclass access is genuinely needed. Make something public only when external callers must access it. This ordering prevents accidental exposure and keeps the public API small, stable, and intentional.
Java
public class AccessExample {

    // ── private — only this class can access ─────────────────────────
    private int secret = 42;
    private void internalHelper() { ... }

    // ── package-private (default) — classes in same package ──────────
    int packageData = 10;
    void packageMethod() { ... }

    // ── protectedthis class, same package, AND subclasses ─────────
    protected String sharedState = "shared";
    protected void overridableMethod() { ... }

    // ── public — accessible from anywhere ────────────────────────────
    public String publicData = "visible everywhere";
    public void publicMethod() { ... }
}

// ── Summary table ─────────────────────────────────────────────────────
//
// Modifier        Same class  Same package  Subclass  Any class
// ─────────────────────────────────────────────────────────────────
// private         YES         NO            NO        NO
// (default)       YES         YES           NO        NO
// protected       YES         YES           YES       NO
// public          YES         YES           YES       YES

// ── Real-world example: Employee class ───────────────────────────────
public class Employee {

    // private — implementation detail, validation enforced
    private String name;
    private double salary;
    private String taxId;           // sensitive — never expose directly

    // package-private — HR utilities in the same package can read
    int departmentCode;

    // protected — subclasses like Manager override this
    protected double calculateBonus() {
        return salary * 0.05;
    }

    // public API — stable contract for all callers
    public String getName()         { return name;   }
    public double getSalary()       { return salary; }
    public void   raiseSalary(double pct) {
        if (pct < 0 || pct > 50) {
            throw new IllegalArgumentException(
                "Raise percentage must be 0-50");
        }
        salary *= (1 + pct / 100);
    }
}

The this Keyword

The keyword this is a reference to the current object — the instance on which the method or constructor was called. It has three distinct uses. First, disambiguating field access when a parameter has the same name as a field: this.name = name assigns the parameter to the field. Second, calling another constructor from a constructor — this() — which must be the first statement and enables constructor chaining. Third, passing the current object as an argument to another method: listener.register(this). Constructor chaining with this() is a powerful pattern for avoiding code duplication. A class might have several constructors with different parameter sets. Rather than repeating the same initialisation logic in each, the less specific constructors delegate to the most specific one using this(). This ensures that there is one canonical constructor that contains all the real initialisation logic, and all other constructors feed into it.
Java
public class Person {

    private final String firstName;
    private final String lastName;
    private final int    age;
    private final String email;

    // ── Most specific constructor — all fields ────────────────────────
    public Person(String firstName, String lastName,
                  int age, String email) {
        // Use 'this.' to distinguish field from parameter
        this.firstName = firstName;
        this.lastName  = lastName;
        this.age       = age;
        this.email     = email;
    }

    // ── Constructor chaining — delegates to the full constructor ──────
    public Person(String firstName, String lastName, int age) {
        this(firstName, lastName, age, "");   // this() must be FIRST
    }

    public Person(String firstName, String lastName) {
        this(firstName, lastName, 0);         // chains upward
    }

    public Person(String fullName) {
        this(fullName.split(" ")[0],          // chains to two-arg
             fullName.split(" ")[1]);
    }

    // ── 'this' as a return value — fluent builder style ───────────────
    public Person withEmail(String email) {
        return new Person(
            this.firstName, this.lastName,
            this.age, email);
    }

    // ── 'this' passed as an argument ──────────────────────────────────
    public void registerWith(PersonRegistry registry) {
        registry.register(this);   // pass the current object
    }

    public String getFullName() {
        return firstName + " " + lastName;
    }

    // ── toString uses 'this' implicitly ───────────────────────────────
    @Override
    public String toString() {
        return "Person{name=" + firstName + " " + lastName +
               ", age=" + age + ", email=" + email + "}";
    }
}

// ── Constructor chaining in action ────────────────────────────────────
Person p1 = new Person("Alice", "Smith", 30, "alice@example.com");
Person p2 = new Person("Bob", "Jones", 25);   // email = ""
Person p3 = new Person("Carol Lee");           // age = 0, email = ""

Static Members and Class-Level Behaviour

Static members belong to the class itself, not to any instance. A static field has one copy regardless of how many instances exist. A static method can be called without any instance. Static members are useful for utility functions that do not depend on object state (Math.sqrt), factory methods that construct objects (List.of), constants shared by all instances, and tracking class-level data such as instance counts. The critical rule: static methods cannot access instance fields or call instance methods directly, because there is no current object (no this) in a static context. The compiler enforces this. Instance methods, however, can freely access static members because static members always exist. Static inner classes and static blocks round out the static landscape. A static initialiser block runs once when the class is first loaded by the JVM, before any constructor runs. It is used for complex static field initialisation that cannot fit in a single declaration expression.
Java
public class MathUtils {

    // ── Static constants — shared, immutable ──────────────────────────
    public static final double PI     = 3.14159265358979;
    public static final double E      = 2.71828182845905;
    public static final double SQRT_2 = 1.41421356237310;

    // ── Static field — tracks class-level state ───────────────────────
    private static int operationCount = 0;

    // ── Private constructor — utility class, no instances ────────────
    private MathUtils() {
        throw new UnsupportedOperationException(
            "Utility class cannot be instantiated");
    }

    // ── Static methods — no 'this', no instance fields ────────────────
    public static double circleArea(double radius) {
        operationCount++;
        return PI * radius * radius;
    }

    public static double hypotenuse(double a, double b) {
        operationCount++;
        return Math.sqrt(a * a + b * b);
    }

    public static boolean isPrime(int n) {
        operationCount++;
        if (n < 2) return false;
        for (int i = 2; i * i <= n; i++) {
            if (n % i == 0) return false;
        }
        return true;
    }

    public static int getOperationCount() { return operationCount; }

    // ── Static initialiser — complex static setup ─────────────────────
    private static final Map<String, Double> CONSTANTS;
    static {
        CONSTANTS = new HashMap<>();
        CONSTANTS.put("pi",    PI);
        CONSTANTS.put("e",     E);
        CONSTANTS.put("sqrt2", SQRT_2);
        CONSTANTS.put("phi",   (1 + Math.sqrt(5)) / 2);   // golden ratio
    }

    public static double getConstant(String name) {
        return CONSTANTS.getOrDefault(name, 0.0);
    }
}

// ── Usage — called on the class, no instance needed ───────────────────
double area  = MathUtils.circleArea(5.0);   // 78.54...
double hyp   = MathUtils.hypotenuse(3, 4);  // 5.0
boolean prime = MathUtils.isPrime(17);       // true
System.out.println(MathUtils.getOperationCount()); // 3

Related Topics in Object-Oriented Programming

OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.
Default Constructor
A default constructor is a constructor that takes no parameters. If you write a class without any constructor, the Java compiler automatically inserts a no-argument constructor that calls the superclass no-argument constructor and does nothing else. The moment you define any constructor yourself — with or without parameters — the compiler no longer inserts the default constructor. Understanding when the default constructor exists, when it disappears, and what it initialises is fundamental to understanding how Java objects come into existence.