05 - Classes and Objects¶
What this session is¶
About 90 minutes. You'll learn how to define your own classes, create objects, write constructors and instance methods, and finally understand the difference between static and instance. By the end you can model real-world things - people, accounts, points, anything with structure.
This is the page that turns Java from "scripting with a lot of ceremony" into "object-oriented programming."
The problem¶
Variables hold one value. Real things have many properties at once: a person has a name, an age, a city. You could pass each property as a separate parameter - but past 3-4, code becomes unreadable.
A class lets you bundle properties (called fields) together, and attach methods that operate on them. An object is one instance of a class - like one specific person built from the Person template.
A class¶
That's it - a class with three fields and no methods. Save as Person.java.
Now use it in another file, Main.java:
public class Main {
public static void main(String[] args) {
Person alice = new Person();
alice.name = "Alice";
alice.age = 30;
alice.city = "Lagos";
System.out.println(alice.name + " is " + alice.age);
}
}
Compile both, run Main:
Output: Alice is 30.
What's new:
new Person()creates a new object of typePerson.newis the keyword for object creation; the parentheses call the constructor (which Java auto-generates if you don't write one - more soon).alice.name = "Alice"sets thenamefield on this specific object.- Same person, written
Person alice- the variable's type.
Constructors: setup logic¶
Setting each field manually after creation is tedious. A constructor runs when the object is created, with arguments:
public class Person {
String name;
int age;
String city;
public Person(String name, int age, String city) {
this.name = name;
this.age = age;
this.city = city;
}
}
The constructor has the same name as the class. No return type. this refers to "the object being constructed" - this.name is the object's field, name (without this) is the parameter. (When parameter names match field names, you need this to disambiguate. When they don't match, you can skip it.)
Now use it:
Much cleaner.
Instance methods¶
A method without static is an instance method - it operates on a specific object via this:
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String greet() {
return "Hi, I'm " + this.name;
}
public void birthday() {
this.age = this.age + 1;
}
}
Call them on an object:
Person alice = new Person("Alice", 30);
System.out.println(alice.greet()); // Hi, I'm Alice
alice.birthday();
System.out.println(alice.age); // 31
alice.greet() calls greet with this = alice. alice.birthday() mutates alice's age field in place.
static vs instance, finally explained¶
Now we can give the full answer:
- Instance members (fields, methods without
static) belong to each object. EachPersonhas its ownname,age,city.alice.greet()operates onalicespecifically. - Static members belong to the class itself, not any object. There's only one of them, shared.
public class Counter {
static int instancesCreated = 0; // shared across all Counters
int count = 0; // each Counter has its own
public Counter() {
instancesCreated++; // bumps the shared count
}
}
You access static members through the class name:
Counter a = new Counter();
Counter b = new Counter();
System.out.println(Counter.instancesCreated); // 2
In page 04 every method was static because we only had main. Now you can write proper instance methods.
main itself is static because it has to be - java Main needs to call it without first creating a Main object.
Encapsulation: private fields, public methods¶
The fields in Person above are accessible from anywhere: alice.name = "garbage" works from any code that has an alice. That's often bad - fields are implementation details that might change; you don't want everyone able to set them to anything.
The Java convention: mark fields private, expose methods to access/modify them.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
public void birthday() {
if (age < 200) age++; // validation: refuse implausible
}
}
Now external code can alice.getName() (read) and alice.birthday() (controlled mutation), but cannot directly alice.age = -1. The methods that read are conventionally called getters (getX); the methods that write are setters (setX). Java tools and frameworks rely on the naming convention.
Three access levels (you'll meet a fourth):
| Modifier | Visible from |
|---|---|
public |
Anywhere |
| (no modifier - "package-private") | Same package only |
private |
Same class only |
Default to private for fields; use public for methods that are part of your class's API.
The toString() method¶
If you print an object without doing anything special, you get something useless:
Override toString() to produce something readable:
public class Person {
private String name;
private int age;
// ... constructor, getters ...
@Override
public String toString() {
return "Person{name=" + name + ", age=" + age + "}";
}
}
The @Override annotation is optional but recommended - it tells the compiler "I'm overriding a method from a parent class" (every Java class inherits from Object, which has a default toString that produces the useless form). If you misspell the method name, the compiler complains because @Override lies.
Now:
println automatically calls toString(). So does string concatenation: "alice is " + alice builds a string by calling alice.toString().
A complete example¶
public class Account {
private final String owner;
private double balance;
public Account(String owner, double initialBalance) {
this.owner = owner;
this.balance = initialBalance;
}
public String getOwner() { return owner; }
public double getBalance() { return balance; }
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
if (amount > balance) throw new IllegalArgumentException("insufficient funds");
balance -= amount;
}
@Override
public String toString() {
return String.format("Account{owner=%s, balance=%.2f}", owner, balance);
}
}
Notes:
private final String owner-owneris immutable after construction.finalon fields is one of the strongest invariants Java offers; reach for it whenever a field's value shouldn't change after the object is built.balance += amountis shorthand forbalance = balance + amount. Common shortcuts:+=,-=,*=,/=.throw new IllegalArgumentException(...)raises an error (next page, page 07, covers exceptions properly).
Use it:
Account a = new Account("Alice", 100.0);
a.deposit(50);
a.withdraw(30);
System.out.println(a); // Account{owner=Alice, balance=120.00}
Inheritance (briefly)¶
Classes can extend other classes - pick up their fields and methods:
public class Animal {
protected String name; // protected = visible to subclasses
public Animal(String name) {
this.name = name;
}
public String speak() {
return "(generic animal sound)";
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name); // call parent constructor
}
@Override
public String speak() {
return name + " says woof";
}
}
Dog extends Animal - Dog inherits Animal's fields and methods. super(name) calls the parent's constructor. @Override on speak() says "I'm replacing the parent's version."
The modern Java advice: prefer composition over inheritance. Inheritance is tight coupling that bites later. Use it for genuine "is-a" relationships (a Dog IS an Animal); reach for "has-a" (a Garage HAS a Car) by storing instances as fields instead.
Page 08 covers a modern alternative - sealed classes + records - that captures most of what inheritance is used for, more safely.
Exercise¶
In a new file Rectangle.java:
-
Define a class
Rectanglewith two private fields:widthandheight(bothdouble). -
Add a constructor taking width and height.
-
Add getter methods for both.
-
Add
area()returningdouble-width * height. -
Add
perimeter()returningdouble-2 * (width + height). -
Override
toString()to produce something likeRectangle{width=5.0, height=3.0}. -
In a
Main.java, create aRectangle(5, 3). Print its area, perimeter, and the rectangle itself. -
Stretch: add a method
Rectangle scale(double factor)that returns a newRectanglewith both dimensions multiplied byfactor. (Don't mutate the original.) Test:r.scale(2).area()forRectangle(5, 3)should be 60.
What you might wonder¶
"What's the difference between a class and an object?" A class is the template ("Person - has name, age, city"). An object is one specific instance ("Alice, 30, Lagos"). You define classes once; you create many objects from each.
"What if I don't write a constructor?"
Java provides a default no-arg constructor: new Person(). As soon as you write any constructor yourself, the default disappears - you'd need to write a no-arg one explicitly if you want both.
"What's protected?"
The fourth access level: visible to subclasses (even in other packages) and to the same package. Used when a parent class wants subclasses to access a field. Rare in modern Java - composition is usually cleaner.
"Should I always write getters?" For classes you're going to use heavily, yes. For small private helpers nobody else sees, often you can skip them and just use the fields directly (in package-private classes). Records (page 08) eliminate the question for data-only types.
Done¶
You can now:
- Define your own classes with fields and methods.
- Write constructors (with this for disambiguation).
- Distinguish static (class-level) from instance (object-level) members.
- Apply encapsulation (private fields, public methods).
- Override toString() for readable debug output.
- Use final for immutable fields.
- Recognize inheritance and the "prefer composition" advice.
You can now model real things. Next page: how Java handles collections - many things at once.