Java Class & Methods — Declaration, Fields, Constructors & Best Practices
Everything you need to know about Java Classes and their Methods — class declaration, fields, constructors, instance & static members, getter/setter, the 'this' keyword, inner classes, anti-patterns, and real-world production code examples.
Last Updated
March 2026
Read Time
26 min
Level
Beginner
Chapter
14 of 35
What is a Class in Java?
A class in Java is the most fundamental building block of object-oriented programming. It is a blueprint or template that defines: (1) the state an object holds — represented by fields (variables), and (2) the behaviour an object can perform — represented by methods. A class itself occupies no memory for data — it is only when you create an object from the class that memory is allocated.
In Java, everything lives inside a class. There are no standalone variables or functions at the top level of a file. Every field, every method, every constructor must be declared inside a class body. This is what makes Java a purely object-oriented language at the structural level — the class is the mandatory container for all code.
Real-world analogy: think of a class as an architectural blueprint for a house. The blueprint defines how many rooms there are (fields), what each room is for, and what actions can be performed in the house (methods). Using the blueprint, you can construct many actual houses (objects) — each one is a separate physical structure with its own rooms, but all follow the same blueprint design.
Class Declaration & Syntax — Anatomy of a Java Class
A Java class declaration follows a specific structure. Understanding each part is essential before writing any class.
[accessModifier] [modifiers] class ClassName [extends ParentClass] [implements Interface1, Interface2] { // fields // constructors // methods // inner classes } Minimum valid class: class Dog { }
• Must start with a letter, $ or _ (not a digit) • PascalCase convention: every word capitalised — BankAccount, OrderService, HttpRequest • Should be a NOUN or noun phrase — represents a thing, not an action • Match the filename exactly (case-sensitive) for public classes • Avoid: Manager, Helper, Utils as standalone names — too vague
Access modifiers: public (any package), default/package-private (same package only). Non-access modifiers: • final — class cannot be subclassed (e.g., String, Integer) • abstract — class cannot be instantiated; may have abstract methods • strictfp — floating-point calculations use strict IEEE 754 Top-level classes cannot be private or protected.
// File: ClassDeclarationDemo.java
// One public class per file — filename must match
// Basic class with all structural elements labelled
public class BankAccount {
// ── FIELDS (State) ──────────────────────────────
private static int totalAccounts = 0; // static field — shared
private final String accountNumber; // instance field — immutable
private String ownerName; // instance field — mutable
private double balance; // instance field — mutable
// ── CONSTANTS ────────────────────────────────────
public static final double MIN_BALANCE = 500.0;
// ── CONSTRUCTOR ──────────────────────────────────
public BankAccount(String accountNumber, String ownerName, double initialDeposit) {
this.accountNumber = accountNumber;
this.ownerName = ownerName;
this.balance = initialDeposit;
totalAccounts++; // Update class-level counter
}
// ── INSTANCE METHODS (Behaviour) ─────────────────
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
this.balance += amount;
}
public boolean withdraw(double amount) {
if (amount <= 0 || amount > balance) return false;
this.balance -= amount;
return true;
}
// ── GETTERS ──────────────────────────────────────
public String getAccountNumber() { return accountNumber; }
public String getOwnerName() { return ownerName; }
public double getBalance() { return balance; }
// ── STATIC METHOD (Class-level) ───────────────────
public static int getTotalAccounts() { return totalAccounts; }
// ── toString ─────────────────────────────────────
@Override
public String toString() {
return String.format("BankAccount[%s | %s | ₹%.2f]",
accountNumber, ownerName, balance);
}
}
// Second class in same file — must NOT be public
class AccountDemo {
public static void main(String[] args) {
BankAccount acc1 = new BankAccount("ACC001", "Ravi", 10000);
BankAccount acc2 = new BankAccount("ACC002", "Priya", 25000);
acc1.deposit(5000);
acc1.withdraw(2000);
System.out.println(acc1); // BankAccount[ACC001 | Ravi | ₹13000.00]
System.out.println(acc2); // BankAccount[ACC002 | Priya | ₹25000.00]
System.out.println("Total accounts: " + BankAccount.getTotalAccounts()); // 2
}
}Class vs Object — Blueprint vs Instance
The distinction between a class and an object is the cornerstone of object-oriented programming. Confusing the two leads to some of the most common Java mistakes — especially the infamous 'non-static field cannot be referenced from a static context' compile error.
public class Student {
// Fields defined in CLASS — each OBJECT gets its own copy
String name;
int rollNumber;
double cgpa;
// Constructor
public Student(String name, int rollNumber, double cgpa) {
this.name = name;
this.rollNumber = rollNumber;
this.cgpa = cgpa;
}
public void introduce() {
System.out.printf("Hi, I am %s (Roll: %d), CGPA: %.1f%n",
name, rollNumber, cgpa);
}
}
public class ClassVsObjectDemo {
public static void main(String[] args) {
// ✅ Creating OBJECTS from the Student CLASS
Student s1 = new Student("Ravi", 101, 8.5);
Student s2 = new Student("Priya", 102, 9.2);
Student s3 = new Student("Arjun", 103, 7.8);
// Each object has its OWN separate copy of fields
s1.introduce(); // Hi, I am Ravi (Roll: 101), CGPA: 8.5
s2.introduce(); // Hi, I am Priya (Roll: 102), CGPA: 9.2
s3.introduce(); // Hi, I am Arjun (Roll: 103), CGPA: 7.8
// Modifying one object does NOT affect others
s1.cgpa = 8.9;
System.out.println(s1.cgpa); // 8.9 — only s1 changed
System.out.println(s2.cgpa); // 9.2 — s2 unchanged
// null reference — variable declared but no object created
Student s4 = null;
// s4.introduce(); // ❌ NullPointerException — no object behind s4
}
}Fields — Instance Variables & Static Variables
Fields (also called member variables or class variables) represent the state of a class. Every object gets its own copy of instance fields, while static fields are shared across all objects of the class. Field design is one of the most important decisions in class design — the wrong choice here leads to fragile, unmaintainable objects.
Declared without 'static'. Each object gets its own separate copy. Hold the unique state for that particular object. Example: in a Student class, 'String name' and 'double cgpa' are instance fields — every student has their own name and cgpa. Accessed through an object reference: student.name. Best practice: always private — expose through getter/setter.
Declared with 'static'. Only ONE copy exists per class — shared by ALL objects. Changes to a static field through any object are visible to all other objects. Used for: counters (totalStudents), constants (MAX_RETRIES), shared configuration. Accessed via class name: Student.totalStudents. Best practice: static mutable fields are risky in multi-threaded programs — prefer static final constants.
'final' means the field can only be assigned ONCE — either at declaration or in the constructor. For primitives: the value is immutable. For objects: the reference cannot be reassigned (but the object's internal state can still be mutated). 'private final String id;' set in constructor = effectively immutable ID. Static final = a constant: 'public static final double PI = 3.14159;'. Immutable fields make objects thread-safe and easier to reason about.
Fields are initialised in this order: (1) Static fields and static initialiser blocks (when the class is first loaded — once only). (2) Instance fields set to their default values (0, false, null). (3) Instance field declarations with initialiser expressions. (4) Constructor body executes. This order matters when one field's default value depends on another — always initialise in constructor for clarity.
public class Employee {
// ── STATIC FIELDS ─────────────────────────────────
private static int employeeCount = 0; // Shared counter
public static final String COMPANY = "TechCorp"; // Constant
// ── INSTANCE FIELDS ───────────────────────────────
private final int employeeId; // Immutable after construction
private String name;
private String department;
private double salary;
private boolean active;
// ── STATIC INITIALISER BLOCK ──────────────────────
// Runs ONCE when class is first loaded
static {
System.out.println("Employee class loaded for: " + COMPANY);
}
// ── INSTANCE INITIALISER BLOCK ────────────────────
// Runs every time an object is created, BEFORE constructor
{
this.active = true; // Default — every new employee is active
employeeCount++; // Increment shared counter
this.employeeId = employeeCount; // Assign unique ID
}
// Constructor
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
// Static method — accesses only static field
public static int getEmployeeCount() { return employeeCount; }
// Instance methods
public int getEmployeeId() { return employeeId; }
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public boolean isActive() { return active; }
public void setSalary(double salary) {
if (salary < 0) throw new IllegalArgumentException("Salary cannot be negative");
this.salary = salary;
}
@Override
public String toString() {
return String.format("[%d] %s | %s | ₹%.0f | %s",
employeeId, name, department, salary, active ? "Active" : "Inactive");
}
}
// Usage:
// Employee e1 = new Employee("Ravi", "Engineering", 85000);
// Employee e2 = new Employee("Priya", "Design", 72000);
// System.out.println(e1); // [1] Ravi | Engineering | ₹85000 | Active
// System.out.println(e2); // [2] Priya | Design | ₹72000 | Active
// System.out.println(Employee.getEmployeeCount()); // 2
// System.out.println(Employee.COMPANY); // TechCorpConstructors — Initialising Objects Correctly
A constructor is a special block of code that is automatically called when an object is created using the new keyword. Its sole purpose is to initialise the object's state — setting field values, validating inputs, and setting up any initial conditions. A properly written constructor guarantees that an object is always in a valid, consistent state from the moment of creation.
1. Name MUST exactly match the class name (case-sensitive) 2. NO return type — not even void 3. Can have any access modifier (public, private, protected, default) 4. Can be overloaded (multiple constructors with different params) 5. If no constructor is written, Java provides a default no-arg constructor 6. If ANY constructor is written, the default is NOT provided automatically
'this(args)' inside a constructor calls ANOTHER constructor in the same class. Must be the FIRST statement in the constructor. Used to avoid code duplication when multiple constructors share setup logic. Example: a no-arg constructor can call a full constructor with default values: 'this("Unknown", 0)'. This is the DRY principle applied to constructors.
'super(args)' calls the PARENT class constructor. Must be the FIRST statement in a subclass constructor. If not explicitly written, Java automatically inserts 'super()' (no-arg parent constructor). If the parent has no no-arg constructor, the subclass MUST explicitly call super(args) — otherwise compile error. Used in inheritance to initialise the parent portion of the object.
A private constructor prevents external instantiation. Used in: (1) Singleton pattern — only one instance ever exists. (2) Utility class — all static methods, no instantiation needed. (3) Factory method pattern — object creation controlled through a static factory method that validates inputs before constructing. Example: 'LocalDate.of(2026, 3, 20)' is a factory method that validates the date before constructing.
public class Product {
private final String productId;
private String name;
private double price;
private int stock;
private String category;
// ── CONSTRUCTOR 1: Full constructor ───────────────
public Product(String productId, String name, double price,
int stock, String category) {
// Validation in constructor — object can never be invalid
if (productId == null || productId.isBlank())
throw new IllegalArgumentException("Product ID required");
if (price < 0)
throw new IllegalArgumentException("Price cannot be negative");
if (stock < 0)
throw new IllegalArgumentException("Stock cannot be negative");
this.productId = productId;
this.name = name;
this.price = price;
this.stock = stock;
this.category = category;
}
// ── CONSTRUCTOR 2: Without category (chaining to constructor 1) ──
public Product(String productId, String name, double price, int stock) {
this(productId, name, price, stock, "General"); // ✅ Constructor chaining
}
// ── CONSTRUCTOR 3: Minimum — just ID and name ─────
public Product(String productId, String name) {
this(productId, name, 0.0, 0); // ✅ Chains to constructor 2
}
// Getters
public String getProductId() { return productId; }
public String getName() { return name; }
public double getPrice() { return price; }
public int getStock() { return stock; }
public String getCategory() { return category; }
// Business methods
public boolean isAvailable() { return stock > 0; }
public void restock(int quantity) { this.stock += quantity; }
public boolean sell(int quantity) {
if (quantity > stock) return false;
this.stock -= quantity;
return true;
}
@Override
public String toString() {
return String.format("%s: %s | ₹%.2f | Stock: %d | %s",
productId, name, price, stock, category);
}
}
// Usage — three ways to create a Product:
// Product p1 = new Product("P001", "Laptop", 65000.0, 10, "Electronics");
// Product p2 = new Product("P002", "Mouse", 499.0, 50); // category = General
// Product p3 = new Product("P003", "Keyboard"); // price=0, stock=0Instance Methods in a Class — Behaviour of Objects
An instance method defines the behaviour of an object. It operates on the object's own state (instance fields) and can read or modify them. Instance methods represent the verbs of a class — what objects of that type can do. Every instance method implicitly receives a this reference — a pointer to the current object — allowing it to access the calling object's fields and other methods.
Instance methods that CHANGE the object's state. Examples: deposit(double amount), withdraw(double amount), addItem(Item item), deactivate(). They typically return void or a status value (boolean for success/failure). Should validate inputs before modifying state. Never expose raw field mutation — always wrap it in a method with business rules.
Instance methods that READ and return information about the object's state without modifying it. Examples: getBalance(), isActive(), calculateTax(), toString(). Should have NO side effects — calling a query method multiple times should give the same result. Boolean queries should be named with 'is', 'has', or 'can': isAvailable(), hasPermission(), canWithdraw(amount).
Instance methods that coordinate between objects — one object calls methods on another. Example: orderService.placeOrder(cart, payment) — the OrderService object collaborates with Cart and Payment objects. These methods are the 'glue' of object-oriented systems. They should delegate to other objects' methods rather than reaching into their internals (Law of Demeter).
public class ShoppingCart {
private final String cartId;
private final java.util.List<CartItem> items;
private String couponCode;
private double discountPercent;
public ShoppingCart(String cartId) {
this.cartId = cartId;
this.items = new java.util.ArrayList<>();
this.discountPercent = 0.0;
}
// ── COMMAND METHODS (Mutators) ────────────────────
public void addItem(CartItem item) {
if (item == null) throw new IllegalArgumentException("Item cannot be null");
// If item already exists, increase quantity
items.stream()
.filter(i -> i.getProductId().equals(item.getProductId()))
.findFirst()
.ifPresentOrElse(
i -> i.increaseQuantity(item.getQuantity()),
() -> items.add(item)
);
}
public boolean removeItem(String productId) {
return items.removeIf(i -> i.getProductId().equals(productId));
}
public void applyCoupon(String code, double discountPercent) {
if (code == null || code.isBlank())
throw new IllegalArgumentException("Coupon code required");
if (discountPercent <= 0 || discountPercent > 100)
throw new IllegalArgumentException("Invalid discount");
this.couponCode = code;
this.discountPercent = discountPercent;
}
public void clear() {
items.clear();
couponCode = null;
discountPercent = 0.0;
}
// ── QUERY METHODS (Accessors) ─────────────────────
public boolean isEmpty() { return items.isEmpty(); }
public int getItemCount() { return items.size(); }
public String getCartId() { return cartId; }
public double getSubtotal() {
return items.stream()
.mapToDouble(i -> i.getPrice() * i.getQuantity())
.sum();
}
public double getDiscountAmount() {
return getSubtotal() * discountPercent / 100;
}
public double getTotal() {
return getSubtotal() - getDiscountAmount();
}
public boolean hasCoupon() { return couponCode != null; }
@Override
public String toString() {
return String.format("Cart[%s] Items: %d | Subtotal: ₹%.2f | " +
"Discount: ₹%.2f | Total: ₹%.2f",
cartId, items.size(), getSubtotal(),
getDiscountAmount(), getTotal());
}
}Static Fields & Static Methods — Class-Level Members
The static keyword on a field or method makes it belong to the class itself rather than to any individual object. Static members exist independently of any instance — they are created when the class is loaded and exist for the lifetime of the application. Understanding when to use static — and when NOT to — is a key design skill.
One copy shared by ALL objects. Changes through any object are visible to all. Common uses: object counters (totalCreated++), shared configuration, caches, constants (static final). Risk: static mutable fields cause subtle bugs in multi-threaded environments and make testing harder (state persists between tests). Prefer static FINAL (constants) over mutable static fields.
No 'this' reference — no access to instance fields. Used for: pure utility functions (Math.sqrt), factory methods (LocalDate.of), helper operations that don't need object state. Cannot be overridden in subclasses (method hiding, not overriding). Can be called without creating an object — ClassName.method().
A 'static { ... }' block runs ONCE when the class is first loaded by the JVM — before any constructor or static method call. Used for: complex static field initialisation, loading configuration files, setting up static caches. Multiple static blocks run in the order they appear in the class. Cannot throw checked exceptions.
import java.util.HashMap;
import java.util.Map;
public class CurrencyConverter {
// ── STATIC CONSTANT ───────────────────────────────
public static final String BASE_CURRENCY = "INR";
// ── STATIC MUTABLE FIELD (rate table) ─────────────
private static final Map<String, Double> EXCHANGE_RATES = new HashMap<>();
// ── STATIC INITIALISER BLOCK ──────────────────────
// Runs once when the class is first loaded
static {
EXCHANGE_RATES.put("USD", 0.012);
EXCHANGE_RATES.put("EUR", 0.011);
EXCHANGE_RATES.put("GBP", 0.0095);
EXCHANGE_RATES.put("JPY", 1.78);
System.out.println("CurrencyConverter loaded — rates initialised");
}
// ── INSTANCE FIELDS ───────────────────────────────
private final String fromCurrency;
private double amount;
public CurrencyConverter(String fromCurrency, double amount) {
this.fromCurrency = fromCurrency;
this.amount = amount;
}
// ── STATIC METHODS ────────────────────────────────
// Factory method — validates currency before constructing
public static CurrencyConverter of(String currency, double amount) {
if (!EXCHANGE_RATES.containsKey(currency) && !currency.equals(BASE_CURRENCY))
throw new IllegalArgumentException("Unsupported currency: " + currency);
if (amount < 0)
throw new IllegalArgumentException("Amount cannot be negative");
return new CurrencyConverter(currency, amount);
}
// Static utility — no instance needed
public static boolean isSupportedCurrency(String currency) {
return EXCHANGE_RATES.containsKey(currency) || currency.equals(BASE_CURRENCY);
}
public static void updateRate(String currency, double rate) {
if (rate <= 0) throw new IllegalArgumentException("Rate must be positive");
EXCHANGE_RATES.put(currency, rate);
}
// ── INSTANCE METHOD ───────────────────────────────
public double convertTo(String targetCurrency) {
if (fromCurrency.equals(BASE_CURRENCY)) {
return amount * EXCHANGE_RATES.getOrDefault(targetCurrency, 1.0);
}
// Convert to INR first, then to target
double inrAmount = amount / EXCHANGE_RATES.getOrDefault(fromCurrency, 1.0);
return inrAmount * EXCHANGE_RATES.getOrDefault(targetCurrency, 1.0);
}
}
// Usage:
// CurrencyConverter.isSupportedCurrency("USD") → true
// CurrencyConverter conv = CurrencyConverter.of("INR", 83000);
// conv.convertTo("USD") → ~996.0The 'this' Keyword — Referring to the Current Object
The this keyword in Java is a reference to the current object — the specific instance on which the method or constructor is currently executing. It is one of the most frequently used and most misunderstood keywords in Java, essential for writing clear class code.
'this.name = name;' — when a constructor or setter parameter has the same name as the instance field, 'this.fieldName' refers to the field and 'paramName' (without this) refers to the parameter. Without 'this', both refer to the parameter — the field is never set. This is the most common use of 'this' in Java code.
'this(args)' inside a constructor calls another constructor in the SAME class. Must be the VERY FIRST statement in the constructor — before any other code. Used to avoid duplicating initialisation logic across multiple constructors. The chain of constructors ultimately leads to the one that does all the real initialisation work.
'someMethod(this)' passes the current object as an argument to another method. Common in event handling: 'button.addListener(this)'. Also used in builder and fluent interface patterns: 'return this;' at the end of a setter returns the current object, enabling method chaining: 'builder.setName("X").setAge(25).build()'.
Static methods belong to the class, not to any object — there is NO current object. Therefore 'this' cannot be used inside a static method or static initialiser block. Attempting to do so is a compile error: 'non-static variable this cannot be referenced from a static context'. This is the same reason you cannot access instance fields from a static method.
public class Person {
private String name;
private int age;
private String city;
// ── Use 1: Resolve field vs parameter name conflict ──
public Person(String name, int age, String city) {
this.name = name; // 'this.name' = field; 'name' = parameter
this.age = age;
this.city = city;
}
// ── Use 2: Constructor chaining with this() ──────────
public Person(String name, int age) {
this(name, age, "Unknown"); // Calls the 3-param constructor
}
public Person(String name) {
this(name, 0); // Calls the 2-param constructor
}
// ── Use 3: Return 'this' for method chaining (Fluent API) ──
public Person setName(String name) {
this.name = name;
return this; // Returns current object for chaining
}
public Person setAge(int age) {
if (age < 0 || age > 150) throw new IllegalArgumentException("Invalid age");
this.age = age;
return this;
}
public Person setCity(String city) {
this.city = city;
return this;
}
// ── Use 3b: Pass 'this' as argument to another method ──
public void register(PersonRegistry registry) {
registry.add(this); // Passes current Person object
}
// Getters
public String getName() { return name; }
public int getAge() { return age; }
public String getCity() { return city; }
@Override
public String toString() {
return String.format("Person{name='%s', age=%d, city='%s'}",
name, age, city);
}
}
// Usage — method chaining via 'return this':
// Person p = new Person("Ravi")
// .setAge(28)
// .setCity("Bengaluru");
// System.out.println(p);
// → Person{name='Ravi', age=28, city='Bengaluru'}
// Constructor chaining:
// Person p2 = new Person("Priya"); // name=Priya, age=0, city=Unknown
// Person p3 = new Person("Arjun", 25); // name=Arjun, age=25, city=UnknownGetter & Setter Methods — Controlled Field Access
Getter and setter methods are the standard mechanism for providing controlled external access to private fields. They are the practical implementation of encapsulation: keep fields private, expose them through methods that can enforce validation, add logging, trigger events, or compute derived values.
Returns the value of a private field. Naming: 'get' + FieldName in PascalCase for most types: getName(), getBalance(), getCategory(). Boolean fields: 'is' + FieldName: isActive(), isVerified(), isPremium(). Getters should be simple and NEVER have side effects — they should only read and return, never modify state.
Sets a private field value — the KEY advantage over public fields is built-in validation. Naming: 'set' + FieldName: setName(), setBalance(). Should validate before assigning: null checks, range checks, format checks. Can throw exceptions for invalid input. Can trigger side effects: update a timestamp, fire a change event, recalculate a derived value.
Some fields should be immutable after construction — provide a getter but NO setter. Example: userId set in constructor should never change. Making a field final + getter-only enforces immutability at the language level. This is safer than just 'forgetting' to write a setter — final prevents accidental reassignment even inside the class.
Not every getter needs a corresponding stored field. Some values can be COMPUTED on demand: 'getFullName() { return firstName + " " + lastName; }'. Or 'getAge() { return Period.between(birthDate, LocalDate.now()).getYears(); }'. Computed getters eliminate synchronisation issues — there is no stored age to become stale. Calculate from source data, not from a cached copy.
import java.time.LocalDate;
import java.time.Period;
import java.util.regex.Pattern;
public class UserProfile {
private static final Pattern EMAIL_REGEX =
Pattern.compile("^[\\w._%+\\-]+@[\\w.\\-]+\\.[a-zA-Z]{2,}$");
// ── Read-only (no setter) ─────────────────────────
private final String userId;
// ── Regular fields with validation setters ────────
private String displayName;
private String email;
private LocalDate birthDate;
private String phoneNumber;
private boolean emailVerified;
public UserProfile(String userId, String displayName, String email) {
this.userId = userId;
setDisplayName(displayName); // Reuse setter validation
setEmail(email); // Reuse setter validation
this.emailVerified = false;
}
// ── GETTER: Read-only field ────────────────────────
public String getUserId() { return userId; } // No setter
// ── GETTER + SETTER with validation ───────────────
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) {
if (displayName == null || displayName.isBlank())
throw new IllegalArgumentException("Display name cannot be blank");
if (displayName.length() > 50)
throw new IllegalArgumentException("Display name max 50 chars");
this.displayName = displayName.trim();
}
public String getEmail() { return email; }
public void setEmail(String email) {
if (email == null || !EMAIL_REGEX.matcher(email).matches())
throw new IllegalArgumentException("Invalid email: " + email);
this.email = email.toLowerCase();
this.emailVerified = false; // Changing email resets verification
}
public LocalDate getBirthDate() { return birthDate; }
public void setBirthDate(LocalDate birthDate) {
if (birthDate != null && birthDate.isAfter(LocalDate.now()))
throw new IllegalArgumentException("Birth date cannot be in the future");
this.birthDate = birthDate;
}
// ── BOOLEAN GETTER: 'is' prefix ────────────────────
public boolean isEmailVerified() { return emailVerified; }
public void verifyEmail() { this.emailVerified = true; } // Business method, not raw setter
// ── COMPUTED GETTER — no stored field ─────────────
public int getAge() {
if (birthDate == null) return -1;
return Period.between(birthDate, LocalDate.now()).getYears();
}
public boolean isAdult() {
return getAge() >= 18;
}
@Override
public String toString() {
return String.format("UserProfile{id='%s', name='%s', email='%s', verified=%b}",
userId, displayName, email, emailVerified);
}
}All Method Types in a Class — A Complete Overview
A well-designed Java class typically contains several types of methods, each serving a distinct purpose. Understanding these roles helps you write classes that are cohesive, readable, and maintainable.
import java.util.Objects;
public class Ticket {
private static int nextId = 1000;
private final int ticketId;
private final String event;
private final String seat;
private double price;
private TicketStatus status;
// ── CONSTRUCTOR ───────────────────────────────────
public Ticket(String event, String seat, double price) {
this.ticketId = nextId++;
this.event = event;
this.seat = seat;
this.price = price;
this.status = TicketStatus.AVAILABLE;
}
// ── STATIC FACTORY METHOD ─────────────────────────
public static Ticket of(String event, String seat, double price) {
if (price < 0) throw new IllegalArgumentException("Price cannot be negative");
return new Ticket(event, seat, price);
}
// ── GETTERS ───────────────────────────────────────
public int getTicketId() { return ticketId; }
public String getEvent() { return event; }
public String getSeat() { return seat; }
public double getPrice() { return price; }
public TicketStatus getStatus() { return status; }
// ── BOOLEAN QUERIES ───────────────────────────────
public boolean isAvailable() { return status == TicketStatus.AVAILABLE; }
public boolean isBooked() { return status == TicketStatus.BOOKED; }
public boolean isCancelled() { return status == TicketStatus.CANCELLED; }
// ── COMMAND METHODS ───────────────────────────────
public void book() {
if (!isAvailable()) throw new IllegalStateException("Ticket not available");
this.status = TicketStatus.BOOKED;
}
public void cancel() {
if (isCancelled()) throw new IllegalStateException("Already cancelled");
this.status = TicketStatus.CANCELLED;
}
// ── QUERY / COMPUTED VALUE ────────────────────────
public double getPriceWithTax() {
return price * 1.18; // 18% GST
}
// ── toString ─────────────────────────────────────
@Override
public String toString() {
return String.format("Ticket[#%d | %s | Seat:%s | ₹%.0f | %s]",
ticketId, event, seat, price, status);
}
// ── equals & hashCode ─────────────────────────────
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Ticket t)) return false;
return ticketId == t.ticketId;
}
@Override
public int hashCode() {
return Objects.hash(ticketId);
}
}
enum TicketStatus { AVAILABLE, BOOKED, CANCELLED }Inner Classes (Nested Classes) — Classes Inside Classes
Java allows declaring a class inside another class. These are called nested classes or inner classes. They are used to logically group classes that are only used in one place, increase encapsulation, and produce more readable and maintainable code. Java has four types of nested classes, each with distinct characteristics.
Declared inside a class without 'static'. Has access to ALL members of the outer class — including private fields. Requires an outer class object to be created: 'Outer.Inner inner = outer.new Inner()'. Use for: iterator implementations, event listeners tightly coupled to their outer class. Avoid if the inner class doesn't truly need access to the outer class instance.
Declared inside a class WITH 'static'. Does NOT have access to instance members of the outer class — only static members. Created without an outer class object: 'Outer.StaticNested obj = new Outer.StaticNested()'. Preferred over non-static inner class when the nested class doesn't need outer instance access. Used for: builder classes (Person.Builder), grouped helper classes, namespace organisation.
Declared INSIDE a method body. Visible only within that method. Can access local variables of the enclosing method if they are effectively final. Rarely used in modern Java — lambdas and anonymous classes typically serve the same purpose more concisely.
A class with no name — declared and instantiated in the same expression. Commonly used to implement interfaces or extend abstract classes with a one-off implementation: 'Runnable r = new Runnable() { public void run() { ... } };'. In modern Java (8+), lambdas replace anonymous classes for functional interfaces. Still used for abstract classes and multi-method interfaces.
// ── STATIC NESTED CLASS: Builder Pattern ──────────────
public class DatabaseConfig {
private final String host;
private final int port;
private final String database;
private final String username;
private final int poolSize;
// private constructor — use Builder to create
private DatabaseConfig(Builder builder) {
this.host = builder.host;
this.port = builder.port;
this.database = builder.database;
this.username = builder.username;
this.poolSize = builder.poolSize;
}
public String getHost() { return host; }
public int getPort() { return port; }
public String getDatabase() { return database; }
@Override
public String toString() {
return String.format("DB[%s:%d/%s user=%s pool=%d]",
host, port, database, username, poolSize);
}
// ✅ STATIC NESTED CLASS — Builder
public static class Builder {
// Required params
private final String host;
private final String database;
// Optional params with defaults
private int port = 5432;
private String username = "root";
private int poolSize = 10;
public Builder(String host, String database) {
this.host = host;
this.database = database;
}
public Builder port(int port) { this.port = port; return this; }
public Builder username(String user) { this.username = user; return this; }
public Builder poolSize(int size) { this.poolSize = size; return this; }
public DatabaseConfig build() {
if (host == null || host.isBlank())
throw new IllegalStateException("Host is required");
return new DatabaseConfig(this);
}
}
}
// Usage — Builder pattern:
// DatabaseConfig config = new DatabaseConfig.Builder("localhost", "mydb")
// .port(3306)
// .username("admin")
// .poolSize(20)
// .build();
// System.out.println(config);
// → DB[localhost:3306/mydb user=admin pool=20]
// ── NON-STATIC INNER CLASS example ───────────────────
class LinkedList<T> {
// Inner class — has access to LinkedList's private state
private class Node {
T data;
Node next;
Node(T data) { this.data = data; }
}
private Node head;
// Node is an implementation detail — hidden from outside world
}Common Mistakes & Pitfalls — Bugs That Trip Everyone Up
These class-and-method mistakes are consistently found in Java beginner and intermediate code. Each one either causes a compile-time error or a subtle runtime bug that is hard to diagnose.
// ❌ MISTAKE 1: Forgetting 'this' — field never assigned
public class Person {
private String name;
public Person(String name) {
name = name; // ❌ Assigns parameter to itself! Field 'this.name' stays null
}
}
// ✅ Fix:
public Person(String name) {
this.name = name; // ✅ Field assigned correctly
}
// ❌ MISTAKE 2: Accessing instance member from static context
public class Counter {
int count = 0;
public static void main(String[] args) {
System.out.println(count); // ❌ Compile error — no 'this' in static
}
}
// ✅ Fix: create an object
public static void main(String[] args) {
Counter c = new Counter();
System.out.println(c.count); // ✅
}
// ❌ MISTAKE 3: Calling this() NOT as first statement
public class Rectangle {
int width, height;
public Rectangle(int width, int height) {
this.width = width;
this(width, height, "blue"); // ❌ Compile error — must be FIRST
}
}
// ❌ MISTAKE 4: Writing return type on constructor
public class Circle {
// void Circle(double radius) { } // ❌ This is a METHOD named 'Circle', not a constructor
// It will never be called by 'new Circle(...)' — silent logical error
// ✅ Correct constructor — no return type:
public Circle(double radius) { }
}
// ❌ MISTAKE 5: Default constructor disappears after writing one
public class Config {
private String env;
public Config(String env) { this.env = env; }
}
// Config c = new Config(); // ❌ Compile error — no-arg constructor gone
// ✅ Fix: explicitly add no-arg constructor if still needed
public Config() { this("development"); }
// ❌ MISTAKE 6: Returning mutable internal collection from getter
public class Team {
private List<String> members = new ArrayList<>();
public List<String> getMembers() {
return members; // ❌ Caller gets direct reference — can modify!
}
}
// ✅ Fix: return defensive copy
public List<String> getMembers() {
return Collections.unmodifiableList(members);
}
// ❌ MISTAKE 7: NullPointerException from uninitialized object field
public class Order {
private List<String> items; // ❌ Null — not initialised
public void addItem(String item) {
items.add(item); // ❌ NPE — items is null
}
}
// ✅ Fix: initialise at declaration
private List<String> items = new ArrayList<>();Bad Practices & Anti-Patterns — What Senior Developers Reject
These class design anti-patterns are the most common causes of failed code reviews in professional Java teams. Each violates fundamental OOP principles and creates long-term maintenance nightmares.
A single class with 50+ fields, 100+ methods, handling every concern of the application. Violates Single Responsibility Principle. Signs: class name ends in Manager, Service, Handler, Processor doing unrelated things. Fix: split by responsibility — UserRegistrationService, UserAuthenticationService, UserProfileService. Each class should have ONE reason to change.
A class that is just a data bag — only private fields and public getters/setters, no business logic. The business logic is scattered across 'service' classes that directly manipulate the domain object's state. Fix: move behaviour INTO the domain class. An 'Account' class should have deposit(), withdraw(), calculateInterest() — not just getBalance()/setBalance().
A class with mutable state accessed by multiple threads without synchronisation. 'counter++' is NOT atomic — it is read-modify-write in three steps. Two threads can both read 5, both increment to 6, and both write 6 — losing one increment. Fix: use immutable objects where possible, AtomicInteger for counters, or synchronised/volatile for shared mutable state.
A constructor with 7+ parameters is a code smell. Hard to call correctly, easy to swap adjacent arguments of the same type (price and stock both int — very easy to mix up). Fix: use the Builder pattern for classes with many optional parameters. Or group related parameters into dedicated value objects: 'Address address' instead of 'String street, String city, String state, String pincode'.
Using objects as Map keys or Set elements without overriding equals() and hashCode() means Java uses REFERENCE equality (identity) — two 'equal' objects are treated as different. Example: two ProductId objects with the same id String will not be found as equal without overriding these methods. Rule: whenever you override equals(), you MUST override hashCode() with the same fields — contracts require this.
Calling a non-final, non-private (i.e., overridable) method inside a constructor is dangerous in an inheritance hierarchy. If a subclass overrides that method and the subclass's constructor hasn't run yet, the overridden version runs before the subclass fields are initialised — accessing uninitialised fields. Fix: only call private or final methods from constructors.
// ❌ ANTI-PATTERN: Calling overridable method in constructor
public class Base {
public Base() {
initialise(); // ❌ Dangerous if overridden in subclass
}
public void initialise() { System.out.println("Base init"); }
}
class Derived extends Base {
private String config;
public Derived() {
super(); // Base() calls initialise() before Derived() sets config
this.config = "loaded";
}
@Override
public void initialise() {
// ❌ config is null here! Derived() hasn't run yet
System.out.println("Config: " + config.toUpperCase()); // NPE!
}
}
// ✅ Fix: make initialise() private or final in Base
// ❌ ANTI-PATTERN: No equals/hashCode — broken Set/Map behaviour
class ProductId {
private final String id;
public ProductId(String id) { this.id = id; }
// Missing equals() and hashCode()
}
// var set = new HashSet<ProductId>();
// set.add(new ProductId("P001"));
// set.contains(new ProductId("P001")); // ❌ Returns false! Different object
// ✅ Fix: override both equals and hashCode
class ProductId {
private final String id;
public ProductId(String id) { this.id = id; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ProductId p)) return false;
return java.util.Objects.equals(id, p.id);
}
@Override
public int hashCode() {
return java.util.Objects.hash(id);
}
}Real-World Production Code Examples — Class Design in Context
The following examples demonstrate complete, production-quality Java class designs — showing how fields, constructors, instance methods, static members, and inner classes work together in real enterprise code.
package com.techsustainify.domain.value;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import java.util.Objects;
/**
* Immutable value object representing a monetary amount.
* No setters — once created, a Money object never changes.
* Operations return NEW Money objects (like BigDecimal, String).
*/
public final class Money {
// Constants
public static final Money ZERO_INR = new Money(BigDecimal.ZERO, "INR");
// Private final fields — immutable after construction
private final BigDecimal amount;
private final String currencyCode;
// ── PRIVATE CONSTRUCTOR — use factory methods ─────
private Money(BigDecimal amount, String currencyCode) {
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currencyCode = currencyCode.toUpperCase();
}
// ── STATIC FACTORY METHODS ────────────────────────
public static Money of(double amount, String currency) {
if (amount < 0) throw new IllegalArgumentException("Amount cannot be negative");
validateCurrency(currency);
return new Money(BigDecimal.valueOf(amount), currency);
}
public static Money of(BigDecimal amount, String currency) {
Objects.requireNonNull(amount, "Amount cannot be null");
if (amount.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Amount cannot be negative");
validateCurrency(currency);
return new Money(amount, currency);
}
// ── GETTERS (no setters — immutable) ──────────────
public BigDecimal getAmount() { return amount; }
public String getCurrencyCode() { return currencyCode; }
// ── BUSINESS METHODS — return new Money objects ───
public Money add(Money other) {
assertSameCurrency(other);
return new Money(this.amount.add(other.amount), currencyCode);
}
public Money subtract(Money other) {
assertSameCurrency(other);
BigDecimal result = this.amount.subtract(other.amount);
if (result.compareTo(BigDecimal.ZERO) < 0)
throw new ArithmeticException("Subtraction results in negative money");
return new Money(result, currencyCode);
}
public Money multiply(double factor) {
if (factor < 0) throw new IllegalArgumentException("Factor cannot be negative");
return new Money(amount.multiply(BigDecimal.valueOf(factor)), currencyCode);
}
public Money applyDiscount(double discountPercent) {
if (discountPercent < 0 || discountPercent > 100)
throw new IllegalArgumentException("Invalid discount percent");
BigDecimal discount = amount.multiply(BigDecimal.valueOf(discountPercent / 100));
return new Money(amount.subtract(discount), currencyCode);
}
public boolean isGreaterThan(Money other) {
assertSameCurrency(other);
return amount.compareTo(other.amount) > 0;
}
public boolean isZero() {
return amount.compareTo(BigDecimal.ZERO) == 0;
}
// ── PRIVATE HELPERS ───────────────────────────────
private void assertSameCurrency(Money other) {
if (!currencyCode.equals(other.currencyCode))
throw new IllegalArgumentException(
"Currency mismatch: " + currencyCode + " vs " + other.currencyCode);
}
private static void validateCurrency(String code) {
try { Currency.getInstance(code); }
catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid currency code: " + code);
}
}
// ── Object contract ───────────────────────────────
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money m)) return false;
return amount.compareTo(m.amount) == 0
&& currencyCode.equals(m.currencyCode);
}
@Override
public int hashCode() {
return Objects.hash(amount.stripTrailingZeros(), currencyCode);
}
@Override
public String toString() {
return currencyCode + " " + amount.toPlainString();
}
}
// Usage:
// Money price = Money.of(1000.00, "INR");
// Money shipping = Money.of(50.00, "INR");
// Money total = price.add(shipping); // INR 1050.00
// Money final_ = total.applyDiscount(10); // INR 945.00
// System.out.println(final_.isGreaterThan(Money.ZERO_INR)); // truepackage com.techsustainify.notification;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class Notification {
// ── ENUM nested inside class ──────────────────────
public enum Type { EMAIL, SMS, PUSH, IN_APP }
public enum Priority { LOW, NORMAL, HIGH, CRITICAL }
// ── IMMUTABLE FIELDS ──────────────────────────────
private final String id;
private final String recipientId;
private final Type type;
private final Priority priority;
private final String subject;
private final String body;
private final List<String> tags;
private final LocalDateTime createdAt;
private final LocalDateTime scheduledFor;
// ── PRIVATE CONSTRUCTOR — use Builder ─────────────
private Notification(Builder b) {
this.id = java.util.UUID.randomUUID().toString();
this.recipientId = b.recipientId;
this.type = b.type;
this.priority = b.priority;
this.subject = b.subject;
this.body = b.body;
this.tags = Collections.unmodifiableList(new ArrayList<>(b.tags));
this.createdAt = LocalDateTime.now();
this.scheduledFor = b.scheduledFor != null ? b.scheduledFor : this.createdAt;
}
// ── GETTERS ───────────────────────────────────────
public String getId() { return id; }
public String getRecipientId() { return recipientId; }
public Type getType() { return type; }
public Priority getPriority() { return priority; }
public String getSubject() { return subject; }
public String getBody() { return body; }
public List<String> getTags() { return tags; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getScheduledFor(){ return scheduledFor; }
// ── QUERY METHODS ─────────────────────────────────
public boolean isUrgent() { return priority == Priority.HIGH || priority == Priority.CRITICAL; }
public boolean isScheduled() { return scheduledFor.isAfter(createdAt); }
public boolean hasTag(String tag) { return tags.contains(tag); }
@Override
public String toString() {
return String.format("Notification[%s | %s | %s | %s | '%s']",
id.substring(0,8), type, priority, recipientId, subject);
}
// ── STATIC NESTED BUILDER CLASS ───────────────────
public static class Builder {
// Required
private final String recipientId;
private final Type type;
private final String body;
// Optional with defaults
private Priority priority = Priority.NORMAL;
private String subject = "";
private List<String> tags = new ArrayList<>();
private LocalDateTime scheduledFor = null;
public Builder(String recipientId, Type type, String body) {
if (recipientId == null || recipientId.isBlank())
throw new IllegalArgumentException("Recipient ID required");
if (body == null || body.isBlank())
throw new IllegalArgumentException("Body required");
this.recipientId = recipientId;
this.type = type;
this.body = body;
}
public Builder priority(Priority p) { this.priority = p; return this; }
public Builder subject(String s) { this.subject = s; return this; }
public Builder tag(String tag) { this.tags.add(tag); return this; }
public Builder scheduledFor(LocalDateTime t){ this.scheduledFor=t;return this; }
public Notification build() {
return new Notification(this);
}
}
}
// Usage:
// Notification n = new Notification.Builder("user-123", Notification.Type.EMAIL, "Your order is confirmed!")
// .subject("Order Confirmation — #ORD-5521")
// .priority(Notification.Priority.HIGH)
// .tag("order")
// .tag("transactional")
// .build();Class Structure Flowchart — Anatomy of a Java Class
This flowchart shows the complete structural anatomy of a Java class — from declaration to all possible members.
Code Execution Flow — from source to output
Java Class & Methods Interview Questions — Beginner to Advanced
These questions are consistently asked in Java fresher and experienced developer interviews, campus placements, and OCPJP/OCA certification exams.
Practice Questions — Test Your Class & Methods Knowledge
Attempt each question independently before reading the answer — active recall significantly improves retention and understanding.
1. What is the output? public class Box { static int count = 0; int id; public Box() { count++; this.id = count; } public static void main(String[] args) { Box b1 = new Box(); Box b2 = new Box(); Box b3 = new Box(); System.out.println(b1.id + " " + b2.id + " " + b3.id); System.out.println(Box.count); } }
Easy2. Identify the bug: public class Circle { private double radius; public Circle(double radius) { radius = radius; } public double getArea() { return Math.PI * radius * radius; } }
Easy3. Will this compile? Explain. public class Vehicle { private String brand; public Vehicle(String brand) { this.brand = brand; } } public class Car extends Vehicle { private int doors; public Car(int doors) { this.doors = doors; } }
Medium4. Design a class 'Rectangle' with: length and width (private), constructor, getArea(), getPerimeter(), isSquare(), and toString(). All fields must be positive.
Medium5. What is the output? public class Parent { static { System.out.println("Parent static block"); } { System.out.println("Parent instance block"); } public Parent() { System.out.println("Parent constructor"); } } public class Child extends Parent { static { System.out.println("Child static block"); } { System.out.println("Child instance block"); } public Child() { System.out.println("Child constructor"); } public static void main(String[] args) { new Child(); new Child(); } }
Hard6. Implement a Thread-safe Singleton class in Java.
Hard7. What is wrong with this equals() implementation? Fix it. public class Student { private int id; private String name; @Override public boolean equals(Object obj) { Student other = (Student) obj; return this.id == other.id; } }
Medium8. Implement a Builder pattern for a class 'EmailMessage' with: to (required), from (required), subject (optional), body (optional), isHtml (optional, default false), priority (optional, default NORMAL).
HardConclusion — Classes & Methods: The Heart of Java OOP
The class is the fundamental unit of object-oriented programming in Java. Every field, every constructor, every method — all live inside a class. Mastering class design is not just about knowing the syntax — it is about understanding how to model real-world entities with appropriate state, behaviour, and encapsulation.
The hallmarks of a well-designed Java class are clear: private fields with controlled access, constructors that guarantee valid state, methods that each do one thing, proper equals/hashCode for value objects, and meaningful toString for debugging. The Builder pattern for complex objects, factory methods for controlled creation, and the static nested class for grouped helpers — these patterns turn average Java code into production-grade software.
Your next step: Java Arrays — where you will learn to work with collections of data, how to pass arrays to methods, use the Arrays utility class, and understand the relationship between arrays and objects in Java. The class design principles you have learned here apply directly to how you design methods that work with arrays. ☕