A static member belongs to the class rather than to any object, which makes it a shared space rather than per-item space. That is exactly what you want for a running counter, a fixed limit or a factory method, and exactly what you do not want when each item needs its own copy.
What does static actually change?
Put static on a field and there is one copy of it, shared by every object of that class, living for as long as the class is loaded. Put static on a method and the method has no receiver at all: it cannot touch instance fields, and it never sees the object you called it on. Static members are reached through the class name rather than through an instance, which is the syntax that tells you, at a glance, that no particular object is involved.
Two consequences follow from that. A static field is not part of the object's state, so a class with static counters has no per-object copy of the counter to reason about. And a static method cannot be overridden later in an inheritance chain, because overriding needs a receiver to dispatch on.
When does a shared counter make sense?
When the thing you are counting belongs to the system rather than to any one item. Ticket numbers, order numbers and generated identifiers are the classic case: the sequence continues across every object, and the sequence itself is the thing worth protecting.
public class Demo {
static class Ticket {
private static int lastNumber = 1000;
private final int number;
Ticket() {
number = ++lastNumber;
}
int number() {
return number;
}
}
public static void main(String[] args) {
Ticket a = new Ticket();
Ticket b = new Ticket();
Ticket c = new Ticket();
System.out.println(a.number() + " " + b.number() + " " + c.number());
System.out.println("counter now at: " + Ticket.lastNumber);
}
}Three tickets, three numbers, one counter. The increment happens in the constructor, which means every ticket gets a number the moment it exists, and the counter cannot be forgotten by a caller because the caller never touches it. Note the reading of the counter at the end: Ticket.lastNumber goes through the class name, which is the clearest signal that this value is not about any single ticket.
Now imagine the same counter as an instance field. Every ticket would start at 1000 and every ticket would be 1001, and the bug would only appear once somebody needed the numbers to be distinct. Shared state is worth it exactly when sharing is the point.
How do static constants and factory methods help?
A static final field is a constant: one value for the whole class, fixed at compile time or during class preparation. Naming it in upper case is the convention, and it saves every caller from inventing their own magic number.
public class Demo {
static class Ticket {
static final int MAX_PER_ORDER = 8;
private static int sold;
static void resetSoldCount() {
sold = 0;
}
static int soldCount() {
return sold;
}
void sell() {
sold++;
}
}
public static void main(String[] args) {
System.out.println("limit: " + Ticket.MAX_PER_ORDER);
new Ticket().sell();
new Ticket().sell();
System.out.println("sold: " + Ticket.soldCount());
Ticket.resetSoldCount();
System.out.println("sold after reset: " + Ticket.soldCount());
}
}The output shows the limit of 8, a sold count of 2 after two tickets, and zero after the reset. Note how the two halves of the class talk to each other: sell is an instance method that updates the shared counter, while soldCount and resetSoldCount are static methods that report on or reset it. A static method may freely call static methods and touch static fields, and it may create and use objects, which is what makes it useful as a factory.
What is a static initializer block for?
Sometimes a constant is not a simple literal. A static initializer block runs once, when the class is first used, and it can compute whatever it needs to. It is the right tool for a lookup table or a configuration value that needs several steps, and the wrong tool for anything that depends on the order of program events.
public class Demo {
static class Registry {
static final int CAPACITY;
static {
CAPACITY = 64;
System.out.println("registry ready, capacity " + CAPACITY);
}
}
public static void main(String[] args) {
System.out.println("main starts");
System.out.println("capacity: " + Registry.CAPACITY);
}
}The output order is the interesting part. main starts prints first, then the registry line, then the capacity. The static block did not run when the class was loaded, it ran the first time something actually touched Registry.CAPACITY. Class initialisation is lazy, which is why static state can produce surprises when you mix it with ordering assumptions.
Where do you already use static every day?
More often than you think. The methods on Math and Integer, the formatting calls on String, and the entry point of every program are all static, and you call them without thinking about it.
public class Demo {
public static void main(String[] args) {
System.out.println(Math.max(3, 9));
System.out.println(Math.round(2.6));
System.out.println(Integer.parseInt("2026"));
System.out.println(String.valueOf(42) + "!");
System.out.println(Boolean.parseBoolean("TRUE"));
}
}These print 9, 3, 2026, 42! and true. Two details are worth stealing for your own classes: Math.round returns a whole number rather than a fractional one, and Boolean.parseBoolean treats anything that is not "true" (ignoring case) as false, so it never throws on odd input. A static utility method that behaves predictably on bad input is a very welcome thing to find in a standard library.
What goes wrong when static creeps into everything?
- Static state is shared by every object, so a test that writes to it can leak into the next test.
- Static methods cannot see instance fields, so reaching for one usually means the logic wanted an instance all along.
- Static methods are hidden rather than overridden in subclasses, so a call like
Base.helper()never reaches an override. - A static field holds one value for the whole program, which is a problem the moment two parts of a program need different values.
- Static initialisation order is lazy and easy to misjudge, so side effects inside static blocks bite later.
Try it yourself: write a SerialNumberIssuer class with a private static counter starting at 1000, a public constructor that takes the counter's next value and stores it in a final field, a static method issued() that returns how many numbers have been handed out, and a static final constant PREFIX holding the text "SN-". Create four instances and print each one's number with the prefix, then print the issued count. Next, move the counter into an instance field and run it again: every object now gets the same number. That contrast is the fastest way to make the difference between shared and per-object state stick.