Non-Access Modifiers in Java

Introduction

Modifiers are keywords that let us fine-tune access to our class and its members, their scope, and behavior in certain situations. For example, we can control which classes/objects can access certain members of our class, whether a class can be inherited or not, whether we can override a method later, whether we should override a method later, etc.

Modifier keywords are written before the variable/method/class (return) type and name, e.g. private int myVar or public String toString().

Modifiers in Java fall into one of two groups - access and non-access:

native is not covered in more detail below since it is a simple keyword that marks a method that will be implemented in other languages, not in Java. It works together with the Java Native Interface (JNI). It's used when we want to write performance critical sections of code in more performance-friendly languages (like C).

Want to learn more about access modifiers, as opposed to non-access? If so, check out our article Access Modifiers in Java.

Non-Access Modifiers

These types of modifiers are used to control a variety of things, such as inheritance capabilities, whether all objects of our class share the same member value or have their own values of those members, whether a method can be overridden in a subclass, etc.

A brief overview of these modifiers can be found in the following table:

Modifier Name Overview
static The member belongs to the class, not to objects of that class.
final Variable values can't be changed once assigned, methods can't be overridden, classes can't be inherited.
abstract If applied to a method - has to be implemented in a subclass, if applied to a class - contains abstract methods
synchronized Controls thread access to a block/method.
volatile The variable value is always read from the main memory, not from a specific thread's memory.
transient The member is skipped when serializing an object.

The static Modifier

