Classes and Objects
The blueprint and the thing built from it. Constructors, fields, methods, the static keyword, and what `new` actually does at the JVM level.
Blueprint and instance
Every program needs to model the world it operates on. A user, a payment, a tweet, a shopping cart. Java's primary unit for modelling is the **class**.
A class is a *blueprint*. It describes what something is made of (its **fields**) and what it can do (its **methods**). The class itself isn't a thing you can interact with directly. To work with a User, you create an **object** — an instance of the class. Each object has its own copy of the fields. They share the method definitions.
public class User {
String name; // field
int age; // field
void greet() { // method
System.out.println("Hi, I'm " + name);
}
}
User alice = new User(); // create an instance
alice.name = "Alice";
alice.age = 30;
alice.greet(); // "Hi, I'm Alice"
User bob = new User(); // another instance, separate state
bob.name = "Bob";
bob.age = 25;
bob.greet(); // "Hi, I'm Bob"
alice and bob are both User objects. They have the same shape (a name and an age) and the same capabilities (greet()), but each holds its own values. Mutating alice.name doesn't touch bob.name. That's the whole point of objects — separate state, shared behaviour.
What `new` actually does
new User() looks like one operation. Under the hood it's four steps the JVM performs in order:
- **Allocate memory** on the heap for a new User object — enough for the object header (~12-16 bytes) plus space for all instance fields.
- **Initialise fields to defaults**: numeric types to 0, booleans to false, references to null. This happens before any code you wrote runs.
- **Run the constructor**: your
User()(or whichever one you called) executes its body. - **Return a reference** to the new object. That's what gets assigned to your variable.
User alice = new User();
// ^ ^ ^ ^
// alloc defaults ctor reference returned
If allocation fails (heap is full), you get an OutOfMemoryError at step 1. If the constructor throws, the half-built object is abandoned and the exception propagates — your variable never gets assigned.
This four-step dance happens millions of times in a typical Java program. The JVM is heavily optimised for it (escape analysis, TLAB allocation, generational GC), which is why allocation in Java is fast despite running through this whole sequence.
Constructors
A constructor is a special method that initialises a freshly-created object. Three things make it different from a normal method:
- Its name is exactly the class name.
- It has no return type — not even
void. - It runs automatically as step 3 of
new.
public class User {
String name;
int age;
// Constructor
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
User alice = new User("Alice", 30); // fields assigned via constructor
The this keyword refers to "the object being constructed" — needed because the parameter name shadows the field name. Without this., the line name = name would just assign the parameter to itself.
**The default constructor.** If you don't write any constructor, Java provides a no-arg one for free that does nothing:
public class Empty {
// No constructor written — Java provides `public Empty() {}`
}
Empty e = new Empty(); // works fine
But the moment you write ANY constructor, the free no-arg one disappears:
public class User {
public User(String name) { ... }
}
User u = new User(); // compile error — no matching constructor
If you still want a no-arg constructor, write it explicitly.
**Constructor overloading.** A class can have multiple constructors with different parameter lists. Use this(...) to call one from another:
public class User {
String name;
int age;
String role;
public User(String name, int age, String role) {
this.name = name;
this.age = age;
this.role = role;
}
public User(String name, int age) {
this(name, age, "guest"); // delegate to the 3-arg constructor
}
public User(String name) {
this(name, 0); // delegate to the 2-arg
}
}
This chained pattern keeps initialisation logic in one place. Calling this(...) must be the first line of a constructor — Java enforces this rule.
Fields: instance vs static
Fields come in two flavours, distinguished by whether they belong to *each object* or to *the class itself*.
**Instance fields** (no static keyword) — every object gets its own copy. Modifying alice.age doesn't change bob.age. They live on the heap as part of each object's storage.
**Static fields** (with static) — exactly one copy exists, shared across all instances. They live in the Method Area / Metaspace, not the heap. Accessed via the class name, not via an instance.
public class User {
String name; // instance field — per object
static int totalUsers = 0; // static field — shared
public User(String name) {
this.name = name;
totalUsers++; // increment the shared counter
}
}
new User("Alice");
new User("Bob");
new User("Carol");
System.out.println(User.totalUsers); // 3
Static fields are useful for:
- **Counters and registries** — totals, caches, lookup tables.
- **Constants** — combine static with final: public static final int MAX_ATTEMPTS = 3;
- **Singletons** — single shared instance: public static final Logger LOG = ...
Common mistake: using a static field for something that should be per-instance state. The classic bug:
public class Counter {
static int count; // BUG — should be instance field
void increment() { count++; }
}
Counter a = new Counter();
Counter b = new Counter();
a.increment();
a.increment();
b.increment();
System.out.println(a.count); // 3, not 2 — count is SHARED
Static is a strong signal. Reach for it deliberately, not by reflex.
Methods: instance vs static
Same idea as fields. Instance methods belong to objects and can access instance fields. Static methods belong to the class and can't access instance fields directly.
public class MathUtil {
static double squareRoot(double x) { // static method
return Math.sqrt(x);
}
}
double r = MathUtil.squareRoot(16); // called via the class, no instance needed
Static methods are perfect for **utility functions** that don't depend on object state. Math.max, Math.abs, Integer.parseInt, String.valueOf — all static. They take input, produce output, no side effects on any object.
Instance methods, by contrast, operate on an object's state:
public class Counter {
int value = 0;
void increment() { // instance method — implicitly operates on `this`
value++;
}
}
Counter c = new Counter();
c.increment();
Inside an instance method, this is implicit — you can write value++ instead of this.value++. You only need this. when there's a naming conflict (like in a constructor parameter).
**A static method cannot access instance fields directly:**
public class User {
String name;
static void greetAll() {
System.out.println("Hi, " + name); // compile error — no `this` available
}
}
This makes sense: a static method runs without any specific instance. There's no "the name" to refer to. To work with instances from a static context, you have to receive them as parameters.
Access from outside: getters, setters, and direct field access
How does code outside the class read and modify fields? Three patterns, with different implications.
**Public fields** — direct access. Simple, fast, dangerous.
public class User {
public String name;
public int age;
}
User u = new User();
u.name = "Alice"; // directly assigned
This is fine for small data-holder objects you control. It becomes a problem when you later want to add validation, logging, or computed values — every place reading or writing the field breaks the encapsulation, so changing the implementation means changing every caller.
**Private fields with getters and setters** — the classic JavaBean pattern.
public class User {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("age can't be negative");
this.age = age;
}
}
Now the field is encapsulated. Callers go through methods, which gives you room to add validation, logging, or change the underlying storage later without breaking anyone.
**Records** (Java 16+) — for pure data classes where you want immutability and no boilerplate:
public record User(String name, int age) {}
That single line gives you: private final fields, a public constructor, getters named name() and age() (no get prefix), equals(), hashCode(), toString(). We'll cover records properly in the modern Java chapter. For now, know they exist — they're often what you actually want in 2026 instead of writing 50 lines of getters and setters.
**Pragmatic rule.** Don't reflexively write getters and setters for every field. If a field is truly part of the public contract and might need validation, use a setter. If it's read-only data, use a record. If it's internal state, keep it private with no setter at all.
The `this` keyword in three contexts
this shows up in three places. Same meaning, different uses.
**1. Disambiguating shadowed fields.** When a parameter has the same name as a field:
public User(String name) {
this.name = name; // this.name is the field; name is the parameter
}
**2. Calling another constructor.** First line of a constructor only:
public User(String name) {
this(name, 0); // call the 2-arg constructor
}
public User(String name, int age) { ... }
**3. Passing the current object as an argument.**
public class Cart {
void checkout() {
PaymentService.process(this); // pass the cart to another service
}
}
Outside a constructor, this is just "the object this method is being called on." It exists implicitly in every instance method. You can use it explicitly when needed, omit it when not.
Static methods don't have this — there's no specific object they belong to.
Initialisation order — when things actually run
Java initialises an object in a strict order. Getting this wrong leads to NullPointerException at object construction time.
For a single object, the order is:
- **Static fields** of the class are initialised (only the first time the class is loaded into the JVM).
- **Static initialiser blocks** run (in source order).
- **Instance fields** with initialisers are assigned (in source order).
- **Instance initialiser blocks** run (rare in practice).
- **The constructor body** executes.
public class Demo {
static int staticField = init("static");
int instanceField = init("instance");
static { System.out.println("static block"); }
{ System.out.println("instance block"); }
public Demo() {
System.out.println("constructor");
}
static int init(String label) {
System.out.println("init " + label);
return 0;
}
}
new Demo();
// Output:
// init static
// static block
// init instance
// instance block
// constructor
In a class hierarchy (covered next chapter), the parent class is fully initialised before the child. If you call an overridable method from a constructor, you can hit half-built state — a subtle bug that's caught us all at least once.
The practical rule: keep constructors simple. Assign fields. Don't call overridable methods. Don't start threads. Don't do I/O. Anything fancier belongs in a factory method or a builder, not in the constructor.
Common mistakes
The shortlist of class-related bugs that show up in real codebases.
**1. Forgetting this.** in a constructor:
public User(String name) {
name = name; // assigns parameter to itself, leaves field unset
}
The IDE will warn. Heed it.
**2. Confusing static and instance scope.**
public class Counter {
int count;
static void reset() {
count = 0; // compile error — no instance available
}
}
Either make reset() non-static or make count static.
**3. Using new inside a static initialiser of a class that hasn't finished loading.** Triggers class-loading recursion. Hard to debug, easy to avoid: keep static initialisers simple.
**4. Calling overridable methods from constructors.**
public class Parent {
public Parent() {
init();
}
public void init() { ... }
}
public class Child extends Parent {
String value;
@Override
public void init() {
value.length(); // NPE! value isn't assigned yet at this point
}
}
When you new Child(), the parent constructor runs first and calls init() — but value hasn't been assigned yet, because Child's field initialisers run after the parent constructor. Solution: don't call overridable methods from constructors.
**5. Holding references that prevent GC.** A static collection that you keep adding to but never clean up is a memory leak. The objects can't be collected because the static field reaches them indirectly.
**6. Mutable default field values.**
public class Container {
public List<String> items = new ArrayList<>(); // every instance shares... no, wait
}
This is actually FINE in Java — each instance gets its own ArrayList. The trap is when you put the assignment in a static field:
public class Container {
public static final List<String> ITEMS = new ArrayList<>(); // ALL instances share this
}
Now there's one ArrayList for the whole JVM. Concurrent modifications from multiple threads can corrupt it. Subtle bug.
⁂ Back to all modules