Introduction to Classes and Objects
Everything in Java is built around the concept of objects. An object is a self-contained entity that holds both state (data) and behavior (actions). When you write a Java application, you are essentially defining what objects your program needs, what data they hold, and what actions they can perform.
If objects are the actual "things" in your program, a class is the blueprint used to create them. You can think of a class like an architectural blueprint for a house. The blueprint itself is not a house—you cannot live in it. However, you can use that single blueprint to build many actual houses (objects), each with its own specific address, paint color, and furniture, but all sharing the same fundamental structure.
Why Object-Oriented Programming?
Modeling software as objects makes complex programs easier to understand, manage, and scale. Instead of having disconnected
variables
and
methods
scattered around, OOP groups related data and logic together. A
Student
object holds the student's name and grades, and also contains the methods to calculate their GPA. This organization mirrors how we naturally think about the real world.
Prerequisites
Before diving into classes and objects, make sure you are comfortable with:
What You Will Learn
- How to define a custom Java class with fields and methods
- The difference between instance variables and local variables
-
How to create objects using the
newkeyword - The role of constructors in object initialization
-
How to use the
thiskeyword to resolve naming conflicts - The principle of encapsulation and how to implement it using access modifiers
State and Behavior
Every class you design should clearly define two things:
-
State (Attributes/Fields):
What does an object of this class know about itself? For a
Carclass, the state might includecolor,make,model, andspeed. In code, state is represented by instance variables. -
Behavior (Methods/Operations):
What can an object of this class do? A
Carcanaccelerate(),brake(), andhonk(). Behavior is represented by methods.
When you create an object from a class, that specific object gets its own unique copy of the instance variables (its own state), but it shares the method definitions (the behavior) with all other objects of that class.
Class Definition Syntax
Here is the general structure for defining a class in Java:
public class ClassName {
// 1. Fields (Instance Variables) - Define the state
private DataType fieldName;
// 2. Constructors - Initialize the state
public ClassName(DataType parameterName) {
this.fieldName = parameterName;
}
// 3. Methods - Define the behavior
public ReturnType methodName() {
// Method logic
}
}
By convention, class names in Java use
PascalCase
(the first letter of every word is capitalized, e.g.,
BankAccount
,
UserService
).
Simple Example: Creating a Car Class
Let's create a simple
Car
class to see how state, behavior, and object creation work together.
public class Car {
// State (Instance Variables)
private String make;
private String model;
private int year;
// Constructor
public Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
// Behavior (Method)
public void displayInfo() {
System.out.println(this.year + " " + this.make + " " + this.model);
}
}
Now, let's create a
Main
class to instantiate (create) some
Car
objects:
public class Main {
public static void main(String[] args) {
// Creating objects using the 'new' keyword
Car car1 = new Car("Toyota", "Corolla", 2022);
Car car2 = new Car("Ford", "Mustang", 2023);
// Calling behavior on the objects
car1.displayInfo();
car2.displayInfo();
}
}
How It Works: Memory and References
When the line
Car car1 = new Car(...);
executes, two distinct things happen in memory:
-
The
newkeyword: It asks the JVM to allocate enough memory on the Heap to hold aCarobject (space for a String reference, another String reference, and an integer). It then initializes those fields using the constructor. -
The
=operator: It stores the memory address of that new Heap object into the reference variablecar1, which lives on the Stack .
car1
is
not
the object itself; it is a remote control pointing to the object. If you write
Car car3 = car1;
, you do
not
create a second car. You simply create a second remote control (
car3
) that points to the exact same Car object on the Heap that
car1
points to.
Understanding Constructors
A constructor is a special block of code used to initialize a newly created object. It looks like a method but has two strict rules:
- It must have the exact same name as the class.
-
It has
no return type
(not even
void).
The Default Constructor
If you do not write any constructor in your class, Java automatically provides a "no-argument" default constructor that does nothing. However, the moment you write
any
constructor (like the one in the
Car
class above), Java removes the default one.
Constructor Overloading
Just like methods , constructors can be overloaded. You can provide multiple ways to create an object.
public class Book {
private String title;
private String author;
private int pages;
// Constructor 1: Full details
public Book(String title, String author, int pages) {
this.title = title;
this.author = author;
this.pages = pages;
}
// Constructor 2: Unknown page count
public Book(String title, String author) {
this.title = title;
this.author = author;
this.pages = 0; // Default value
}
// Constructor 3: No-argument constructor
public Book() {
this.title = "Unknown Title";
this.author = "Unknown Author";
this.pages = 0;
}
}
The
this()
Constructor Call
You can call one constructor from another using
this()
as the first line of a constructor. For example,
public Book() { this("Unknown", "Unknown", 0); }
. This prevents you from duplicating initialization code across multiple constructors.
Real-World Example: Bank Account with Encapsulation
A core principle of OOP is Encapsulation : hiding the internal state of an object and requiring all interaction to be performed through an object's methods. This protects the data from being put into an invalid state (like a bank account balance dropping below zero).
public class BankAccount {
// State is hidden (private)
private String accountNumber;
private double balance;
// Constructor to initialize the account
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
// Enforce business rule: balance cannot start negative
if (initialBalance >= 0) {
this.balance = initialBalance;
} else {
System.out.println("Error: Initial balance cannot be negative. Set to 0.");
this.balance = 0;
}
}
// Behavior: Safe way to modify state
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.printf("Deposited: $%.2f. New Balance: $%.2f%n", amount, balance);
} else {
System.out.println("Error: Deposit amount must be positive.");
}
}
public void withdraw(double amount) {
if (amount <= 0) {
System.out.println("Error: Withdrawal amount must be positive.");
} else if (amount > balance) {
System.out.println("Error: Insufficient funds.");
} else {
balance -= amount;
System.out.printf("Withdrew: $%.2f. New Balance: $%.2f%n", amount, balance);
}
}
// Read-only access to private data (Getters)
public String getAccountNumber() { return accountNumber; }
public double getBalance() { return balance; }
}
And here is how it is used safely:
public class Main {
public static void main(String[] args) {
BankAccount myAccount = new BankAccount("123456789", 500.00);
myAccount.deposit(150.50);
myAccount.withdraw(100.00);
myAccount.withdraw(600.00); // Tests insufficient funds
// myAccount.balance = -1000; // COMPILE ERROR! balance is private.
}
}
Common Mistakes to Avoid
Mistake 1: Forgetting the
new
Keyword
Beginners often try to create an object like they declare a primitive variable.
Car myCar; // This only creates a reference, NOT an object!
myCar.displayInfo(); // NullPointerException at runtime
Car myCar = new Car("Honda", "Civic", 2021); // Object is created on the heap
Mistake 2: Static Context Errors
You cannot directly access instance variables (like
make
) or call instance methods from a
static
method (like
main
) without first creating an object.
public static void main(String[] args) {
System.out.println(make); // Compile error: non-static variable make cannot be referenced from a static context
}
Mistake 3: Creating Multiple Classes in One File Improperly
In Java, a file can only have
one
public
class, and the file name must exactly match that public class name (e.g.,
Car.java
for
public class Car
). You can have other non-public classes in the same file, but for beginners, it is highly recommended to put every class in its own separate
.java
file to avoid confusion.
Best Practices for Classes and Objects
-
Enforce Encapsulation:
Always make your instance variables
private. Expose them only through public "getter" and "setter" methods if external code needs to read or modify them. -
Design for Single Responsibility:
A class should have one, and only one, reason to change. A
Studentclass should manage student data. It should not contain logic for saving to a database—that belongs in aStudentRepositoryclass. - Use Constructors for Mandatory Data: If an object cannot logically exist without certain data (like a BankAccount without an account number), require that data in the constructor. Do not rely on setters to set mandatory data later.
-
Prefer Immutability When Possible:
If an object's state shouldn't change after creation (like a
MonthorColorobject), make all fieldsprivate finaland don't provide setters. In Java 14+, you can use therecordkeyword for this.
Performance Considerations
While modern JVMs are incredibly efficient at creating and destroying objects, keeping memory behavior in mind helps write scalable applications:
- Object Creation Overhead: Allocating memory on the heap takes longer than allocating primitives on the stack. Avoid creating unnecessary objects inside tight loops.
-
Memory Footprint:
Every object in Java has a header (usually 12-16 bytes) overhead, plus padding. A simple object holding one
booleanmight actually take up 16 bytes of memory. If you need to process millions of data points, consider using primitive arrays instead of collections of objects.
for (int i = 0; i < 1000000; i++) {
// Creating a new DecimalFormat object 1 million times is wasteful
DecimalFormat df = new DecimalFormat("#.##");
System.out.println(df.format(someValue));
}
// BETTER: Create it once outside the loop
DecimalFormat df = new DecimalFormat("#.##");
for (int i = 0; i < 1000000; i++) {
System.out.println(df.format(someValue));
}
Exercises
Exercise 1: The Rectangle Class
Create a
Rectangle
class with
width
and
height
as private double fields. Include a constructor, a method named
getArea()
that returns the area, and a method named
getPerimeter()
that returns the perimeter. Instantiate a rectangle in
main
and print its area and perimeter.
Exercise 2: Enhanced Student Class
Create a
Student
class with
name
and
grades
(an array or ArrayList of integers). Provide a method
addGrade(int grade)
that adds a grade to the list, and a method
getAverageGrade()
that calculates and returns the average as a double. Ensure grades cannot be added if they are outside the 0-100 range.
Solutions
Solution to Exercise 1: The Rectangle Class
public class Rectangle {
private double width;
private double height;
public Rectangle(double width, double height) {
// Validate inputs to prevent negative dimensions
if (width > 0 && height > 0) {
this.width = width;
this.height = height;
} else {
throw new IllegalArgumentException("Dimensions must be positive.");
}
}
public double getArea() {
return width * height;
}
public double getPerimeter() {
return 2 * (width + height);
}
public static void main(String[] args) {
Rectangle room = new Rectangle(5.5, 3.2);
System.out.printf("Area: %.2f%n", room.getArea());
System.out.printf("Perimeter: %.2f%n", room.getPerimeter());
}
}
Explanation: Notice how the constructor actively protects the object's state by throwing an exception if invalid data is passed. This is much better than allowing a negative width and letting the program calculate nonsensical results later.
Solution to Exercise 2: Enhanced Student Class
import java.util.ArrayList;
import java.util.List;
public class Student {
private String name;
private List<Integer> grades;
public Student(String name) {
this.name = name;
this.grades = new ArrayList<>();
}
public void addGrade(int grade) {
if (grade >= 0 && grade <= 100) {
grades.add(grade);
} else {
System.out.println("Invalid grade: " + grade + ". Must be 0-100.");
}
}
public double getAverageGrade() {
if (grades.isEmpty()) {
return 0.0;
}
int sum = 0;
for (int grade : grades) {
sum += grade;
}
return (double) sum / grades.size();
}
public String getName() { return name; }
public static void main(String[] args) {
Student alice = new Student("Alice");
alice.addGrade(85);
alice.addGrade(105); // Will be rejected
alice.addGrade(92);
alice.addGrade(78);
System.out.println(alice.getName() + "'s average: " +
String.format("%.2f", alice.getAverageGrade()));
}
}
Explanation:
The
addGrade
method encapsulates the business rule that grades must be between 0 and 100. The
getAverageGrade
method safely handles the edge case where no grades have been added yet to prevent a division-by-zero error.
Summary
- A class is a blueprint defining state (fields) and behavior (methods).
- An object is a specific instance of a class allocated in the Heap memory.
-
The
newkeyword allocates memory and invokes a constructor. - Constructors initialize the object's state and can be overloaded to provide multiple ways to create an object.
-
The
thiskeyword refers to the current object and is used to differentiate between instance variables and parameters. -
Encapsulation
(hiding fields with
privateand exposing them via methods) protects the integrity of an object's data.
Frequently Asked Questions
A class is a blueprint or template that defines the structure (fields) and capabilities (methods) of something. An object is a concrete, living instance created from that blueprint in memory. For example,
String
is a class, but
"Hello World"
is an object of the
String
class.
The
new
keyword tells the Java Virtual Machine to allocate memory on the Heap for a new object. It invokes the class's constructor to set up the initial state of that object, and finally, it returns a reference (memory address) to that object so you can assign it to a variable.
The
this
keyword is a reference variable that points to the current object—the specific instance whose method or constructor is currently executing. It is most commonly used to resolve shadowing, such as when a constructor parameter has the same name as an instance field (e.g.,
this.name = name;
).
Objects themselves are stored in the Heap , which is a large pool of memory shared across all threads in your application. However, the variables that "point" to those objects (the references) are stored on the Stack , alongside local primitive variables and method call data.
Yes, syntactically, Java allows an empty class (e.g.,
class Empty {}
). However, an object created from an empty class has no state and no behavior, making it practically useless. In modern Java, if you just need to pass a few pieces of immutable data around, you should use a
record
instead of a manually written empty class.