The static modifier makes a class member independent of any object of that class. There are a few features to keep in mind here:

  • Variables declared static are shared among all objects of a class (since the variable essentially belongs to the class itself in this case), i.e. objects don't have their own values for that variable, instead, they all share a single one.
  • Variables and methods declared static can be accessed via the class name (instead of the usual object reference, e.g. MyClass.staticMethod() or MyClass.staticVariable), and they can be accessed without the class being instantiated.
  • static methods can only use static variables and call other static methods, and cannot refer to this or super in any way (an object instance might not even exist when we call a static method, so this wouldn't make sense).

Note: It's very important to note that static variables and methods can't access non-static (instance) variables and methods. On the other hand, non-static variables and methods can access static variables and methods.

This is logical, as static members exist even without an object of that class, whereas instance members exist only after a class has been instantiated.

Static Variables

For variables, we use static if we want the variable to be common/shared for all objects.

Let's take a look at how static variables behave differently from regular instance variables:

class StaticExample {
    public static int staticInt = 0;
    public int normalInt = 0;
    
    // We'll use this example to show how we can keep track of how many objects
    // of our class were created, by changing the shared staticInt variable
    public StaticExample() {
        staticInt++;
        normalInt++;
    }
}
// No instances of StaticExample have been created yet
System.out.println(StaticExample.staticInt); // Prints: 0
// System.out.println(StaticExample.normalInt); // this won't work, obviously

// Let's create two instances of StaticExample
StaticExample object1 = new StaticExample();
// We can refer to static variables via an object reference as well, 
// however this is not common practice, we usually access them via class name
// to make it obvious that a variable/method is static
System.out.println(object1.staticInt); // Prints: 1
System.out.println(object1.normalInt); // Prints: 1

StaticExample object2 = new StaticExample();
System.out.println(object2.staticInt); // Prints: 2
System.out.println(object2.normalInt); // Prints: 1

// We can see that increasing object2's staticInt 
// increases it for object1 (and all current or future objects of that class)

object1.staticInt = 10;
object1.normalInt = 10;
System.out.println(object2.staticInt); // Prints: 10
System.out.println(object2.normalInt); // Prints: 1 (object2 retained its own value for normalInt as it depends on the class itself)

Static Methods

The most common example of using static is the main() method, it is declared as static because it must be called before any objects exist. Another common example is the Math class since we use the methods of that class without making an instance of it first (like Math.abs()).

A good way to think about static methods is "Does it make sense to use this method without first creating an object of this class?" (e.g. you don't need to instantiate the Math class in order to calculate the absolute value of a number).

Static methods can be used to access and modify static members of a class. Though, they're commonly used to manipulate method parameters or compute something and return a value.

These methods are referred to as utility methods:

static int average(int num1, int num2) {
    return (num1+num2)/2;
}

This utility method can be used to calculate the average of two numbers, for an example.

As mentioned above, the Math class is often used for calling static methods. If we look at the source code, we can notice that it mostly offers utility methods:

public static int abs(int i) {
    return (i < 0) ? -i : i;
}

public static int min(int a, int b) {
    return (a < b) ? a : b;
}

public static int max(int a, int b) {
    return (a > b) ? a : b;
}

Static Blocks

There's also a static block. A static block gets executed only once when the class is first instantiated (or a static member has been called, even if the class isn't instantiated), and before the rest of the code.

Let's add a static block to our StaticExample class:

class StaticExample() {
    ...
    static {
        System.out.println("Static block");
    }
    ...
}
StaticExample object1 = new StaticExample(); // "Static block" is printed
StaticExample object2 = new StaticExample(); // Nothing is printed

Irrespective of their position in the class, static blocks are initialized before any other non-static blocks, including constructors:

class StaticExample() {
    public StaticExample() {
        System.out.println("Hello from the constructor!");
    }

    static {
        System.out.println("Hello from a static block!");
    }
}

Instantiating this class would output:

Hello from a static block!
Hello from the constructor!

If multiple static blocks are present, they will run in their respective order:

public class StaticExample {
    
    static {
        System.out.println("Hello from the static block! 1");
    }
    
    public StaticExample() {
        System.out.println("Hello from the constructor!");
    }
    
    static {
        System.out.println("Hello from the static block! 2");
    }
}

Instantiating this class would output:

Hello from the static block! 1
Hello from the static block! 2
Hello from the constructor!

Static Imports

As already mentioned, it's better to call static members prefixed with the class name, rather than the instance name. Also, in some cases, we never really instantiate a class with static methods, such as the Math class, which offers numerous utility methods regarding mathematics.

That being said, if we use a class' static members often, we can import individual members or all of them using a static import. This allows us to skip prefixing their calls with the class name:

package packageOne;

public class ClassOne {
    static public int i;
    static public int j;

    static public void hello() {
        System.out.println("Hello World!");
    }
}
package packageTwo;

static import packageOne.ClassOne.i;

public class ClassTwo {
    public ClassTwo() {
        i = 20;
    }
}

Or, if we'd like to import all static members of ClassOne, we could do it like so:

package packageTwo;

import static packageOne.ClassOne.*;

public class ClassTwo {
    public ClassTwo() {
        i = 20;
        j = 10;
    }
}

The same applies to methods:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

package packageTwo;

import static packageOne.ClassOne.*;

public class ClassTwo {
    public ClassTwo() {
        hello();
    }
}

Running this would output:

Hello World!

This may not seem that important, but it helps when we call many static members of a class:

public int someFormula(int num1, int num2, int num3) {
    return Math.ceil(Math.max(Math.abs(num1), Math.abs(num2))+Math.max(Math.abs(num2), Math.abs(num3)))/(Math.min(Math.abs(num1), Math.abs(num2))+Math.min(Math.abs(num2), Math.abs(num3)));
}

// Versus...
import static java.lang.Math.*;
public int someFormula(int num1, int num2, int num3) {
    return ceil(max(abs(num1), abs(num2))+max(abs(num2), abs(num3)))/(min(abs(num1), abs(num2))+min(abs(num2), abs(num3)));
}

The final Modifier

The keyword final can have one of three meanings:

  • to define named constants (variables whose values can't change after initialization)
  • to prevent a method from being overridden
  • to prevent a class from being inherited

Named Constants

Adding the final modifier to a variable declaration makes that variable unchangeable once it's initialized.

The final modifier is often used together with the static modifier if we're defining constants. If we only apply static to a variable, it can still be changed easily. There's also a naming convention tied to this:

static final double GRAVITATIONAL_ACCELERATION = 9.81;

Variables such as these are often included in utility classes, such as the Math class, accompanied by numerous utility methods.

Though, in some cases, they also warrant their own classes, such as Constants.java:

public static final float LEARNING_RATE = 0.3f;
public static final float MOMENTUM = 0.6f;
public static final int ITERATIONS = 10000;

Note: when using final with object reference variables, be careful of what type of behavior you expect. Consider the following:

class MyClass {
    int a;
    int b;

    public MyClass() {
        a = 2;
        b = 3;
    }
}
    final MyClass object1 = new MyClass();
    MyClass object2 = new MyClass();

The reference variable object1 is indeed final and its value can't change, but what does that mean for reference variables anyway? It means that object1 can't change which object it is pointing to anymore, but we can change the object itself. This is something that often confuses people:

    // object1 = object2; // Illegal!
    object1.a = 5; // Perfectly fine

Method parameters can also be declared final. This is used to make sure our method doesn't change the parameter it receives when it's called.

Local variables can also be declared final. This is used to make sure the variable receives a value only once.

Preventing Overriding

If you specify the final modifier while defining a method, any future subclass can't override it.

class FinalExample {
    final void printSomething() {
        System.out.println("Something");
    }
}
class ExtendsFinalExample extends FinalExample {
    // This would cause a compile-time error
    //void printSomething() {
    //  System.out.println("Some other thing");
    //}
    
    // However, you are perfectly free to overload this method
    void printSomething(String something) {
        System.out.println(something);
    }
}

One small bonus of declaring truly final methods as final is a slight performance boost whenever we call this method. Usually, Java resolves method calls dynamically at run-time, but with methods declared final, Java can resolve a call to it at compile time, or if a method is really small it can simply inline calls to that method since it "knows" that it won't be overridden. This eliminates the overhead associated with a method call.

Preventing Inheritance

This usage of final is fairly straight-forward, a class defined with final cannot be inherited. This of course implicitly declares all methods of that class final as well (they can't be overridden if the class can't be inherited in the first place).

final class FinalExample {...}

The abstract Modifier

The abstract modifier is used to define methods that will be implemented in a subclass later on. Most often it's used to suggest that some functionality should be implemented in a subclass, or (for some reason) it can't be implemented in the superclass. If a class contains an abstract method, it must also be declared abstract.

Note: You can not create an object of an abstract class. In order to do that, you need to provide an implementation for all the abstract methods.

An example would be if we had a simple class called Employee that encapsulates data and methods for an employee. Let's say that not every employee is paid in the same way, some types of employees are paid by the hour and some are paid a fixed salary.

abstract class Employee {
    int totalHours; // In a month
    int perHour;    // Payment per hour
    int fixedRate;  // Fixed monthly rate
    ...
    abstract int salary();
    ...  
}
class Contractor extends Employee {
    ...
    // Must override salary if we wish to create an object of this class
    int salary() {
        return totalHours*perHour; 
    }
    ...
}
class FullTimeEmployee extends Employee {
    ...
    int salary() {
        return fixedRate; 
    }
    ...
}
class Intern extends Employee {
    ...
    int salary() {
        return 0; 
    }
    ...
}

If a subclass doesn't provide an implementation to all abstract methods in the superclass, it has to be declared as abstract as well, and an object of that class can't be created.

Note: abstract is used heavily with polymorphism, e.g. we'd say ArrayList<Employee> employees = new ArrayList();, and add Contractor, FullTimeEmployee, and Intern objects to it. Even though we can't create an object of the Employee class, we can still use it as a reference variable type.

The synchronized Modifier

When two or more threads need to use the same resource, we somehow need to make sure that only one of them has access to it at a time, i.e. we need to synchronize them.

This can be achieved in several ways, and one simple and readable way (albeit with somewhat limited usage) is by using the synchronized keyword.

An important concept to understand before you see how to use this keyword is the concept of a monitor. Every object in Java has its own implicit monitor associated with it. A monitor is a "mutually exclusive" lock, meaning that only one thread can "own" a monitor at a time. When a thread enters the monitor, no other thread can enter it until the first thread exits. This is what synchronized does.

Threads are beyond the scope of this article so I will focus on the syntax of synchronized only.

We can synchronize access to methods, and blocks of code. Synchronizing blocks of code works by providing an object instance that we want to synchronize access to and the code that we want to perform related to that object.

class SynchronizedExample {

    ...
    SomeClass object = new SomeClass();
    ....

    synchronized(object) {
         // Code that processes objects
         // only one thread at a time
    }
    
    // A synchronized method
    synchronized void doSomething() {
         ...
    }
    ...
}

The volatile Modifier

The volatile modifier tells Java that a variable can be changed unexpectedly by some other part of the program (like in multi-threaded programming), and so that variable's value is always read from main memory (and not from CPU cache), and that every change to the volatile variable is stored in main memory (and not in CPU cache). With this in mind, volatile should only be used when necessary, since reading/writing to memory every time is more expensive than doing so with CPU cache and only reading/writing to memory when necessary.

In simplified terms - when a thread reads a volatile variable value, it is guaranteed that it will read the most recently written value. Basically, a volatile variable does the same thing that synchronized methods/blocks do, we just can't declare a variable as synchronized.

The transient Modifier

When a variable is declared as transient, that means that its value isn't saved when the object is stored in memory.
transient int a; means that when we write the object to memory, the contents of "a" will not be included. For example, it's used to make sure that we don't store private/confidential information in a file.

When we try to read an object that contains transient variables, all transient variable values will be set to null (or default values for primitive types), no matter what they were when we wrote the object to the file. Another example of usage would be when a variable's value should be derived based on other data (such as someone's current age), and isn't part of the persistent object state.

Note: Something very interesting happens when we use transient and final together. If we have a transient final variable that is evaluated as a constant expression (Strings or primitive types) the JVM will always serialize it, ignoring any potential transient modifier. When transient final is used with reference variables, we get the expected, default behavior of transient.

Conclusion

Modifiers are keywords that let us fine-tune access to our class and its members, their scope and behavior in certain situations. They provide fundamental traits for our classes and their members. Every developer should thoroughly be acquainted with them to make the best use of them.

Like being aware that protected access control can easily be bypassed, or the transient final modifier when it comes to constant expressions.

Last Updated: August 16th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

Olivera PopovićAuthor

LinkedIn: https://rs.linkedin.com/in/227503161
If you need any help - post it in the comments :) That way someone else can reply if I'm busy.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms