Let us start by completing our journey into Object-Oriented Programming. In the previous tutorials we learned how to define a class, create objects, and write constructors. Now, what if we want to pass an object as an argument to a method? It is surprisingly simple — and very powerful.
Think of it this way: just as you can pass an int or a double into a method, you can pass an entire object. The method then works with that object using a reference.
- You can write utility methods that work on any object of a given class.
- You can compare two objects inside a method (e.g.,
compareAreas(Circle c1, Circle c2)). - Methods can modify the state of the passed object — giving you tremendous flexibility.
2.1 — What Does "Instance" Mean?
Let us start with something you already know. You have written code like this before:
You probably said: "I have created an object of Circle." That is correct. But here is the important part — you can also say it differently:
"I have created an instance of Circle."
Both sentences mean exactly the same thing. The word instance just emphasizes that this particular object was born from the Circle class. And we can create multiple instances from the same class — each one is independent, each one has its own data.
new keyword, you create a new instance. Each instance lives in its own region of memory (the Heap).
2.2 — Instance Variables & Instance Methods
Look at our Circle class. It has radius and color. Now imagine we create two circles:
radius and color are instance variables because each instance (c1, c2) has its own private copy of these values. Changing c1's radius does not affect c2's radius.
getArea() is an instance method because it belongs to a specific instance — it works on that instance's data (this.radius). You must call it on an object: c1.getArea().
2.3 — Static Variables: Shared by All Instances
Now here is the big question: what if we want a variable that is not owned by any single instance, but instead is shared by all instances? For example, suppose we want to count how many Circle objects have been created so far. Where do we put such a counter?
If we make counter a regular (instance) variable, each circle would have its own counter — and they would always be 1. That is useless. We need a variable that all circles look at together.
That is exactly what the keyword static does.
We have now created three objects of type Circle. Each object has its own radius and color — those are instance variables, private to each object. But the counter is a static variable: it does not belong to any single object. Every time a new Circle is created, all three objects see the same counter value.
The following visualization shows exactly how this looks in memory — how c1, c2, and c3 each hold a reference to the same static counter.
new Circle(...)new Circle(...)new Circle(...)- Instance variable/method — belongs to a specific object. Each object has its own copy. Access via:
objectName.field - Static variable/method — belongs to the class itself. Shared by all objects. Access via:
ClassName.field(e.g.,Circle.getCounter())
A student might raise their hand and ask: "Professor, I understand what static is — but why do we need it? Can we not do everything with regular instance variables?"
Excellent question. Let us look at two real, compelling scenarios that make static absolutely necessary.
Scenario 1 — Unique Random Student IDs (Why We Need a Static Variable)
Student class with fields: studentID, name, and age.The requirement is: every student must automatically receive a random unique ID chosen from the range 1 to 2000. The IDs are not sequential — they are random. Ali might get 5, Hana might get 100, Omar might get 800. But no two students can ever share the same ID.
Now think carefully — how do we guarantee uniqueness? Each instance is completely independent. When Ali's object is being created, it has no way of knowing which IDs Hana's or Omar's objects have already taken. This is the real challenge.
Step 1 — A Student's First Idea (and Why It Fails)
A student raises their hand and says: "Professor, that is okay — the Student class can generate the random ID by itself inside the constructor. But I will maintain a tracking array in main. After I create a Student, I check its ID — if it is already taken, I discard that object and create a new one. I keep doing this until I get a unique ID."
The professor smiles. "Good thinking — but let us look at what that actually looks like in code."
Student class. He will scream. Every time he creates a student, he is forced to maintain a list, write a loop, and manage the ID checking himself. That is not his job — that is the class's job. The abstraction is broken.
The professor says: "You found the right idea — a tracking array. But it belongs inside the class, not in
main. Make it static, and the class manages everything by itself."
Step 2 — The Real Solution: a Static Array Inside the Class
Here is the insight: we need a single pool of used IDs that every Student instance can consult and update. That pool must be shared — not duplicated per object.
We declare a static boolean[] array inside the Student class. Because it is static, there is exactly one copy no matter how many Student objects exist. Every instance looks at the same array.
The algorithm inside the constructor is:
usedIDs array. Is usedIDs[randomNumber] already true?usedIDs[randomNumber] = true and assign this number as the student's ID.usedIDsis static — there is exactly one array, shared by every Student instance ever created.- When s1 marks
usedIDs[5] = true, that change is immediately visible to s2 and s3. They will never pick 5 again. - The caller just writes
new Student("Ali", 20). The ID complexity is completely hidden inside the class. This is encapsulation and abstraction working together. generateUniqueID()is also static because it only touches static fields — it does not need any instance data.
Scenario 2 — Utility Operations (Why We Need a Static Method)
Now let us think about static methods. When should a method be static?
A method should be static when it does not need to access any instance variable. It performs a general-purpose operation that does not depend on the state of any specific object.
Math.sqrt(), Math.pow(), and Math.abs(). You have been calling these all semester. Notice that you never wrote new Math() first. Why? Because they are static methods — they live on the class itself, not on any object.Another real example: suppose we write a
Circle class and we want a method that, given two radius values, tells us which one would produce the bigger area. This method needs no particular instance — it just does a calculation. Making it static is the natural and correct choice.
- The method does not use any instance variable (no
this.something). - The method performs a general utility operation (math, conversion, factory logic).
- You want callers to use it without creating an object first.
this does not exist inside a static context.
4.1 — The Big Picture: Project → Package → Class
A Java program is organized in layers. Think of it like a building:
4.2 — Visibility Modifiers
Now, here is the critical question: if my project has many classes across many packages, who is allowed to see and use which fields and methods? This is controlled by visibility (access) modifiers.
There are four modifiers in Java:
public int radius; — anyone can read it.
private int radius; — only Circle's own methods can read it.
int radius; — no keyword means default.
protected int radius;
| Modifier | Same Class | Same Package | Subclass (other package) | Everywhere |
|---|---|---|---|---|
| public | ✔ | ✔ | ✔ | ✔ |
| protected | ✔ | ✔ | ✔ | ✗ |
| default | ✔ | ✔ | ✗ | ✗ |
| private | ✔ | ✗ | ✗ | ✗ |
getArea() (public)✔ Can call
helper() (default — same package)✗ Cannot access
radius (private)
getArea() (public)✗ Cannot call
helper() (default — different package)✗ Cannot access
radius (private)
protected, you need to understand Inheritance (parent and child classes). We will come back to it in the Inheritance tutorial. For now, just know it exists and it is between default and public in terms of openness.
5.1 — Why Should Data Fields Be Private?
We have been writing private in front of data fields. But why? What is wrong with public fields?
public double radius;Now any code anywhere in the project can write:
myCircle.radius = -999;A negative radius makes no sense. But Java would allow it because there is no protection. Your object is now in an invalid, broken state — and it is impossible to enforce rules from inside the class.
5.2 — Accessors (Getters) and Mutators (Setters)
When you make a field private, you provide controlled access through two types of methods that you will hear about constantly in professional Java development:
Naming convention:
get + FieldNameReturns: the field's value — no modification.
Example:
getRadius(), getColor(), getName()
Naming convention:
set + FieldNameReturns:
void. Takes one parameter.Example:
setRadius(double r), setColor(String c)
- Data integrity: Invalid values are rejected before they corrupt the object.
- Easy maintenance: Change the internal storage format any time — external code using getters/setters is not affected.
- Debugging made easy: Every read/write goes through one place — you can add logging there instantly.
So far we have stored single objects in variables like Student s1, Student s2, Student s3. This is fine for two or three objects. But what if we have 100 students? Writing 100 separate variables is impossible.
The answer is an array of objects — and it works very similarly to an array of primitives, with one important difference you must understand deeply.
6.1 — Declaring and Creating an Array of Objects
This is one of the most important points to understand. When you create new Student[5], Java creates an array of 5 reference slots — not 5 Student objects. The objects themselves live separately in the Heap memory.
The array holds addresses (references) that point to where the objects are. The objects can be scattered anywhere in the Heap.
6.2 — Iterating Over an Array of Objects
new Student[5], all slots are null by default. If you try to call a method on a null slot, you get a NullPointerException. If you do not fill all slots, use a null check:
6.3 — Complete Working Example
Student[1] Ali — Age: 20
Student[2] Hana — Age: 21
Student[3] Omar — Age: 19
Student[4] Lina — Age: 22
Student[5] Reem — Age: 20
Oldest student: Lina
usedIDs[] array that guarantees unique random IDs across all instances) and for utility methods that need no instance data.