February 22, 2024

lealceldeiro

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.

What’s the 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.

— Oxford English Dictionary

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.

Use cases for the static keyword

Static classes and interfaces

As 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 static as a class or interface modifier if the class or interface is not a member of another class or interface.

For example, in the previous code snippet, we can’t declare the class A as static or it causes a compilation error.

Static fields

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 Payment to access the static field, Java ignores the object and accesses the static field through the class, that’s it, the instance of the class is only used to know what’s the type that declares the field.

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:

class Order {
    static int basicPackage = 1;
}

public class Main {
    public static void main(String[] args) {
        Order order = new Order();
        System.out.println(order.basicPackage); // prints 1

        order = null;
        System.out.println(order.basicPackage); // prints 1 too
    }
}

I hope you came to the conclusion it prints 1 twice because the object order is only used in this context by Java to know that this object is of type Order and get the value of the static field from the type itself, instead of getting it from the instance.

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

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

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):

  1. It’s a compile-time error if a static initializer can’t complete normally.

  2. It’s a compile-time error if a return statement appears anywhere within a static initializer.

  3. For more complex use cases, there are also some exhaustive definitions that you should be aware of.

Static imports

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);
    }
}

Additional considerations

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.

Final thoughts

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.