Java Classes and Objects
A class is a blueprint that describes what data an object holds and what it can do, while an object is a concrete instance of that blueprint created while your program runs. Almost everything in Java is organized around classes and objects — understanding them well is the foundation for everything else in object-oriented programming, from encapsulation to inheritance to polymorphism.
Overview: How Classes and Objects Work
A class does not itself hold any data. It is a template that defines fields (the data an object will carry) and methods (the behavior an object can perform). Nothing actually exists in memory until you use the new keyword to create an object from that class — this process is called instantiation.
When you write Car myCar = new Car("Toyota", "Corolla", 2022);, several things happen. The Java Virtual Machine (JVM) first makes sure the Car class has been loaded into memory (class loading happens once per class, not once per object). It then allocates a block of memory on the heap large enough to hold all of the object’s instance fields. The constructor runs and initializes those fields, and finally the new expression evaluates to a reference — essentially an address — pointing at that block of memory. That reference is what gets stored in the variable myCar.
This distinction between the object (on the heap) and the variable (a reference to it, usually stored on the stack for local variables) matters a lot in practice. When you assign one object variable to another, like Car anotherCar = myCar;, you are not copying the car’s data — you are copying the reference. Both variables now point at the exact same object, so a change made through one variable is visible through the other. Objects are only truly duplicated if you write code that explicitly copies their field values into a new object.
Each object gets its own independent copy of the class’s instance fields (also called instance variables). If you create three Car objects, each one has its own make, model, and odometer stored separately in memory. Methods, on the other hand, are not duplicated per object — the JVM stores one copy of the method’s bytecode and simply runs it with a hidden reference to whichever object it was called on (accessible inside the method as this).
A well-designed class also practices encapsulation: it keeps its fields private so outside code cannot reach in and corrupt its data directly, and it exposes controlled access through public methods (often called getters and setters, or more meaningful behavior methods like deposit() and withdraw()). This lets the class enforce its own rules — for example, refusing to let a bank balance go negative.
Syntax
class ClassName {
// fields (instance variables)
accessModifier type fieldName;
// constructor(s)
ClassName(parameters) {
// initialize fields, usually with 'this'
}
// methods
accessModifier returnType methodName(parameters) {
// behavior
}
}
// creating an object from the class
ClassName variableName = new ClassName(arguments);
| Part | Meaning |
|---|---|
class ClassName |
Declares a new blueprint. By convention, class names start with an uppercase letter. |
| Fields | Variables declared directly inside the class body (outside any method) that store an object’s state. |
| Constructor | A special method with the same name as the class and no return type, used to initialize a new object’s fields. |
this |
A reference to the current object, most often used to distinguish a field from a parameter that shares its name. |
| Methods | Functions defined inside the class that describe the object’s behavior; they can read and modify its fields. |
new |
The operator that allocates memory for a new object and calls its constructor, returning a reference to the object. |
| Access modifiers | private, public, or (no modifier) package-private, controlling who can see a field or method. |
Examples
Example 1: A basic class with fields, a constructor, and methods
public class Main {
public static void main(String[] args) {
Car car1 = new Car("Toyota", "Corolla", 2022);
Car car2 = new Car("Honda", "Civic", 2023);
car1.displayInfo();
car2.displayInfo();
car1.drive(150);
car1.displayInfo();
}
}
class Car {
String make;
String model;
int year;
int odometer;
Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
this.odometer = 0;
}
void drive(int miles) {
odometer += miles;
}
void displayInfo() {
System.out.println(year + " " + make + " " + model + " - Odometer: " + odometer + " miles");
}
}
Output:
2022 Toyota Corolla - Odometer: 0 miles
2023 Honda Civic - Odometer: 0 miles
2022 Toyota Corolla - Odometer: 150 miles
Two independent Car objects are created, each with its own make, model, and odometer. Calling car1.drive(150) changes only car1‘s odometer — car2 is completely unaffected, which demonstrates that each object owns a separate copy of the instance fields.
Example 2: Encapsulation and constructor overloading
public class Main {
public static void main(String[] args) {
BankAccount acc1 = new BankAccount("Alice", 500.0);
BankAccount acc2 = new BankAccount("Bob");
acc1.deposit(150.0);
acc1.withdraw(200.0);
acc2.deposit(50.0);
System.out.println(acc1);
System.out.println(acc2);
acc1.withdraw(10000.0);
}
}
class BankAccount {
private String owner;
private double balance;
BankAccount(String owner, double initialBalance) {
this.owner = owner;
this.balance = initialBalance;
}
BankAccount(String owner) {
this(owner, 0.0);
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
void withdraw(double amount) {
if (amount > balance) {
System.out.println("Insufficient funds for " + owner);
} else {
balance -= amount;
}
}
public String toString() {
return owner + "'s balance: $" + balance;
}
}
Output:
Alice's balance: $450.0
Bob's balance: $50.0
Insufficient funds for Alice
The balance field is private, so outside code cannot set it directly — it can only be changed through deposit() and withdraw(), which enforce rules like rejecting negative deposits and overdrafts. The second constructor uses this(owner, 0.0) to reuse the first constructor, avoiding duplicated initialization logic. Overriding toString() lets System.out.println(acc1) print a readable summary instead of a cryptic memory address.
Example 3: Working with an array of objects
public class Main {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("Maria", 92.5);
students[1] = new Student("James", 78.0);
students[2] = new Student("Wei", 88.25);
double total = 0;
for (Student s : students) {
s.printReport();
total += s.getGrade();
}
double average = total / students.length;
System.out.println("Class average: " + average);
}
}
class Student {
private String name;
private double grade;
Student(String name, double grade) {
this.name = name;
this.grade = grade;
}
double getGrade() {
return grade;
}
void printReport() {
String status = grade >= 60 ? "PASS" : "FAIL";
System.out.println(name + ": " + grade + " (" + status + ")");
}
}
Output:
Maria: 92.5 (PASS)
James: 78.0 (PASS)
Wei: 88.25 (PASS)
Class average: 86.25
This example shows objects stored in an array, just like any other type. Each Student object is independent, and the loop calls getGrade() on each one to accumulate a total, showing how objects are commonly grouped and processed in real programs.
Under the Hood: What Happens When You Create an Object
When the JVM executes a new expression, it performs roughly these steps:
- Class loading: if the class has not been used yet, the JVM’s class loader reads the compiled
.classfile, verifies its bytecode, and prepares anystaticfields (this happens only once per class, no matter how many objects you create). - Memory allocation: the JVM reserves a contiguous block of memory on the heap sized to hold all of the object’s instance fields, plus some internal bookkeeping (like a pointer to the object’s class metadata, used for things such as
instanceofchecks). - Default initialization: before your constructor runs, every field is set to its default value —
0for numeric types,falseforboolean, andnullfor object references. - Constructor execution: the matching constructor runs, typically overwriting the defaults with the values you passed in.
- Reference returned: the
newexpression evaluates to a reference to the freshly built object, which you usually store in a variable.
Objects live on the heap until the JVM’s garbage collector determines that nothing references them anymore, at which point their memory is reclaimed automatically — you never manually free memory in Java the way you would in C.
Common Mistakes
Mistake 1: Calling an instance method as if it were static
Instance methods belong to an object, not to the class itself. You cannot call one without first creating an object.
class Dog {
String name;
void bark() {
System.out.println(name + " says Woof!");
}
}
public class Main {
public static void main(String[] args) {
bark(); // ERROR: non-static method bark() cannot be referenced from a static context
}
}
The main method is static, meaning it runs without any object existing yet. Calling bark() directly fails to compile because the compiler has no object to run it on. The fix is to create a Dog object first, then call the method on that object:
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.name = "Rex";
myDog.bark();
}
}
class Dog {
String name;
void bark() {
System.out.println(name + " says Woof!");
}
}
Mistake 2: Using an object reference that was never assigned
Declaring a field of an object type does not create an object — it only creates a reference, which defaults to null until you assign it with new.
class Garage {
Car car; // just a reference, currently null - no Car object exists yet
void start() {
car.drive(10); // NullPointerException: car has never been assigned an object
}
}
Because car was declared but never set to a new Car(...), it still holds null. Calling a method on it throws a NullPointerException at runtime. The fix is to instantiate the field, either in a constructor or before it’s used:
class Garage {
Car car;
Garage() {
car = new Car("Ford", "Focus", 2020);
}
void start() {
car.drive(10);
}
}
Best Practices
- Keep fields
privateand expose behavior through methods, so the class can validate and control how its state changes. - Give constructors everything they need to leave the object in a valid, ready-to-use state — avoid objects that require several setup calls before they work correctly.
- Use
thisto disambiguate a field from a parameter of the same name inside a constructor or setter. - Name classes as singular nouns (
Car, notCars) since each instance represents one thing. - Override
toString()on classes you plan to print or log, so debugging output is meaningful instead of a memory address likeCar@1b6d3586. - Remember that assigning one object variable to another copies the reference, not the object — write an explicit copy method if you actually need a duplicate.
- Favor small, focused classes with a clear single responsibility over large classes that try to do everything.
Practice Exercises
- Exercise 1: Write a
Rectangleclass with privatewidthandheightfields, a constructor, and a methodarea()that returnswidth * height. Create two rectangles inmainand print both of their areas. - Exercise 2: Write a
Bookclass with fields fortitle,author, and aboolean checkedOut. Add methodscheckOut()andreturnBook()that updatecheckedOutand print a message; havecheckOut()print a warning instead if the book is already checked out. - Exercise 3: Create a
Temperatureclass that stores a value in Celsius and has a methodtoFahrenheit()returning the Fahrenheit equivalent (celsius * 9 / 5 + 32). Build an array of threeTemperatureobjects with different values and print each one’s Fahrenheit conversion.
Summary
- A class is a blueprint defining fields (data) and methods (behavior); an object is a concrete instance of that blueprint created with
new. - Objects live on the heap; variables hold references to them, so assigning one object variable to another copies the reference, not the data.
- Each object has its own independent copy of instance fields, but shares the same method code defined in the class.
- Constructors initialize new objects and can be overloaded to offer multiple ways to create an object.
- Encapsulation — keeping fields private and exposing controlled methods — protects an object’s internal consistency.
- Calling instance methods requires an object; uninitialized object references default to
nulland cause aNullPointerExceptionif used before assignment.
