Java OOP Introduction
Object-oriented programming (OOP) is a way of structuring code around objects — self-contained units that bundle data (fields) together with the behavior (methods) that operates on that data. Instead of writing a long list of instructions that manipulate loose variables, you model your program as a collection of interacting objects, each responsible for its own state. Java was built from the ground up as an object-oriented language: almost everything you write, other than a few primitive types, lives inside a class. Understanding OOP is therefore not optional in Java — it is the foundation every other feature is built on.
Overview: What Is Object-Oriented Programming?
In procedural programming, you write functions that take data and transform it, and the data itself is usually kept separate from the logic that changes it. OOP flips this around: data and the functions that act on that data are packaged together into a single unit called an object. An object is created from a class, which acts as a blueprint or template describing what fields (data) and methods (behavior) every object of that type will have.
For example, a Car class might define that every car has a brand and a speed, and a method accelerate() that changes the speed. The class itself is not a car — it is a description of what a car looks like. Each time you use the new keyword, you create an instance of that class: an actual object living in memory, with its own independent copy of the fields.
Java’s OOP model rests on four core ideas, often called the four pillars:
- Encapsulation — bundling data and the methods that operate on it inside a class, and hiding the internal details from outside code (typically using
privatefields with public getter/setter methods). - Inheritance — letting one class reuse and extend the fields and methods of another class, forming an “is-a” relationship (covered in a later lesson).
- Polymorphism — allowing different classes to be treated through a common interface, so the same method call can behave differently depending on the actual object (also covered later).
- Abstraction — exposing only the essential features of an object while hiding the complicated implementation details.
This lesson focuses on the foundation of all four: classes, objects, fields, methods, and constructors, plus your first taste of encapsulation.
How the JVM Handles Classes and Objects
Understanding what actually happens when you write object-oriented Java code makes the syntax feel far less arbitrary. When your program starts, the JVM’s class loader reads each compiled .class file and stores the class’s structure — its method bytecode, field definitions, and metadata — in an area of memory called the method area (part of what is often called Metaspace in modern JVMs). This happens once per class, no matter how many objects you create from it.
Every time you call new SomeClass(), the JVM allocates a fresh block of memory on the heap large enough to hold that object’s fields, initializes those fields to their default values (0, false, or null depending on type), and then runs the constructor to set up the object’s real starting state. The new expression returns a reference — essentially a memory address — which you typically store in a variable on the stack. That variable does not contain the object itself; it points to it. This is why assigning one object variable to another copies the reference, not the object, and why Java’s automatic garbage collector can safely reclaim heap memory once no reference anywhere in the program still points to it.
Syntax
The general shape of a class definition looks like this:
class ClassName {
// fields (state)
dataType fieldName;
// constructor
ClassName(parameters) {
// initialize fields
}
// methods (behavior)
returnType methodName(parameters) {
// logic
}
}
// creating and using an object
ClassName variableName = new ClassName(arguments);
variableName.fieldName;
variableName.methodName(arguments);
| Part | Purpose |
|---|---|
class ClassName |
Declares the blueprint. By convention, class names start with an uppercase letter. |
| Fields | Variables declared inside the class body (outside any method) that hold each object’s state. |
| Constructor | A special method with the same name as the class and no return type, run automatically when new is used, used to set up initial field values. |
| Methods | Functions defined inside the class that describe the object’s behavior; they can read and change the object’s fields. |
new |
The keyword that allocates a new object on the heap and returns a reference to it. |
. (dot operator) |
Used to access a field or call a method on a specific object reference. |
Examples
Example 1: A Simple Class with Fields and a Method
public class Main {
static class Car {
String brand;
int speed;
void accelerate(int amount) {
speed += amount;
System.out.println(brand + " is now going " + speed + " mph.");
}
}
public static void main(String[] args) {
Car myCar = new Car();
myCar.brand = "Toyota";
myCar.speed = 0;
myCar.accelerate(30);
myCar.accelerate(25);
}
}
Output:
Toyota is now going 30 mph.
Toyota is now going 55 mph.
Here Car is a blueprint with two fields, brand and speed. myCar is one object created from that blueprint. Calling accelerate() modifies that specific object’s speed field — if you created a second Car, it would have its own independent speed, completely unaffected by the first.
Example 2: Using a Constructor to Initialize Objects
Setting fields one by one after new is error-prone — you might forget one. A constructor lets you require the caller to supply the initial state up front.
public class Main {
static class Dog {
String name;
String breed;
Dog(String name, String breed) {
this.name = name;
this.breed = breed;
}
void bark() {
System.out.println(name + " the " + breed + " says Woof!");
}
}
public static void main(String[] args) {
Dog d1 = new Dog("Rex", "German Shepherd");
Dog d2 = new Dog("Bella", "Poodle");
d1.bark();
d2.bark();
}
}
Output:
Rex the German Shepherd says Woof!
Bella the Poodle says Woof!
The constructor Dog(String name, String breed) runs automatically the moment new Dog(...) executes. Inside it, this.name = name; assigns the constructor’s parameter to the object’s field — this refers to “the object currently being constructed”, which is essential when a parameter has the same name as a field.
Example 3: Encapsulation with Private Fields
So far, any code that has a reference to an object can directly change its fields, even to invalid values. Encapsulation fixes this by making fields private (accessible only inside the class) and exposing controlled access through methods.
public class Main {
static class BankAccount {
private String owner;
private double balance;
BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
} else {
System.out.println("Withdrawal denied: insufficient funds.");
}
}
double getBalance() {
return balance;
}
String getOwner() {
return owner;
}
}
public static void main(String[] args) {
BankAccount account = new BankAccount("Alice", 100.0);
account.deposit(50.0);
account.withdraw(30.0);
account.withdraw(1000.0);
System.out.println(account.getOwner() + "'s balance: $" + account.getBalance());
}
}
Output:
Withdrawal denied: insufficient funds.
Alice's balance: $120.0
Because balance is private, no code outside BankAccount can set it directly — it can only be changed through deposit() and withdraw(), both of which validate the request first. The final withdrawal of 1000 is rejected because it exceeds the balance, so the account correctly ends at 120.0 (100 + 50 − 30).
How It Works Step by Step (Under the Hood)
- 1. Class loading. The first time a class is used, the JVM loads its bytecode into the method area and verifies it.
- 2. Allocation. When
new BankAccount(...)runs, the JVM reserves a block of heap memory sized for the object’s fields (ownerandbalance). - 3. Default initialization. Before the constructor runs, fields are zeroed out:
ownerstarts asnull,balancestarts as0.0. - 4. Constructor execution. The constructor body runs, assigning the real starting values passed in by the caller.
- 5. Reference returned. The address of the new object on the heap is returned and stored in the
accountvariable, which lives on the stack of themainmethod. - 6. Method dispatch. Each call like
account.deposit(50.0)uses the reference to locate the object on the heap, then executes thedepositbytecode from the method area against that object’s fields. - 7. Garbage collection. Once no variable anywhere references the object (for example, after
mainreturns), it becomes eligible for the garbage collector to reclaim its heap memory.
Common Mistakes
Mistake 1: Leaving Fields Public
If fields are declared public, any code with a reference to the object can set them to invalid values, bypassing any validation logic you write:
public class Main {
static class Account {
public double balance;
}
public static void main(String[] args) {
Account acc = new Account();
acc.balance = -500;
System.out.println("Balance: " + acc.balance);
}
}
Output:
Balance: -500.0
Nothing stops balance from becoming negative, because there is no method guarding it. The fix is exactly what Example 3 showed: make the field private and only allow changes through methods like deposit()/withdraw() that can reject bad input.
Mistake 2: Forgetting this in a Constructor
When a constructor parameter has the same name as a field, omitting this causes the parameter to refer to itself instead of the field — the field is silently never set:
public class Main {
static class Point {
int x;
int y;
Point(int x, int y) {
x = x;
y = y;
}
}
public static void main(String[] args) {
Point p = new Point(5, 10);
System.out.println(p.x + ", " + p.y);
}
}
Output:
0, 0
This compiles without any error or warning, which makes it especially dangerous — the fields silently keep their default values of 0 instead of 5 and 10. Using this.x = x; and this.y = y; tells the compiler explicitly “assign to the object’s field”, producing the correct output of 5, 10.
Best Practices
- Make fields
privateby default, and only expose what callers actually need through methods — this is the essence of encapsulation. - Give every class a clear, single responsibility; if a class is doing too many unrelated things, split it up.
- Prefer constructors that require all mandatory fields as parameters, so an object can never exist in a half-initialized state.
- Use
thisexplicitly whenever a constructor or method parameter shares a name with a field, to avoid silent shadowing bugs. - Name classes with nouns (
Car,BankAccount) and methods with verbs (accelerate,deposit) so code reads naturally. - Validate input inside setter-style methods rather than trusting the caller to pass sensible values.
Practice Exercises
- Exercise 1: Write a
Rectangleclass with privatewidthandheightfields, a constructor, and a methodgetArea()that returnswidth * height. Create two rectangles and print both of their areas. - Exercise 2: Write a
Studentclass with fieldsnameand anint[]of test scores, plus a methodaverageScore()that returns the average as adouble. Create one student with at least three scores and print the average. - Exercise 3: Take the
BankAccountclass from Example 3 and add atransfer(BankAccount other, double amount)method that withdraws from the current account and deposits intoother, only if the withdrawal succeeds. Test it with two accounts.
Summary
- OOP organizes programs as interacting objects, each combining data (fields) and behavior (methods).
- A class is a blueprint; an object is a specific instance created with
new. - Constructors initialize an object’s starting state automatically when it is created.
- The JVM stores class definitions in the method area and allocates each object on the heap, accessed through references held in variables.
- Encapsulation — using
privatefields with controlled public methods — protects an object’s internal state from invalid changes. - Forgetting
thiswhen a parameter shadows a field is a common, silent bug — always double-check constructors that assign fields. - The remaining OOP pillars, inheritance and polymorphism, build directly on the class and object fundamentals covered here.
