DEV Community

Mayur Bodhare
Mayur Bodhare

Posted on

Why Does Java’s main Method Have static?

When I was learning Java, these topics felt like a random pile of keywords: private, this, static, "constructor", "getter", "setter". Every tutorial showed the syntax, but nobody told me how they fit together or why Java wants me to write code this way.

Here's what finally helped: they're all answers to two questions. Who is allowed to touch my data? and does this thing belong to each object, or to the class as a whole?

In this post, we'll go through encapsulation, getters and setters, this, constructors and static, one at a time. I'll also show what Java does behind the scenes, because that's where the "aha" moments live. At the end, we'll put everything into one small program.

Unless I show a full class, the snippets go inside a main method.

Encapsulation: put a wall around your data

Encapsulation means two things working together:

  1. Keep the data (fields) of a class private, so outside code can't touch it directly.
  2. Give the outside world public methods to work with that data in a safe way.

Think of an ATM. You can't walk into the bank's vault and grab cash. You use the ATM, which checks your card and your balance first. The vault is private, the ATM is the public method.

Here's the problem encapsulation solves:

class Person {
    public int age;
}

Person p = new Person();
p.age = -50;                 // nothing stops this
System.out.println(p.age);
Enter fullscreen mode Exit fullscreen mode

Output:

-50
Enter fullscreen mode Exit fullscreen mode

Nobody has a negative age, but Java happily allows it, because the field is open to everyone. Now the encapsulated version:

class Person {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age < 0 || age > 150) {
            System.out.println("Invalid age");
            return;
        }
        this.age = age;
    }
}
Enter fullscreen mode Exit fullscreen mode
Person p = new Person();
p.setAge(-50);
p.setAge(25);
System.out.println(p.getAge());
Enter fullscreen mode Exit fullscreen mode

Output:

Invalid age
25
Enter fullscreen mode Exit fullscreen mode

Now every change goes through one gate, and the rule lives in one place. If the rule changes later, you fix one method instead of hunting through the whole project.

Access modifiers: how the wall is built

Java gives you four levels of visibility:

  • private: only inside the same class.
  • (no keyword), called default or package-private: inside the same package.
  • protected: same package, plus subclasses.
  • public: everyone.

A good rule of thumb is to start with the tightest level and open up only when you need to. Fields are almost always private.

Behind the scenes

