Technology  /  Java

โ˜• Java 15 guides ยท updated 2026

From the JVM to streams, generics, and concurrency โ€” the language behind most enterprise backends, taught with modern Java in mind.

Classes and Objects in Java: Constructors, this, static, and Object Design

Everything in Java lives inside a class โ€” but thatโ€™s a syntax rule, not understanding. The real shift is conceptual: instead of writing procedures that push data around, you design things that bundle data with the behavior that belongs to it. Java without that shift is just awkward C.

This guide builds the object model from the ground up: classes as blueprints, objects as instances, constructors, this, static, overloading, and โ€” the part tutorials skip โ€” how object references actually behave in memory.


Blueprint vs. Instance

A class describes what something is โ€” its data (fields) and behavior (methods). An object is a concrete instance with its own copy of that data:

public class BankAccount {
// fields โ€” every instance gets its own copies
private String owner;
private long balancePaise; // money as integer paise; never double
// constructor โ€” how instances get born
public BankAccount(String owner, long openingPaise) {
this.owner = owner;
this.balancePaise = openingPaise;
}
// methods โ€” behavior that operates on this instance's data
public void deposit(long paise) {
if (paise <= 0) throw new IllegalArgumentException("Deposit must be positive");
balancePaise += paise;
}
public long getBalancePaise() {
return balancePaise;
}
}

Creating and using instances:

BankAccount a = new BankAccount("Priya", 50_000);
BankAccount b = new BankAccount("Arun", 10_000);
a.deposit(2_500);
System.out.println(a.getBalancePaise()); // 52500
System.out.println(b.getBalancePaise()); // 10000 โ€” b is untouched

One class, many independent objects. a.deposit(...) changes only aโ€™s data โ€” that locality is the entire point. State and the rules governing it live in one place, and (thanks to private) nothing outside the class can put the account into a nonsense state like a negative deposit.


References: What a Variable of Object Type Really Holds

This is the model that, once internalized, explains half of Javaโ€™s โ€œsurprisingโ€ behavior. A variable of object type doesnโ€™t contain the object โ€” it contains a reference (think: arrow) to an object on the heap:

stack heap
โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
a โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ BankAccount{owner:"Priya", balance:52500}
b โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ BankAccount{owner:"Arun", balance:10000}
c โ”€โ”€โ”
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ (same object as a)
BankAccount c = a; // copies the REFERENCE, not the object
c.deposit(1_000);
System.out.println(a.getBalancePaise()); // 53500 โ€” a sees it, same object!

Consequences youโ€™ll hit weekly:


Constructors

A constructor runs exactly once per object, at new time. Its job: put the object into a valid state, or refuse to create it.

public BankAccount(String owner, long openingPaise) {
if (owner == null || owner.isBlank())
throw new IllegalArgumentException("Owner required");
if (openingPaise < 0)
throw new IllegalArgumentException("Opening balance cannot be negative");
this.owner = owner;
this.balancePaise = openingPaise;
}

Validating in the constructor means an invalid object can never exist. Thatโ€™s a much stronger guarantee than hoping every caller checks โ€” and it moves bug discovery from โ€œweird corrupted state three hours laterโ€ to โ€œclear exception at the creation site.โ€

Details worth knowing:

public BankAccount(String owner) {
this(owner, 0); // delegate to the main constructor
}

Put the validation in one โ€œcanonicalโ€ constructor and have the others delegate to it. This pattern scales; copy-pasted validation doesnโ€™t.


this: The Current Object

this is a reference to the object the method was called on. Two everyday uses:

Disambiguating fields from parameters (the standard constructor idiom):

public BankAccount(String owner, long balancePaise) {
this.owner = owner; // this.owner = field, owner = parameter
this.balancePaise = balancePaise;
}

Returning this for fluent APIs โ€” how StringBuilder chaining works:

public BankAccount tag(String label) {
this.label = label;
return this;
}
// account.tag("savings").deposit(100);

Thereโ€™s no magic: inside any instance method, balancePaise silently means this.balancePaise. this just makes it explicit when names collide or when you need to hand out the reference.


static: Belongs to the Class, Not to Instances

Mark a member static and thereโ€™s exactly one of it, shared by all instances โ€” indeed, it exists even with zero instances:

public class BankAccount {
private static long totalAccounts = 0; // one counter for the whole class
private static final long MAX_DEPOSIT = 10_00_00_000L; // class constant
private String owner; // per-instance
public BankAccount(String owner, long openingPaise) {
this.owner = owner;
totalAccounts++; // every construction bumps the shared counter
}
public static long getTotalAccounts() { // called as BankAccount.getTotalAccounts()
return totalAccounts;
}
}

The mental test: does this belong to a particular account, or to the concept of accounts? Owner: instance. Count of all accounts: static. Conversion helper like Math.max: static (thatโ€™s why main is static โ€” it must run before any object of your class exists).

Two rules of hygiene:

  1. Static methods canโ€™t touch instance fields โ€” thereโ€™s no this to read them from. The compiler enforces it; beginners meet this as โ€œnon-static variable cannot be referenced from a static contextโ€ the first time main tries to use a field.
  2. Mutable static state is a design smell. A static counter is fine for illustration, but shared mutable statics become race conditions under concurrency and hidden coupling everywhere. static final constants: great. static mutable fields: think twice, then think again.

Method Overloading

One name, several parameter lists โ€” the compiler picks by argument types at compile time:

public void deposit(long paise) { ... }
public void deposit(long paise, String memo) { ... }
public void deposit(Money amount) { ... }

