Module 3: Static Methods
Module Overview
Understand static methods, when to use them, and how to properly test code that uses static methods.
Learning Objectives
- Understand static methods and their use cases
- Learn when static methods are appropriate and when to avoid them
- Recognize the testing challenges presented by static methods
- Explore techniques for testing code that uses static methods
- Master PowerMock for mocking static methods in tests
- Refactor code to improve testability by reducing static method dependencies
- Learn to use dependency injection to make code with static dependencies more testable
- Understand testing strategies for wrapper classes around static methods
- Apply best practices for testing date/time and random number utilities
Introduction to Static Methods
In this reading, we'll review instance methods and introduce static methods, which are declared using the keyword, static. We'll also go over common uses of static methods and potential tradeoffs.
Instance methods
As you've previously learned, a class can be thought of as a blueprint from which individual objects are created. Each object is an instance of that class in memory.
A class defines the type of attributes each object will have. An Employee class might contain attributes such as jobTitle and level. Each object will contain values for these attributes. An SDE at Amazon might have the values "SDE I" and 4 set as their job title and level, while a product manager might have the values "Project Manager" and 5 set as their job title and level. We often refer to these as instance variables, as each instance of a class will have these variables.
A class also defines the behaviors each object will have. We call these methods. The methods of a class typically update, return, or act upon the instance variables we described above. Let's consider a getter method in our Employee class, getLevel(). Each instance of a class will return a value unique to its instance variable, level. So, these methods are often referred to as instance methods.
Consider the code below that updates the levels of two Employee objects.
Employee sdeI = new Employee("SDE I", 4);
sdeI.setLevel(5);
Employee productManager = new Employee("Project", 6);
productManager.setLevel(7);
Each object is updated using the instance method setLevel(). Each Employee's level instance variable will be updated in memory.
Static methods
A static method is a class method that does not update, return, or act upon instance variables. It contains logic that is common to all instances of a class. For example, you may have methods that accept an argument and return a result but do not change the value of any instance variables. These types of methods are known as utility or conversion methods. These are often referred to as being stateless, meaning that they don't change or rely on any internal state.
Static methods can be identified by looking for the keyword static in their method headers. A static method you may have seen before is String's valueOf() method, which returns the string representation of the argument passed in. Its method header looks like this:
public static String valueOf(int i)
Calling a static method
A static method is invoked by calling the method on the class rather than on an object of the class. For example, the call to String's valueOf() below is called on the class, String, rather than on a particular instance:
int intVersion = 24;
String stringVersion = String.valueOf(intVersion);
In comparison, the call to String's length() instance method is called on an object of type String:
int intVersion = 24;
String stringVersion = String.valueOf(intVersion);
int stringLength = stringVersion.length();
This is because length() needs information specific to the stringVersion object.
Uses of static methods
What's the benefit of using a static method instead of an instance method? First, many methods that you'd like to use are defined as static methods. Take a look at the Java Collections utility class, for example. It has a bunch of useful utility methods that are all declared static. (You can't instantiate a Collections object even if you wanted to!) You can call a static method even when no instance exists.
In your own code, if a method doesn't make use of any instance members of a class (i.e. mutating the state of the instance), ask yourself:
- Does it make sense for the method to be part of this class?
- Does it relate very specifically to this class?
- Does it provide a tool that's important to have available when working with this class?
If the answers are "yes", then you might consider making that method a static method.
Utility methods
Utility methods, which we described above as methods that take an argument and return a result but do not change the internal state of any instance, are the most common usage of static methods. You've probably seen some of them before, such as Math.random() and Integer.toString(int j). Math is an example of a utility class, which is a class that contains related static methods (like Math.random()) that can be reused across the application. These utility methods perform an operation that is related to the class and provide necessary functionality, but don't specifically require an instance of the class.
Alternative constructors
Static methods can also be used as an alternative to constructors. These static methods return a new object of the same type as the class they are defined in. For example, String.valueOf(int i), is a way of creating a new String object.
Unlike constructors, these methods have names. We can tell from the method name, that the valueOf(int i) method is going to parse its parameter and create a new String object with the value.
Tradeoffs of static methods
There are also some tradeoffs and potentially incorrect situations to use static methods.
Static methods can access other static methods directly, but static methods cannot access instance methods or instance variables. If you attempt to, the Java compiler will generate the error: "non-static variable [variable name] cannot be referenced from a static context."
It's also important to understand the structure of your classes before creating a static method. Static methods can't be overridden by subclasses, so they can't be called polymorphically -- you'll learn about subclasses and polymorphism in a future lesson.
Static methods can create issues with testing. It's possible to mock out a static method call, but the associated tools are fragile and updates to Java can break these tests. So, we won't typically mock static method calls. You'll learn more about mocking in tests in a later unit.
Our recommendations
We recommend going ahead and calling static methods that are useful to you, particularly factory methods and utility class methods. Often a library you are using only exposes static methods, so go ahead and use them in that case as well. Outside these cases, don't go out of your way to use static methods, especially if you might want to mock that method's behavior in your tests.
Similarly, we only recommend creating a static method if you're implementing the factory pattern or creating a utility class. In those cases, the classes should only have static methods and should be declared final (this means they can't be subclassed---more in a future lesson).
Static Variables and Constants
In the previous reading, you learned about static methods and their uses. In this reading, you will learn how the static keyword applies to variables, especially for creating constants.
Static variables
Static methods are methods that are associated with a class rather than an object instance. Can we do something similar with a variable, associate it with a class rather than an instance? Yes, we can! This is called a static variable. A static variable is associated with a class and shared by all instances of a class. The value can be used by any instance of the class.
Like instance variables, static variables can be initialized either in the declaration or later in the class. A static variable is declared in the following way:
private static long employeeCount = 0;
A static variable contains a single value across all instances so modifying its value affects every instance. Each time we create a new Employee object the constructor might increment this value.
We can run into trouble sharing a modifiable variable like this. We'll cover this more when we get to concurrency in a later unit. For now, we'll show you a way to share an unmodifiable shared variable.
Static final variables (constants)
A common use of a static variable is as a constant. A constant is a variable whose value should not and cannot be reassigned during the program execution. Constants should be immutable objects, meaning that the objects' data cannot change after construction, but they aren't guaranteed to be. A static final variable that points to a mutable object is almost always a bad design decision and should be avoided. You'll typically see constants that are Strings or primitive data types. The exception to this is in unit test classes. SDEs often create constants of complex types to be shared in the test methods.
Constants are declared in Java using the keywords static and final:
private static final int MIMIMUM_REQUIRED_CHOCOLATE_CHIPS = 5;
The final keyword means that we can never assign a new value to the variable during the program execution. It is a common convention that the variable name for constants is in all caps with words separated by underscores. This naming format is enforced by ATA's checkstyle and is a best practice, but it is not actually enforced by the Java compiler itself.
Constants are meant to represent variables that universally have one value that won't change, such as scientific constants (i.e. earth's gravity = 9.81 meters per second) or the maximum value of resources available during execution (e.g. max open files = 50). Java, for example, provides the value of pi in the Math utility class which can be accessed with Math.PI.
Public vs. private
Like any other variable in Java, a static constant can be declared as public or private. Private constants are accessible only by the class they're declared in. This is a safe usage of constants because the scope of its usage is very narrow.
Public constants should be used carefully. A public static constant can be referenced by any class throughout the entire program, just like a public instance variable. If the value is updated it could have far reaching and unexpected repercussions in the classes using it. If you're declaring a public static constant, be careful to only declare values that would never change, such as scientific/mathematic constants that you know will always have the same value.
Our recommendations
- Try not to create (non-final) static variables if you can avoid them. If you have a compelling use case for it, make them private.
- Create constants as you wish. Determine if they need to be accessed from outside the class and if not make them private.
- Think twice about using a mutable object as a static final constant. This goes against the definition of a constant.
Understanding Static Methods
Static methods belong to a class, not to instances of that class. They can be called without creating an object of the class and are commonly used for utility functions or operations that don't require object state.
While static methods are useful in many scenarios, they present unique challenges for testing because:
- They cannot be overridden like instance methods
- Dependencies are typically hard-coded within the method
- Standard mocking frameworks don't work with static methods by default
// Example of a static method
public class DateUtils {
public static LocalDate getCurrentDate() {
return LocalDate.now();
}
public static boolean isWeekend(LocalDate date) {
DayOfWeek day = date.getDayOfWeek();
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
}
}
Testing Static Methods
There are several approaches to testing code that uses static methods:
- Using PowerMock: Directly mock static method calls (more complex setup)
- Wrapper Classes: Create instance-based wrappers around static utilities
- Dependency Injection: Pass in utilities as dependencies
- Design for Testability: Refactor to avoid direct static dependencies
// Example using PowerMock to mock a static method
@RunWith(PowerMockRunner.class)
@PrepareForTest(DateUtils.class)
public class WeekendPricingTest {
@Test
public void calculatePrice_onWeekend_appliesWeekendSurcharge() {
// GIVEN
LocalDate saturday = LocalDate.of(2023, 7, 15);
PowerMockito.mockStatic(DateUtils.class);
when(DateUtils.getCurrentDate()).thenReturn(saturday);
when(DateUtils.isWeekend(saturday)).thenReturn(true);
WeekendPricing pricing = new WeekendPricing();
// WHEN
double price = pricing.calculatePrice(100);
// THEN
assertEquals(120, price, 0.01); // Expecting 20% weekend surcharge
}
}
Designing for Testability
The most maintainable solution is often to design your code to minimize direct static dependencies:
// Before: Hard to test due to static dependency
public class WeekendPricing {
public double calculatePrice(double basePrice) {
if (DateUtils.isWeekend(DateUtils.getCurrentDate())) {
return basePrice * 1.2; // 20% weekend surcharge
}
return basePrice;
}
}
// After: Improved testability with dependency injection
public class WeekendPricing {
private final DateProvider dateProvider;
public WeekendPricing(DateProvider dateProvider) {
this.dateProvider = dateProvider;
}
public double calculatePrice(double basePrice) {
if (dateProvider.isWeekend(dateProvider.getCurrentDate())) {
return basePrice * 1.2; // 20% weekend surcharge
}
return basePrice;
}
}
// Simple interface that can be mocked
public interface DateProvider {
LocalDate getCurrentDate();
boolean isWeekend(LocalDate date);
}
// Default implementation uses static methods
public class DefaultDateProvider implements DateProvider {
@Override
public LocalDate getCurrentDate() {
return LocalDate.now();
}
@Override
public boolean isWeekend(LocalDate date) {
DayOfWeek day = date.getDayOfWeek();
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
}
}