Classes and Objects
Declare a class with fields and methods, create objects with new, and understand that variables hold references, not the objects themselves.
class Name {
Type field;
ReturnType method(Params) { ... }
}
Name obj = new Name();
obj.method(args);A class describes a kind of thing: the data it holds (fields) and what it can do (methods). An object is one instance of that class, created at runtime with new. A back-end service is full of them: a request DTO, a UserService, a connection pool, a cache. Each object has its own copy of the instance fields, so two Counter objects count independently.
Variables hold references
A variable of a class type does not contain the object. It contains a reference that points at an object on the heap. Assigning one variable to another copies the reference, so both names now point at the same object and a change through one is visible through the other:
Counter a = new Counter();
Counter b = a; // same object, two names
b.hit(); // a.hits is now 1 as wellThis is the source of a common bug in services: a method stores a list or object it was handed, the caller keeps modifying it, and the stored state changes underneath you. == on references asks "is this the same object?", not "do these hold the same data?". For value comparison you need equals, covered later in this stage.
Default values and null
Fields are given default values when the object is created: 0 for numbers, false for boolean, and null for every reference type, including String and List. Local variables get no default; the compiler refuses to let you read one before it is assigned. That difference matters: a forgotten field assignment compiles fine and fails later with a NullPointerException, often far from the cause. Constructors (next lesson) exist so an object is complete the moment it is created.
Classes, records or neither
Use a class when an object has behaviour and state that changes over its lifetime, such as a rate limiter or an in-memory repository. When a type is only a carrier of values, like a JSON request body or a database row, a record is shorter and gives you equality for free. Keep each class focused on one job; a class that parses requests, talks to the database and formats emails is three classes waiting to be split.
Objects are cheap to create in Java. Do not reuse one mutable object across requests to save allocation; the garbage collector handles short-lived objects well, while shared mutable state across threads causes real bugs.
Example
When to use it
- Model an in-memory user repository that keeps its map and id counter together with the methods that change them.
- Create one service object at startup and pass the same reference to every request handler that needs it.
- Track hit counts per endpoint with one counter object per route.
More examples
Default field values
Every field starts at zero, false or null without any code from you. A configuration object built this way silently runs on port 0 or with a null host unless something sets the values, which is why constructors that demand them are safer.
import java.util.List;
class Settings {
int port;
boolean debug;
double ratio;
String host;
List<String> origins;
}
void main() {
Settings s = new Settings();
IO.println("port=" + s.port);
IO.println("debug=" + s.debug);
IO.println("ratio=" + s.ratio);
IO.println("host=" + s.host);
IO.println("origins=" + s.origins);
}
// Output:
// port=0
// debug=false
// ratio=0.0
// host=null
// origins=nullA method changes the caller's object
The method receives a copy of the reference, which still points at the caller's object, so the discount is visible afterwards. Passing null compiles happily and fails only when a field is read through it.
class Order {
String id;
int totalCents;
}
void applyDiscount(Order order, int percent) {
order.totalCents = order.totalCents * (100 - percent) / 100;
}
void main() {
Order order = new Order();
order.id = "ord-1";
order.totalCents = 5000;
applyDiscount(order, 10);
IO.println(order.id + " " + order.totalCents);
Order missing = null;
try {
applyDiscount(missing, 10);
} catch (NullPointerException e) {
IO.println("Caught " + e.getClass().getSimpleName());
}
}
// Output:
// ord-1 4500
// Caught NullPointerExceptionA small service object
This is the shape of an in-memory repository: state (the map and the id counter) kept together with the methods that change it. Every caller that shares this one object sees the same users, which is what you want for a store and why it must later be made thread-safe.
import java.util.LinkedHashMap;
import java.util.Map;
class UserStore {
Map<String, String> emailsById = new LinkedHashMap<>();
int nextId = 1;
String register(String email) {
String id = "u" + nextId++;
emailsById.put(id, email);
return id;
}
String find(String id) {
return emailsById.getOrDefault(id, "not found");
}
}
void main() {
UserStore store = new UserStore();
String ada = store.register("[email protected]");
String lin = store.register("[email protected]");
IO.println(ada + " -> " + store.find(ada));
IO.println(lin + " -> " + store.find(lin));
IO.println("u9 -> " + store.find("u9"));
IO.println(store.emailsById.size() + " users");
}
// Output:
// u1 -> [email protected]
// u2 -> [email protected]
// u9 -> not found
// 2 users
Discussion