Youโ€™ve been using overloads all along: System.out.println has ten of them; thatโ€™s why it accepts an int, a String, or an object without complaint. Overloading is good API design when the operation is genuinely the same and only the input form differs. Itโ€™s bad design when overloads behave differently โ€” callers wonโ€™t read the fine print.

Note that overloading is resolved by parameter types only โ€” you canโ€™t overload on return type alone, and long vs Long overloads plus autoboxing can produce genuinely confusing resolution. Keep overload sets simple.


toString, and the Object Methods Every Class Inherits

Every class implicitly extends Object, inheriting toString(), equals(), hashCode(), and a few others. Default toString() is useless (BankAccount@1b6d3586), so override it early โ€” debugging quality of life improves immediately:

@Override
public String toString() {
return "BankAccount[owner=%s, balance=%d]".formatted(owner, balancePaise);
}

The @Override annotation asks the compiler to verify youโ€™re actually overriding something โ€” catching the classic typo tostring() that would otherwise silently create a new, never-called method. Use @Override on every override, always.

equals() and hashCode() matter as soon as your objects go into sets or map keys; they have contract rules subtle enough that we cover them with records, which generate them correctly for free.


A Worked Example: Designing a Small Class Well

Requirements: track inventory items โ€” name, quantity, unit price; support restocking and selling; never allow negative quantity.

public class InventoryItem {
private final String name; // never changes โ†’ final
private int quantity;
private final long unitPricePaise;
public InventoryItem(String name, int quantity, long unitPricePaise) {
if (name == null || name.isBlank()) throw new IllegalArgumentException("name");
if (quantity < 0) throw new IllegalArgumentException("quantity < 0");
if (unitPricePaise <= 0) throw new IllegalArgumentException("price <= 0");
this.name = name;
this.quantity = quantity;
this.unitPricePaise = unitPricePaise;
}
public void restock(int units) {
if (units <= 0) throw new IllegalArgumentException("units must be positive");
quantity += units;
}
public void sell(int units) {
if (units <= 0) throw new IllegalArgumentException("units must be positive");
if (units > quantity)
throw new IllegalStateException("Only " + quantity + " in stock");
quantity -= units;
}
public long stockValuePaise() { return quantity * unitPricePaise; }
public int getQuantity() { return quantity; }
public String getName() { return name; }
@Override
public String toString() {
return "InventoryItem[%s x%d @ %d]".formatted(name, quantity, unitPricePaise);
}
}

Design decisions to notice, because theyโ€™re the habits that separate maintainable code from homework code:


Object Design Habits Worth Forming Early

Beyond syntax, a few habits distinguish code that ages well โ€” all of them cheap to adopt now and expensive to retrofit later.

Make the class small and the name honest. A class named OrderManager that validates, persists, emails, and logs is four classes sharing a trench coat. When you canโ€™t name a class without โ€œManagerโ€, โ€œHandlerโ€, or โ€œUtilโ€, the responsibilities havenโ€™t been found yet. InventoryItem, StockLevel, ReorderPolicy โ€” nameable things, one job each.

Constructor injection for dependencies. If your class needs a repository or a clock, take it as a constructor parameter rather than constructing it internally or reaching for a static. new InvoiceService(repository, Clock.systemUTC()) is testable with a fake repository and a fixed clock; a service that news up its own dependencies is welded to them. This single habit is 80% of what dependency-injection frameworks automate โ€” learn it manually first and Spring will later feel obvious instead of magical.

Model with a clock, not LocalDateTime.now(). Scattered now() calls make time-dependent logic untestable (โ€œdoes the discount apply at midnight?โ€). Inject java.time.Clock and every test can freeze time. Niche-sounding, saves real pain.

Prefer returning over mutating parameters. A method that fills a list passed into it (void loadInto(List<Order> target)) hides its output; List<Order> load() declares it. Reserve out-parameters for measured performance needs.

Frequently Asked Questions

When do objects actually get destroyed? You never destroy them โ€” when no reference chain reaches an object, the garbage collector reclaims it eventually. This is why Java has no delete and why forgetting cleanup of memory isnโ€™t a bug โ€” though forgetting to close files or connections still is; see try-with-resources and the GC guide.

Whatโ€™s the difference between a class and an object, one more time? The class is compile-time (the definition, one per program); objects are runtime (instances, as many as you create). static members belong to the class side of that line; instance members to the object side.

Can I define a class inside a class? Yes โ€” nested classes. static nested classes are the common, benign kind (a helper type scoped to its owner, like Map.Entry). Inner (non-static) classes secretly hold a reference to their enclosing instance โ€” occasionally useful, but a known source of surprise memory retention; prefer static nested unless you need the outer reference.

Why do tutorials say โ€œfavor immutabilityโ€ but this guideโ€™s examples mutate? Because BankAccount and InventoryItem model things whose state genuinely changes over time โ€” entities. The guidance to prefer immutability applies to values (money, coordinates, IDs), which we formalize with records in the immutability guide. Knowing which of the two youโ€™re modeling is the actual skill.

Self-Check

  1. BankAccount x = new BankAccount("Z", 100); BankAccount y = x; y.deposit(50); โ€” what does x.getBalancePaise() return, and why?
  2. Why does writing a BankAccount(String, long) constructor break new BankAccount() elsewhere in the codebase?
  3. Can a static method call this.getBalancePaise()? Why not?
  4. What bug does @Override catch that the compiler otherwise wouldnโ€™t?

(Answers: 150, same object via copied reference; the implicit default constructor vanishes once any constructor is declared; no โ€” static methods have no this; misspelled or wrongly-typed override signatures silently becoming new methods.)

Next: Inheritance and Polymorphism โ€” where one variable can hold many shapes of object, and Javaโ€™s dynamic dispatch starts doing real work.