class A {
class B {
}
}
In the world of Java programming, the static
keyword plays a significant role,
often thought of as a fundamental concept to grasp.
However, its nuances can sometimes be elusive, especially for those new to the language.
In this article we’ll go over this construct and some of the details to have in mind when using it in our code.
static
keyword in Java?static
[Computing] (of a process or variable) not able to be changed during a set period, for example, while a program is running.
That’s how the Oxford English Dictionary defines static as a word in the field of computing. Not able to change; that’s the most important part.
Ok, we didn’t have to check the dictionary to know that: when something is static, it doesn’t change. For example, a lighthouse is static in the sense that it doesn’t move from one place to another, like animals do. Although the lantern(s) and related components move, the tower structure doesn’t change its position on the soil.
How much does this meaning relate to the static
keyword in Java?
Very much!
The static
keyword in Java can be used to modify the definition of
classes,
fields,
methods,
initializers, and more.
It’s always used to reflect a sense of not changing its [the component’s] behavior depending on other factors,
such as an object state.
Let’s see some use cases along with some meaningful examples.
static
keywordAs we saw previously, we can declare inner classes and interfaces, that’s it, classes and interfaces as members of other classes and interfaces. For example:
class A {
class B {
}
}
In the previous code, B
is an inner class.
It’s a member of A
(see tutorial for a full example).
This is a code smell, because to create an instance of B
, we must create first an instance of A
:
var a = new A();
var b = a.new B();
or the shorter version:
var b = new A().new B();
That code is almost never correct or justified. Just to mention some of its many flaws, we can say:
To create an instance of B
, we must allocate first memory to create an instance of A
.
If a declaration of some type (i.e.: a member variable) in a particular scope (i.e.: the inner class) has the same name as another declaration in the enclosing scope, then the declaration shadows the declaration of the enclosing scope.
In such cases, the correct approach is to declare a static nested class or interface (§8.1.1.4, §9.1.1.3):
class A {
static class B {
}
}
Now B
no longer "lives within" A
, and we can simply use it like this:
var b = new A.B();
Notice we can’t use For example, in the previous code snippet, we can’t declare the class |
In a previous article, we saw how to declare fields.
To make them static we just need to add the static
modifier.
Let’s see an example:
class Payment {
static int MAX = 1000;
}
In the previous snippet, MAX
is a static field.
This means it’s not related to any specific instance of Payment
.
Instead, it is associated with the class where it’s declared.
For example, assuming Payment
is located in the same package the following Main
class is located,
then the MAX
field could be used like this:
public class Main {
public static void main(String[] args) {
System.out.println(Payment.MAX);
}
}
Notice here, how we didn’t have to instantiate
Payment
to access the static field.
If we use an object of type A long time ago, I answered a question on StackOverflow where the original poster asked about this, and, despite I learned about it at the time, 6 years later it came back to bite me in a job interview. When I was asked what the following code would print/do, I hesitated to answer correctly and without any doubts:
I hope you came to the conclusion it prints |
The declaration of a static field (also known as class variable) introduces a static context,
which limits the use of constructs that refer to the current object
(§8.3.1.1).
For example, a static field can’t be accessed from an instance method, or the this
or super
keywords.
This type of construct is useful when declaring constants or variables shared across all instances of a class. That’s because the values of such fields are not associated to a specific instance of a class but rather to the class itself — hence they’re also called class variables.
Because of this behavior, any change made to a static field is reflected everywhere it’s used, regardless whether it’s being accessed from one or another instance of the class that declares it, or even by other classes.
The most common scenario where static fields are used is when combined with the final keyword to create inmutable constants that we can use throughout our code. For example:
public class Order {
public static final int BASIC_PACKAGE = 1;
}
Static methods, just like static fields, are regular
methods to which the static
modifier
have been applied to.
Let’s build on top of our previous example:
class Payment {
static int MAX = 1000;
static boolean isValidTransactionAmount(int amount) {
return amount > 0 && amount <= MAX;
}
}
Here we’ve declared a method isValidTransactionAmount
which is static,
that’s it, we don’t need an object of type Payment
to use it.
For example, this is how we’d call it:
public class Main {
public static void main(String[] args) {
System.out.println(Payment.isValidTransactionAmount(20)); // prints true
}
}
Again, static methods involve a static context
from where we don’t have access to constructs that refer to the current object,
such as instance fields, the this
keyword, or the super
keyword.
That’s only expected.
Static fields and methods are constructed when the class is initialized, not when class instances are constructed, so there’s no way to access those instance constructs.
Static initializers are almost the same as instance initializers from a syntax perspective.
For example, building on top of the previous example, we could initialize the MAX
variable like this:
class Payment {
static int MAX;
static {
MAX = 1000;
}
}
This construct is useful when the initialization of a static variable is not simple enough to fit in one line. In this example, it’s not worth it, but there are real-world scenarios where there’s some complex logic we want to execute when the class is constructed.
However, this construct should be used carefully because sometimes it’s not easy to reason about the logic being implemented. That’s because the field declaration and the actual initialization are separated.
The same rules about accessing constructs that refer to the current object apply here.
Additionally, there are a few notes we should remember about static initializers (§8.7):
It’s a compile-time error if a static initializer can’t complete normally.
It’s a compile-time error if a return
statement appears anywhere within a static initializer.
For more complex use cases, there are also some exhaustive definitions that you should be aware of.
Static imports (§7.5.3) are used to import static-accessible members from other types, such as classes, into a given type, for example, another class.
It works by specifying the static
keyword after the import
keyword.
For example, let’s suppose we have the following Payment
class, located in a package named com.payment
.
package com.payment;
public class Payment {
public static int MAX = 1000;
}
If we have another class called Service
in a package called com.services
and we want to import statically,
the MAX
static field from Payment
, we could do so like this:
package com.services;
import static com.payment.Payment.MAX;
class Service {
static {
System.out.println(MAX);
}
}
It also works for the wildcard (*
) import. This one would import all static members of Payment
.
import static com.payment.Payment.*;
The alternative to this import is the regular import:
package com.services;
import com.payment.Payment;
class Service {
static {
System.out.println(Payment.MAX);
}
}
While static fields and methods allow us to share common attributed and behaviors defined in a class, if not used properly they could cause concurrency issues in multithreaded environments.
For that reason we must use them judiciously and take the appropriate measure to make our data structure thread-safe to avoid data corruption, race conditions, deadlocks and many of the other well-known concurrency challenges we face in software programming.
Static constructs offer us the flexibility to go beyond simple object-oriented programming (OOP) designs. With it, we can implement our solutions following a different approach in many cases, including some functional-programming style.
From declaring simple constant fields to building pure functions,
the static
keyword brings to the Java world the necessary power to be more than just an OOP language.
This may seems superfluous, but given the wide adoption Java has had, it’s important to keep the language and
platform flexible enough to meet different use cases.
Hopefully, the insights and links shared here are a good reference and a starting point for you to dig deeper
into the intricacies and use cases of the static
keyword and its constructs, to bring their benefits to your
daily coding job; and, as always, be aware of possible pitfalls while using it.