Brittle POJO

POJO pattern is pretty popular in Java world. This article demonstrates what's wrong with that.

POJO or JavaBean is popular pattern in Java for data holders. These are simple objects with the main purpose to hold the data in memory. Many frameworks are using them. And they can be created very fast, every IDE has feature to generate getters and setters. Unfortunately such easy pattern is designed to produce brittle code. This will cause you more and more troubles as project grows and gets more complicated. This article is going to demonstrate several issues that come with POJO pattern.

Note: Precondition for further reading is a basic understanding of Java and JUnit.

Example 1

Let's define Rectangle class like this.

public class Rectangle {

    private double width;
    
    private double height;

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }
    
}

Then, what about equality of 2 rectangles? Naturally, I would expect 2 rectangles to be equal as soon as they are same in width and height. Following the actual behavior of Rectangle class definition. Comments on the right show the console output.

Rectangle rect1 = new Rectangle();
rect1.setWidth(1);
rect1.setHeight(2);
Rectangle rect2 = new Rectangle();
rect2.setWidth(1);
rect2.setHeight(2);

System.out.println(rect1.equals(rect1));      // true
System.out.println(rect2.equals(rect2));      // true
System.out.println(rect1.equals(rect2));      // false
System.out.println(rect2.equals(rect1));      // false

As you see, the different instances of the Rectangle are not considered equal, although they are same in width and height. This for example affects the behavior of collections.

List<Rectangle> list = new ArrayList<>();
list.add(rect1);
System.out.println(list.contains(rect1));     // true
System.out.println(list.contains(rect2));     // false

Set<Rectangle> set = new HashSet<>();
set.add(rect1);
System.out.println(set.contains(rect1));     // true
System.out.println(set.contains(rect2));     // false

Map<Rectangle, Integer> map = new HashMap<>();
map.put(rect1, 2);
System.out.println(map.containsKey(rect1));     // true
System.out.println(map.containsKey(rect2));     // false

Here contains method always returns false, unless the tested object is the object instance which was added in. Same for the map keys. That means you can't test whether 2 collections are equal unless they hold the exact same object instances. This creates a huge complications in unit tests. For example, let's define RectangleParser interface.

public interface RectangleParser {
    public List<Rectangle> parse(String input);
}

Next, imagine there is an implementation called SuperParser which needs to be tested. Because there is no way how to test the equality of 2 rectangle objects, then all properties have to be compared manually. Something like this.

RectangleParser parser = new SuperParser();
List<Rectangle> rects = parser.parse("rectangle[1,2],recangle[2,3]");
assertEquals(2, rects.size());
assertEquals(1d, rects.get(0).getWidth(), 0d);
assertEquals(2d, rects.get(0).getHeight(), 0d);
assertEquals(2d, rects.get(1).getWidth(), 0d);
assertEquals(3d, rects.get(1).getHeight(), 0d);

And this is very often the reason why many developers don't write unit tests at all. Very typical excuses are:

  • We don't have time for unit tests
  • Business logic in our project is too complicated
  • I am a developer, not a tester

Many others just write unit tests for simplest units, or end up by comparing for example only list sizes without having any clue about the objects inside. Such tests are just good to show the green bar to non-tech managers and doesn't bring any real value to the project. What brings real value to the project are unit tests of the most complex units with deep comparison. Unfortunately lack of equals method in data holding objects makes impossible to create them. Therefore let's improve that.

Example 2

Adding equals method. This method comes together with hashCode method. For those who haven't done this yet, I would recommend to spend 5 minutes and read about them in Javadoc. Here is the next version of the Rectangle class.

public class Rectangle {

    // ... same properties with getters and setters as before

    @Override
    public int hashCode() {
        return (int) width + 13 * (int) height;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof Rectangle)) {
            return false;
        }
        Rectangle other = (Rectangle) obj;
        return other.width == width && other.height == height;
    }

}

The outcome of previous code after adding equals and hashCode methods.

Rectangle rect1 = new Rectangle();
rect1.setWidth(1);
rect1.setHeight(2);
Rectangle rect2 = new Rectangle();
rect2.setWidth(1);
rect2.setHeight(2);

System.out.println(rect1.equals(rect1));      // true
System.out.println(rect2.equals(rect2));      // true
System.out.println(rect1.equals(rect2));      // true
System.out.println(rect2.equals(rect1));      // true

List<Rectangle> list = new ArrayList<>();
list.add(rect1);
System.out.println(list.contains(rect1));     // true
System.out.println(list.contains(rect2));     // true

Set<Rectangle> set = new HashSet<>();
set.add(rect1);
System.out.println(set.contains(rect1));     // true
System.out.println(set.contains(rect2));     // true

