When fields are wide open
The Student class from the last lesson has a hole in it:
Student s = new Student("Maya", 88);
s.score = -999;
s.score = 10000;
System.out.println(s.score); // 10000
No error. No warning. The score is now 10000, and your program will happily compute averages, assign grades, and print reports based on it.
The worse part comes later. When someone notices the bad data, the question is "which line did this?" — and the answer is any line in the entire program, because score is public and every piece of code can reach it. There's no place to set a breakpoint. There's nothing to search for.
Encapsulation: one door, with a guard on it
public class Student {
private String name;
private double score;
public Student(String name, double score) {
this.name = name;
setScore(score);
}
public double getScore() {
return score;
}
public void setScore(double score) {
if (score < 0 || score > 100) {
throw new IllegalArgumentException("Score must be 0-100, got: " + score);
}
this.score = score;
}
public String getName() {
return name;
}
}
Now:
Student s = new Student("Maya", 88);
s.score = -999; // will not compile
The compiler says score has private access in Student. The bad assignment is impossible to write, not merely discouraged.
s.setScore(-999);
This compiles, runs, and throws IllegalArgumentException: Score must be 0-100, got: -999.0. The program stops at the exact line that tried to do something wrong, with a message naming the value. Compare that to discovering a corrupt score three screens of output later.
Two details in that class are worth pointing out:
The constructor calls setScore(score) rather than this.score = score. That means the validation runs at construction time too. Write it the other way and new Student("Maya", -999) would sail right past the guard you just built. Validation logic should exist in exactly one place, and everything else should go through it.
name has a getter but no setter. That's a deliberate design decision expressed in code: a student's name is read-only. There's no "please don't change this" comment to ignore — there is simply no way to change it.
That's the whole point of encapsulation: collapse "who can change this, and how" down to one method you can actually read. When the data goes bad, you have one place to look.
The four access modifiers
| Modifier | Who can reach it | When to use it |
|---|---|---|
private | This class only | Default choice for every field |
| (nothing) | Same package | Rare |
protected | Same package plus subclasses | Deliberately offered to subclasses |
public | Everyone | Methods you mean as your public interface |
Leaving the modifier off gives you package-private, which is a real thing and not the same as public. It mostly shows up by accident, when someone forgot to type a modifier.
The practical rule is short: fields are private; only methods you intend as your interface are public. Locking the door and opening it deliberately is much easier than leaving it open and trying to reconstruct later who walked in.
Naming getters and setters
public String getName() // get + FieldName
public void setName(String name) // set + FieldName
public boolean isPassing() // booleans use is
This isn't a style preference. It's a convention that tools depend on. Spring, Jackson, Hibernate and a long list of other frameworks find your data by looking for methods named exactly this way. Name a getter fetchName() and JSON serialization will report your object as empty, with no error to explain why.
Every IDE generates these with a keyboard shortcut. Use it. Hand-typing twenty getters is how typos get in.
static: belongs to the class, not to any object
public class Student {
private static int count = 0;
private String name;
public Student(String name) {
this.name = name;
count++;
}
public static int getCount() {
return count;
}
}
new Student("Maya");
new Student("Sam");
System.out.println(Student.getCount());
Prints 2.
Look at how that last line is written: Student.getCount(), using the class name, with no object anywhere. That's what static means.
Every object gets its own name. There is exactly one count, shared by the whole class — it exists even before any Student is created, and it survives after they're all gone. A non-static field is "one per object." A static field is "one, total."
Use static for things that genuinely belong to the concept rather than to any individual: a counter, a shared configuration value, a utility method like Math.max that doesn't need any object to do its job.
Which finally explains main
public static void main(String[] args)
That static from lesson 1 now has a real answer.
A normal method has to be called on an object: s.describe(). At the instant your program starts, there are no objects. Not one line of your code has run. There is nothing for the JVM to call a method on.
static means the method belongs to the class itself, so the JVM can invoke Main.main(...) without constructing anything first. That's not a historical accident — it's the only thing that could work. Something has to run first, and that something cannot require a "first" to already exist.
The rule that follows from this
A static method cannot use non-static fields directly.
public class Student {
private String name;
public static void printName() {
System.out.println(name); // won't compile
}
}
You get non-static variable name cannot be referenced from a static context.
This error confuses beginners, and the reason is simpler than it sounds. printName() belongs to the class. There may be zero Students in existence, or fifty. When you write name, whose name do you mean? The question has no answer, so the compiler refuses the question.
This is also why main can't touch your instance fields directly. It has to create an object first — which is exactly what the project in the next lesson does on its very first line.
Constants: static final together
public static final int MAX_SCORE = 100;
static means one shared copy. final means it can't be reassigned. Together they're the standard way to declare a constant in Java, and you'll see this exact line shape everywhere.
Referred to as Student.MAX_SCORE from outside, since it belongs to the class.
Ten-second check
Why does main have to be static?
Because when the program starts, no objects exist yet. A non-static method can only be called on an object, and there isn't one — so the entry point has to be callable on the class itself. static is what makes that possible.