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()); // 52500System.out.println(b.getBalancePaise()); // 10000 โ b is untouchedOne 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 objectc.deposit(1_000);System.out.println(a.getBalancePaise()); // 53500 โ a sees it, same object!Consequences youโll hit weekly:
- Assignment never copies objects. Two variables, one object.
- Method parameters are references too. A method receiving your list can mutate it. (Java is strictly pass-by-value โ but the value passed is the reference.)
==compares references,.equals()compares contents โ the same rule you met with strings.nullmeans โreference to nothing.โ Calling any method through a null reference throwsNullPointerExceptionโ the most common exception in Java history. Since Java 14, the message helpfully names the null variable.
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:
- If you write no constructor, Java supplies a no-argument default. The moment you write any constructor, that default disappears โ a common compile-error surprise.
- Constructors can call each other with
this(...)to avoid duplicating logic:
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:
- Static methods canโt touch instance fields โ thereโs no
thisto read them from. The compiler enforces it; beginners meet this as โnon-static variable cannot be referenced from a static contextโ the first timemaintries to use a field. - Mutable static state is a design smell. A
staticcounter is fine for illustration, but shared mutable statics become race conditions under concurrency and hidden coupling everywhere.static finalconstants: great.staticmutable 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:
@Overridepublic 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:
- Fields are
private; unchanging ones are alsofinal. Nobody outside can setquantity = -50. - The object defends its own invariants โ
sellchecks stock; callers canโt forget to. - No setters by default.
restock/sellexpress business operations;setQuantity(int)would express nothing and allow anything. Add accessors deliberately, not reflexively. IllegalArgumentExceptionfor bad inputs,IllegalStateExceptionfor legal input at the wrong time. Standard vocabulary other Java developers instantly read.
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
BankAccount x = new BankAccount("Z", 100); BankAccount y = x; y.deposit(50);โ what doesx.getBalancePaise()return, and why?- Why does writing a
BankAccount(String, long)constructor breaknew BankAccount()elsewhere in the codebase? - Can a
staticmethod callthis.getBalancePaise()? Why not? - What bug does
@Overridecatch 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.