AlgoInsight

Free lesson · Java Basic To Advance

Classes Objects

Medium8 min readjava

The Syntax of OOP

Topic 7.1 introduced OOP conceptually. This Topic gets concrete: how to declare a class in Java, how to create objects from it, where those objects live in memory, and the mechanics that make all of this work.

If Topic 7.1 was about the mental model, this Topic is about the syntax that implements the mental model. Most of the OOP concepts in later topics build on the basic mechanics covered here.

Declaring a Class

The simplest class:

public class User {
}

This is a valid Java class. It has no fields, no methods, no constructors — just an empty body. You can compile this and create instances of it, though they'd be useless.

Let's add some structure:

public class User {
    // Fields (state)
    private String name;
    private int age;

    // Methods (behavior)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

The class declares:

The private and public modifiers control access (Topic 7.5). The this keyword refers to the current object (Topic 7.8). We'll cover both in detail. For now, just know that this is a typical class shape: some private state, some public methods to access and modify it.

Class File Organization

By convention, each public class lives in its own file with the same name as the class:

src/
  main/
    java/
      com/
        algoinsight/
          User.java          // contains "public class User"
          Order.java         // contains "public class Order"

The file name must match the public class name exactly — User.java for class User. This is enforced by the compiler. You can have multiple non-public classes in a single file, but only one public class.

Package declarations and imports go at the top:

package com.algoinsight;

import java.util.List;
import java.time.LocalDate;

public class User {
    // ...
}

We covered packages briefly in earlier topics. Module 11 covers them in depth.

Creating Objects with new

To create an instance of a class, use the new keyword:

User alice = new User();

Breaking this down:

  1. new User() — invokes the User constructor, allocates memory for a new User object, and returns a reference to it
  2. User alice — declares a variable of type User to hold the reference
  3. alice = ... — assigns the reference to the variable

After this line, alice holds a reference to a User object somewhere in memory. We can call methods on it:

alice.setName("Alice");
alice.setAge(30);
System.out.println(alice.getName());   // "Alice"

The alice.method() syntax means "call this method on this specific object." The object's data is the implicit context — getName() returns this particular Alice's name, not some shared name across all users.

You can create multiple independent instances:

User alice = new User();
User bob = new User();
User charlie = new User();

alice.setName("Alice");
bob.setName("Bob");
charlie.setName("Charlie");

System.out.println(alice.getName());   // "Alice"
System.out.println(bob.getName());     // "Bob"
System.out.println(charlie.getName()); // "Charlie"

Each object has its own copy of the fields. Setting Alice's name doesn't affect Bob.

Interactive animation: What new User("Ada") does - allocate on the heap, set every field to its default, run the constructor, return a reference.

Where Objects Live: Stack vs Heap

Java has two main memory regions:

When you write User alice = new User():

graph LR
    A["Stack Frame<br/>(current method)"] --> B["alice: reference"]
    A --> C["bob: reference"]
    A --> D["charlie: reference"]

    B --> O1["User object<br/>name='Alice', age=30<br/>HEAP"]
    C --> O2["User object<br/>name='Bob', age=25<br/>HEAP"]
    D --> O3["User object<br/>name='Charlie', age=35<br/>HEAP"]

    style A fill:#7c3aed,color:#fff
    style O1 fill:#10b981,color:#fff
    style O2 fill:#10b981,color:#fff
    style O3 fill:#10b981,color:#fff
This diagram shows the memory layout when you create three User objects. The current method's stack frame contains three variables — alice, bob, and charlie — each holding a reference. The actual User objects live in the heap, with their state (name and age fields). Each variable on the stack points to a separate object on the heap. The variables are small (just a memory address, typically 8 bytes on 64-bit systems). The objects can be much larger. When the method returns, the stack frame is destroyed and the references disappear, but the objects remain on the heap until the garbage collector determines that nothing else references them. This stack-pointer-to-heap-object model is fundamental to how Java manages object memory.

This stack/heap distinction matters in several scenarios:

Pass-by-value semantics (Topic 6.7): when you pass alice to a method, the reference is copied — the receiving method gets a copy of the pointer, both pointing to the same object on the heap.

Garbage collection: when no references point to an object, the GC can reclaim its memory. This is why memory leaks in Java usually involve "I'm still holding a reference I forgot about" rather than "I forgot to free memory."

Performance: heap allocation is more expensive than stack allocation. For very performance-sensitive code, escaping to the heap matters. For typical application code, it doesn't.

We cover memory and GC in detail in Module 19.

Interactive animation: Two stack variables point at one User on the heap - the method frame dies, but the object outlives it.

The null Reference

A reference variable can hold null — a special value meaning "doesn't point to any object":

User user = null;
System.out.println(user);   // null
user.getName();              // NullPointerException

Calling methods on a null reference throws NullPointerException — the most common runtime error in Java. We've seen this throughout the earlier modules; it's central to OOP debugging.

Null exists for legitimate reasons: representing "no value," "not yet initialized," "lookup failed." But it's also Java's "billion dollar mistake" — countless bugs trace back to forgetting null checks.

Modern Java offers alternatives:

We cover these in their respective modules. For now, be aware that null is everywhere in Java and you need to handle it.

Interactive animation: Calling getName() on a null reference throws NullPointerException - nothing is wrong with the object, because there is no object.

Equality of Objects

Two object references are equal under == only if they refer to the same object:

User a = new User();
User b = new User();
User c = a;

a == b   // false — different objects
a == c   // true — same object

Even if a and b have identical field values, == returns false because they're separate objects on the heap.

For content equality, override equals():

public class User {
    private String name;
    private int age;

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof User)) return false;
        User u = (User) other;
        return age == u.age && Objects.equals(name, u.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

equals() is what defines "are these two objects equivalent" for your class. The default implementation (inherited from Object) is reference equality — same as ==. You override it to provide content equality.

We cover equals() and hashCode() in depth in Topic 7.9.

Interactive animation: Two new User("Ada") objects are false under == and under an equals nobody overrode, while two equal Strings print true.

Classes Are Templates

A subtle but important fact: a class itself isn't an object. It's a template. The class User doesn't have a name or an age — the instances of User have names and ages.

User.name = "Alice";   // compile error — no field 'name' on class User

To work with class-level data (shared across all instances or independent of any instance), you use static members. We cover those in Topic 7.7.

This distinction matters in interviews. "Where does the field name live?" The answer: in each instance, on the heap. Not in the class. The class is the blueprint; each instance gets its own copy of the fields the blueprint specifies.

A Common Pattern: POJOs

A POJO — Plain Old Java Object — is a class with private fields, public getters and setters, and minimal logic. It's the simplest useful class shape:

public class User {
    private String name;
    private int age;
    private String email;

    public User() {}   // default constructor (covered in Topic 7.4)

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

POJOs are mostly carriers of data — useful for transferring data between layers (API responses, database records, configuration). Frameworks like Spring and Hibernate rely heavily on POJO conventions.

In modern Java, records (Java 14+, covered in Topic 7.13) often replace POJOs for pure data carriers:

public record User(String name, int age, String email) {}

Same data shape, much less boilerplate. The record automatically generates the constructor, getters, equals, hashCode, and toString. For new code, prefer records when the class is just a data carrier.

For classes with significant logic (real methods that do work), POJO-style is still appropriate — records are designed for pure data.

Common Pitfalls

Warning: Forgetting new when creating an object. User alice; declares a variable but doesn't create an object. alice.method() throws NullPointerException. You need User alice = new User(); to actually have an object.

Treating a class like a variable. User.name = "Alice" doesn't work — there's no field name on the class itself. Fields belong to instances.

Public fields everywhere. Skipping encapsulation by making fields public. Works for trivial code, fails for real codebases. Topic 7.6 covers why encapsulation matters.

Mismatched filename and class name. User.java must contain public class User. The compiler rejects mismatches.

Reusing class names across packages without imports. Java resolves bare class names to the current package or imported types. User could mean different things in different files if you have multiple User classes in different packages.

Why This Matters in Interviews

Interview: "What's the difference between a class and an object?" is a foundational Java interview question. The expected answer: a class is a blueprint, defining structure and behavior. An object is an instance — concrete data in memory, built from the class blueprint. Many instances can exist for one class.

Follow-up: "Where do objects live in memory?" Heap, with references on the stack. This goes deeper into JVM internals in Module 19.

Topic 7.14 covers the Interview Lens for OOP foundations.

Try It

Exercise: Write each:

  1. A class Book with fields for title, author, isbn, year. Add getters and setters.
  2. Create three Book instances with different data. Verify each has independent field values.
  3. Create a fourth variable Book b4 = book1;. Modify book1's title. Verify b4 also sees the change (same object).
  4. Set a Book variable to null. Try calling a method on it. Observe the NullPointerException.
  5. Compare two Books with == — note it returns false even with same data.
  6. Try assigning a value to the class itself: Book.title = "..."; — observe the compile error.

Each exercise reinforces a specific concept from this Topic. The "same object" exercise (3) in particular shows pass-by-value of references in action.

Back to Java Basic To Advance