Free lesson · Java Basic To Advance
Classes Objects
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:
- Fields
nameandage— the state each User object will have - Methods
getName,setName,getAge,setAge— the operations available on User objects
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:
new User()— invokes the User constructor, allocates memory for a new User object, and returns a reference to itUser alice— declares a variable of type User to hold the referencealice = ...— 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:
- Stack — fast, automatically managed memory for method frames and local variables
- Heap — slower but flexible memory for objects, garbage-collected when no longer referenced
When you write User alice = new User():
- The variable
alicelives on the stack (it's a local variable in the current method) - The User object itself lives on the heap
- The variable holds a reference — essentially a memory address pointing to the object
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:
- Optional (Java 8+) for "might or might not be present"
- Records and immutable types to avoid null fields
- Annotations like
@NonNullfor static checking
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
newwhen creating an object.User alice;declares a variable but doesn't create an object.alice.method()throws NullPointerException. You needUser alice = new User();to actually have an object.Treating a class like a variable.
User.name = "Alice"doesn't work — there's no fieldnameon 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.javamust containpublic 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.
Usercould 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:
- A class
Bookwith fields for title, author, isbn, year. Add getters and setters.- Create three Book instances with different data. Verify each has independent field values.
- Create a fourth variable
Book b4 = book1;. Modify book1's title. Verify b4 also sees the change (same object).- Set a Book variable to null. Try calling a method on it. Observe the NullPointerException.
- Compare two Books with
==— note it returns false even with same data.- 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.