The wall is real. If you write p.age = 5 from another class when age is private, the compiler refuses to compile your code. The JVM also checks access when the code runs, as a second line of defense. (Special tools like reflection can sneak past this, but regular code can't.)

Getters and setters

A getter reads a private field. A setter changes it. They're ordinary methods that follow a naming habit:

  • Getter: getName(), getAge(). For a boolean, isActive().
  • Setter: setName(String name), setAge(int age).

You might ask, "If a getter just returns the field, why not make the field public?" There are good reasons:

  • Validation. A setter can reject bad values, like we saw with setAge.
  • Read-only fields. Want a value that can be read but never changed from outside? Write a getter and skip the setter.
  • Freedom to change the inside. Today you store age as a number. Next year you decide to store birthYear and calculate the age. If everyone uses getAge(), their code keeps working. If they used p.age directly, you'd break every one of them.
  • Tools expect them. Many libraries and frameworks find your data through the getX and setX names.

One honest note: don't generate a getter and setter for every field out of habit. A setter on a field that should never change is a hole in your wall. Add only what the outside world really needs.

(For classes that are just plain data containers, newer Java also has record, which writes the constructor and getters for you. That's a topic for another post.)

this: "the object I'm working on"

Inside a method or constructor, this is a reference to the current object, the one the method was called on.

Use 1: telling a field apart from a parameter

Look at this innocent-looking bug:

class Box {
    int size;

    Box(int size) {
        size = size;      // oops
    }
}
Enter fullscreen mode Exit fullscreen mode
Box b = new Box(5);
System.out.println(b.size);
Enter fullscreen mode Exit fullscreen mode

Output:

0
Enter fullscreen mode Exit fullscreen mode

Inside the constructor, the name size means the parameter, because it's the closest one. So size = size just assigns the parameter to itself, and the field is never touched. The fix is this.size = size;. Now it's clear: the left side is the field of this object, the right side is the parameter.

Use 2: method chaining

If a method returns this, you can call methods one after another:

class Counter {
    private int value;

    Counter add(int n) {
        value += n;
        return this;
    }

    int get() {
        return value;
    }
}
Enter fullscreen mode Exit fullscreen mode
System.out.println(new Counter().add(2).add(3).get());
Enter fullscreen mode Exit fullscreen mode

Output:

5
Enter fullscreen mode Exit fullscreen mode

Use 3: calling another constructor

this(...) calls a different constructor of the same class. We'll see it in the next section.

Behind the scenes

How does one getAge() method, stored once in memory, know which Person to use? The JVM secretly passes the object as a hidden first argument. You can picture p.getAge() as getAge(p). Inside the method, this is that hidden argument. That's all it is.

Constructors: how objects are born

A constructor is a special block that runs when you create an object with new. It has the same name as the class and no return type (not even void).

class Book {
    String title;
    int pages;

    Book(String title, int pages) {
        this.title = title;
        this.pages = pages;
    }
}

Book b = new Book("Java Basics", 300);
Enter fullscreen mode Exit fullscreen mode

The default constructor

If you write no constructor, the compiler quietly adds an empty one for you. That's why new Dog() works even when you never wrote a constructor.

But here's the trap. The moment you write any constructor, the compiler stops adding that free one:

class Pen {
    String color;

    Pen(String color) {
        this.color = color;
    }
}

Pen p = new Pen();     // compile error
Enter fullscreen mode Exit fullscreen mode

The error looks like this:

constructor Pen in class Pen cannot be applied to given types;
Enter fullscreen mode Exit fullscreen mode

If you want both versions, write both.

Overloading and chaining constructors

You can have several constructors with different parameters. To avoid repeating code, let one constructor call another with this(...):

class Book {
    String title;
    int pages;

    Book(String title, int pages) {
        this.title = title;
        this.pages = pages;
        System.out.println("two-arg");
    }

    Book(String title) {
        this(title, 100);          // reuse the other constructor
        System.out.println("one-arg");
    }
}
Enter fullscreen mode Exit fullscreen mode
Book b = new Book("Java Basics");
System.out.println(b.title + " " + b.pages);
Enter fullscreen mode Exit fullscreen mode

Output:

two-arg
one-arg
Java Basics 100
Enter fullscreen mode Exit fullscreen mode

See the order? The one-arg constructor calls the two-arg one first, so "two-arg" prints before "one-arg". The rule to remember: this(...) goes on the first line of the constructor. (Very recent Java versions loosen this a little, but keeping it on the first line is a safe habit.)

Behind the scenes: what new really does

  1. The JVM loads the class if it hasn't yet.
  2. It reserves memory for the object on the heap.
  3. It sets every field to its default (0, false or null).
  4. It runs the constructor. The very first thing any constructor does is call its parent's constructor, super(), unless you used this(...).
  5. It hands back a reference to the finished object.
  new Book("Java")
      |
      v
  [ allocate on heap ] -> [ fields = 0 / null ] -> [ constructor runs ] -> reference returned
Enter fullscreen mode Exit fullscreen mode

Constructors are not inherited. A child class must define its own, and it calls the parent's with super(...).

Small extra: you can make a constructor private. Then nobody outside the class can create objects directly. This is used for utility classes and for patterns like the singleton.

The static keyword

Now the second big question: does something belong to each object, or to the class itself?

  • Without static, a field or method belongs to each object. Every Student has their own name.
  • With static, it belongs to the class. There is one copy, shared by everyone, and you don't need an object to use it.

Static variable

class Visitor {
    static int total = 0;     // one shared copy
    String name;              // one copy per object

    Visitor(String name) {
        this.name = name;
        total++;
    }
}
Enter fullscreen mode Exit fullscreen mode
new Visitor("Asha");
new Visitor("Ravi");
new Visitor("Meera");

System.out.println(Visitor.total);
Enter fullscreen mode Exit fullscreen mode

Output:

3
Enter fullscreen mode Exit fullscreen mode

Each Visitor has its own name, but they all share one total. Notice we read it with the class name, Visitor.total, not through an object.

A very common use is a constant, written as static final:

static final double PI = 3.14159;
Enter fullscreen mode Exit fullscreen mode

By convention, constants use UPPER_CASE names.

Static method

A static method also belongs to the class. You call it with the class name:

class MathUtil {
    static int square(int x) {
        return x * x;
    }
}

System.out.println(MathUtil.square(6));
Enter fullscreen mode Exit fullscreen mode

Output:

36
Enter fullscreen mode Exit fullscreen mode

Static methods have one big limit: they have no this, because no object is involved. So they can't directly use instance fields or instance methods:

class Demo {
    int count = 5;

    static void show() {
        System.out.println(count);    // compile error
    }
}
Enter fullscreen mode Exit fullscreen mode
non-static variable count cannot be referenced from a static context
Enter fullscreen mode Exit fullscreen mode

Java is asking: "Whose count? Which object?" There's no object, so it can't answer.

This also explains something you've written many times without thinking:

public static void main(String[] args)
Enter fullscreen mode Exit fullscreen mode

main is static because the JVM needs to call it before any object exists. If it were an instance method, you'd need an object to start the program, but you need the program to create the object. A chicken-and-egg problem. Static solves it.

One more fact: static methods are chosen by the compiler, based on the variable's type. They don't take part in overriding the way instance methods do. If a child class declares a static method with the same signature as the parent's, it hides the parent's version instead of overriding it.

Static block

A static block is a chunk of code that runs once, when the class is first loaded and initialized. It's useful when a static field needs more than a simple one-line value.

class Config {
    static String env;

    static {
        System.out.println("Static block runs");
        env = "PROD";
    }

    Config() {
        System.out.println("Constructor runs");
    }
}

public class Demo {
    public static void main(String[] args) {
        System.out.println("main starts");
        new Config();
        new Config();
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

main starts
Static block runs
Constructor runs
Constructor runs
Enter fullscreen mode Exit fullscreen mode

The static block ran once, right when Config was first used, and before its first constructor. The constructor ran twice, once for each object. If a class has several static blocks and static variables, they run in the order you wrote them.

Behind the scenes: where static things live

Instance fields live inside each object on the heap. Static fields live once, with the class. That's why there's only one copy.

  CLASS (loaded once)                HEAP (one block per object)
 +----------------------+          +--------------------+
 | Student              |          | a: id=1            |
 |   count = 2 (static) |          |    name = "Asha"   |
 |   methods            |          +--------------------+
 +----------------------+          | b: id=2            |
                                   |    name = "Ravi"   |
                                   +--------------------+
Enter fullscreen mode Exit fullscreen mode

The class is loaded and initialized by the JVM before it's first used, and that's the moment static variables get their values and static blocks run. This happens only once per class.

Putting it all together

Here's one full program that uses everything: private fields, getters and setters, this, two constructors with chaining, a static variable, static methods and a static block.

public class Student {
    private static int count;
    private static final String SCHOOL;

    static {
        SCHOOL = "Green Valley High";
        System.out.println("Student class loaded");
    }

    private final int id;
    private String name;
    private int marks;

    public Student(String name, int marks) {
        this.id = ++count;
        this.name = name;
        setMarks(marks);
    }

    public Student(String name) {
        this(name, 0);
    }

    public int getId()      { return id; }
    public String getName() { return name; }
    public int getMarks()   { return marks; }

    public void setMarks(int marks) {
        if (marks < 0 || marks > 100) {
            System.out.println("Invalid marks: " + marks);
            return;
        }
        this.marks = marks;
    }

    public static int getCount()      { return count; }
    public static String getSchool()  { return SCHOOL; }

    public static void main(String[] args) {
        System.out.println("main starts");

        Student a = new Student("Asha", 88);
        Student b = new Student("Ravi");
        b.setMarks(150);
        b.setMarks(72);

        System.out.println(a.getId() + " " + a.getName() + " " + a.getMarks());
        System.out.println(b.getId() + " " + b.getName() + " " + b.getMarks());
        System.out.println("Total: " + Student.getCount() + " at " + Student.getSchool());
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Student class loaded
main starts
Invalid marks: 150
1 Asha 88
2 Ravi 72
Total: 2 at Green Valley High
Enter fullscreen mode Exit fullscreen mode

Two things are worth a close look:

  • "Student class loaded" prints before "main starts". The main method lives inside Student, so the JVM must load and initialize the class first, which runs the static block. Only then can it call main.
  • id is final. It's set once in the constructor and never changes, and there's no setter for it. That's encapsulation protecting a value that should be permanent.

Which one should I use?

  • Static or instance? If the data or behavior is different for each object (a student's name), make it instance. If it's shared by all, or it's a pure helper that needs no object data (like square), make it static.
  • Constructor or setter? Put values that an object must have to be valid into the constructor. Use setters for values that can change later.
  • Chaining or copy-paste? If two constructors share the same setup, chain them with this(...). One place to fix, fewer bugs.
  • Getter and setter, or public field? Use a private field with methods, almost always. Open fields only for simple constants like static final.
  • Static block or inline value? If a static field needs one simple value, write it inline: static int max = 10;. Use a static block when it needs several steps, a loop or error handling.

Common mistakes

  1. Writing size = size; in a constructor. Without this., you assign the parameter to itself and the field stays at its default. Always write this.size = size;.
  2. Forgetting the default constructor disappears. Once you write any constructor, new Pen() stops working unless you add a no-argument one yourself.
  3. Using instance members from a static method. You'll get "non-static variable cannot be referenced from a static context." There's no object there, so create one, or make the member static if it truly belongs to the class.
  4. Making everything static. It feels easy, but shared mutable data is a source of confusing bugs, especially when many threads are involved. Use static only for things that really belong to the whole class.
  5. Returning a private mutable object from a getter. If getScores() returns your internal array or list directly, outside code can change it and break your wall. Return a copy instead.
  6. Adding a getter and setter to every field by habit. A setter on a value that should never change defeats the purpose. Expose only what's needed, and add validation where it matters.

Quick recap

  • Encapsulation: private fields, public methods, one gate where the rules live.
  • Getters and setters: normal methods that let you validate, keep fields read-only and change the inside later without breaking callers.
  • this: the current object. It separates fields from parameters, enables chaining and calls other constructors with this(...).
  • Constructors: run on new, have no return type, the free default one vanishes when you write your own, and this(...) chains them.
  • static: belongs to the class, not to objects. One shared copy of a static variable, no this in a static method.
  • Static block: runs once, when the class is first loaded, before any object is created.

Try it yourself

Here's a small practice idea: build a BankAccount class. Make balance private and set it only through deposit and withdraw methods that reject bad amounts. Add a static counter that gives each account a unique number, a static constant for the minimum balance, and a static block that prints a message when the class loads. Then add a second constructor that chains to the first with this(...).

For a bonus experiment, try calling an instance method from a static one and read the compiler's error message. It's the quickest way to remember the rule.

If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.

Top comments (0)