Map<Rectangle, Integer> map = new HashMap<>();
map.put(rect1, 2);
System.out.println(map.containsKey(rect1));     // true
System.out.println(map.containsKey(rect2));     // true

You see, rectangles are considered to be equal and it is possible to test whether collections contains the specific one. Then the test case for RectangleParser can be rewritten in this way.

RectangleParser parser = new YourSuperParser();
List<Rectangle> expected = Arrays.asList(... insert the rectangles...);
List<Rectangle> rects = parser.parse("rectangle[1,2],recangle[2,3]");
assertEquals(expected, rects);

This is much better, because objects are deeply compared. Such tests are much more robust than the previous ones so developers can seamlessly catch and fix the (side) effects of the code changes. Seems like a problem solved. Unfortunately such way brings another issue. Look at this.

Rectangle rect1 = new Rectangle();
rect1.setWidth(1);
rect1.setHeight(2);
Rectangle rect2 = new Rectangle();
rect2.setWidth(1);
rect2.setHeight(2);

Set<Rectangle> set = new HashSet<>();
set.add(rect1);
System.out.println(set.contains(rect1));     // true
System.out.println(set.contains(rect2));     // true

rect1.setWidth(5);

System.out.println(set.contains(rect1));     // false
System.out.println(set.contains(rect2));     // false

Now rectangle was inserted to the set. Since both rectangles are equal, then set returns true when calling contains method. Next, original rectangle was changed. That means it's hash code value changed as well. But set isn't aware of this change. That means it keeps the object in the wrong bucket. And therefore it looks like the rectangle disappeared from the set. In this example it is easy to spot, but it's very hard to find it when same situation happens in a large system. This means that invocation of public method can easily break completely different portion(s) of the application.

This is the problem of all mutable patterns. You might solve it by convention to say, no one is going to call setter after the object is constructed. It might or might not work out for you. I have personally chosen not to rely on such convention.

Example 3

What about popular inheritance? Let's extend Rectangle class and add color in there.

public class ColorRectangle extends Rectangle {

    private int color;

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }

    @Override
    public int hashCode() {
        return (int) getWidth() + 13 * (int) getHeight() + 169 * color;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof ColorRectangle)) {
            return false;
        }
        ColorRectangle other = (ColorRectangle) obj;
        return other.getWidth() == getWidth() && other.getHeight() == getHeight() && other.color == color;
    }

}

And again a small test.

Rectangle rect1 = new Rectangle();
rect1.setWidth(1);
rect1.setHeight(2);
ColorRectangle rect2 = new ColorRectangle();
rect2.setWidth(1);
rect2.setHeight(2);
rect2.setColor(0x00ffff00);

System.out.println(rect1.equals(rect2));     // true
System.out.println(rect2.equals(rect1));     // false

Result is that rect1 is equal to rect2, and rect2 is not equal to rect1. That means the symmetric relation for equals method is broken. And it is proven that if you extend some class and add extra property into the child one, then there is no way how to make equals method work according contract written in Javadoc, unless the parent class is aware of the child. This can easily cause a weird behavior which is hard to uncover.

Other issues

Regarding consistency. POJO objects are not guaranteed to be consistent. Properties are set one by one after the construction. Objects might be in invalid state and validation has to be invoked somehow externally. This means another responsibility for users. In addition, any later call of the setter might put object into an invalid state again.

Regarding thread safety. POJO objects are not thread safe by definition. This brings another limitations to the users.

Conclusion

In this article I have demonstrated several issues with POJO pattern. For those reasons I have decided to use purely immutable objects with prohibition of inheritance as a main data holders. These objects might be constructed for example by builder pattern or static factory method. Like this.

Rectangle rect1 = new Rectangle.Builder().
        setWidth(1).
        setHeight(2).
        build();
Rectangle rect2 = Rectangle.ceate(1, 2);

Important is that every object is guaranteed to be in valid state and immutable for the whole life time. Therefore it is safe to use such objects in collections and multi threaded environment. Mentioned issued just doesn't exists. POJOs are still good as a bridge to various framework as soon as they are used purely inside that integration layer, and never ever leak to the core application code. If you would like to get more details about this topic, then Effective Java written by Joshua Bloch is a great resource.

GET NEW CONTENT STRAIGHT INTO YOUR BOX


Preview

Anomaly detection using the Bag-of-words model

A practical method of finding anomalies within the set of activities using the Bag-of-words model.

Preview

Unit-Level Performance tuning in Java

How to simply improve performance of your java applications. Everything purely in IDE, without even starting the whole app.

Preview

Bitcoin and JUnit

How to unit test bitcoin with Java and JUnit.

Preview

Brittle POJO

POJO pattern is pretty popular in Java world. This article demonstrates what's wrong with that.