Blog-Archiv

Freitag, 22. Januar 2021

Why Java Static on Methods Is Bad

In this article I'd like to argue about the Java static keyword. Static is an access modifier that binds a field or method to its class instead of its object instance. It has become an unpleasant obsession to use statics, because implementing with globals is much easier than to follow the object-oriented idea of small, comprehensible and controllable scopes. Java is OO, and we should not let us throw back to structured programming as done with old languages like C.

Mind that the static keyword on inner classes has a complete different semantic. It's not bad, it's actually good. It makes the instances of the inner class independent of the outer class instance, because they don't have a (hidden) reference to the outer object. The garbage collector will be able to easily collect such instances, whereby it never could collect instances of non-static inner classes as long as the outer instance is still in use.
Also the static keyword on import statements is not subject of this article.

I won't discuss static fields. Anybody that used Java for some years will tell you that you should not use static non-final fields. Always make them final. Read these quotes from Stackoverflow:

Quotes

You cannot override static methods. They don't participate in runtime polymorphism. Interfaces and abstract classes only define non-static methods. Static methods thus don't fit very well into inheritance. Statics have a lifetime that matches the entire runtime of the program. The memory consumed by all those static variables can never be garbage collected. In object-oriented programming, each object has its own state, represented by non-static instance variables. Static variables, on the other hand, represent state across instances which can be much more difficult to unit-test. Statics can’t be easily mocked. Since all statics fields live in just one space, all threads wishing to use them must go through a slow synchronization control that you have to implement on each of them. Static fields wouldn't be serialized. The tighter the scope of something, the easier it is to reason about. We're good at thinking about small things, but it's hard to reason about the state of a system made from millions lines of code. Statics tend to produce spaghetti code and don't easily allow refactoring or testing.

Override Example

Following example shows that code in static methods can not be overridden.

public class PrintUtil
{
    public static void print(String text)    {
        final String printText = "["+text+"]";
        System.out.println(printText);
    }
}

public class CustomPrintUtil extends PrintUtil
{
    public static void print(String text)    {
        final String printText = ">"+text+"<";
        System.err.println(printText);
    }
}

public class Main
{
    public static void main(String[] arguments)    {
    	PrintUtil printUtil = new CustomPrintUtil();
    	printUtil.print("Hello World");
    }
}

Output of Main.main() is, on System.out:

[Hello World]

Expected was, on System.err:

>Hello World<

Reason is that main() assigned the new object to a variable typed as PrintUtil. Had it been CustomPrintUtil, it would have worked as expected, but - who can control that?

Constructor Class Example

Important to understand: constructor calls are static references, although there is no static keyword anywhere!

This example shows that it is impossible to replace class Line by IndentedLine inside the MultilineText class, because there is a static class reference, i.e. a constructor call with the new operator, that has not been put into an overridable factory method.

Here is the element class Line:

public class Line
{
    private final String text;
	
    public Line(String text)    {
    	this.text = text;
    }
    
    public void output(PrintStream printStream)	{
    	printStream.println(text);
    }
}

It lives inside MultilineText:

public class MultilineText
{
    private final List<Line> lines = new ArrayList<>();
	
    public void addLine(String line)    {
    	lines.add(new Line(line));
    }
    
    public Iterable<Line> lines()	{
    	return Collections.unmodifiableList(lines);
    }
}

There is no static modifier anywhere, but the new Line() constructor call in line 6 must be seen as static class reference, basically it is the same as a static LineFactory.create() call:

public final class LineFactory
{
    public static Line create(String text) {
        return new Line(line);
    }
}
(This static factory is useless and here just to illustrate the static nature of constructors.)
....
    public void addLine(String line)    {
    	lines.add(LineFactory.create(line));
    }
....

Here comes the IndentedLine class that we would like to have inside the MultilineText class:

public class IndentedLine extends Line
{
    public IndentedLine(String text)    {
    	super(text);
    }
    
    @Override
    public void output(PrintStream printStream)	{
    	printStream.print("\t");
    	super.output(printStream);
    }
}

The following procedure tries to replace Line inside MultilineText with an anonymous class override:

        MultilineText multilineText = new MultilineText()    {
            @Override
            public void addLine(String line) {
                lines.add(new IndentedLine(line));  // <- compile error ...
                // ... because lines is private!
            }
        };
        multilineText.addLine("Hello World!");
        multilineText.addLine("Goodbye World!");
        
        for (Line line : multilineText.lines())
            line.output(System.out);

The override of the addLine() method causes a compile error because the private lines list is not accessible for sub-classes. If it was protected, sub-classes could access it, but also corrupt it.


How to fix that? Break encapsulation and make line-list protected? I know a better solution:

public class MultilineText
{
    private final List<Line> lines = new ArrayList<>();
	
    public void addLine(String line)    {
        lines.add(createLine(line));
    }
    
    protected Line createLine(String line)    {
        return new Line(line);
    }
    
    public Iterable<Line> lines()	{
        return Collections.unmodifiableList(lines);
    }
}

We encapsulated the Line constructor into an overridable factory-method protected Line createLine(). Now we can easily override the factory method and generate another type of Line:

        MultilineText multilineText = new MultilineText()    {
            @Override
            public Line createLine(String line) {
                return new IndentedLine(line));
            }
        };
        multilineText.addLine("Hello World!");
        multilineText.addLine("Goodbye World!");
        
        for (Line line : multilineText.lines())
            line.output(System.out);

And we get it indented:

    Hello World!
    Goodbye World!

Mind what a powerful techique factory methods are: we can override/customize not just a certain class but a whole class graph by embedding further anonymous overrides!

Conclusion

It is really hard to give a convincing proof that static methods, or constructor calls without factory-method, are a bad practice. They are so frequent, everybody uses them.

Historically seen, using static is a technical throwback of 30 years. Code inside static methods is not overridable, and only reusable when all mutable state is given as parameters, in other words, when it is a "pure function".

The Spring framework tried to solve the problem of statics and globals. You should not use the Spring context as global static space, moreover you should have one context per class-graph (component, module). Then apply dependency injection wherever you are tempted to use the static keyword.

The object-oriented vision is that we solve problems using object graphs that wrap any new operator or static call into a protected overridable (non-final) factory-method. Such graphs are then fully customizable and thus reusable frameworks. One such graph can represent a component (or Java 9 JPMS-module), and components can be bound together at runtime over their interfaces using service loading or dependency injection. JPMS modules support service declarations.




Keine Kommentare: