Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Brittle POJOs

DZone's Guide to

Brittle POJOs

Let's examine POJOs to see how they're more breakable than you might expect as well as some considerations you can implement to keep them robust.

· Java Zone ·
Free Resource

Automist automates your software deliver experience. It's how modern teams deliver modern software.

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

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

Example 1

Let's define a 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;
    }

}


But what about the equality of two rectangles? Naturally, I would expect two rectangles to be equal as soon as they are same in width and height. What follows is the actual behavior of the 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 can see, the different instances of Rectangle are not considered equal, although they are the 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, the contains method always returns false — unless the tested object is the object instance that was added in. Same for the map keys. That means you can't test whether two collections are equal unless they hold the exact same object instances. This creates huge complications in unit tests. For example, let's define a RectangleParser interface.

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


Next, imagine there is an implementation called SuperParser that needs to be tested. Because there is no way to test the equality of two rectangle objects, 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.
  • The business logic in our project is too complicated.
  • I am a developer, not a tester.

Many others just write unit tests for the simplest units, or end up 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 don'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 comparisons. Unfortunately, the lack of an equals method in data holding objects makes it impossible to create them.

Let's improve that.

Example 2

Let's add an equals method. This method comes together with the hashCode method. For those who haven't done this yet, I would recommend spending 5 minutes reading about them in the 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 the previous code after adding the 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 that the rectangles are considered to be equal and it is possible to test whether collections contain a 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 and approach 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, a rectangle was inserted into the set. Since both rectangles are equal, the set returns true when calling the contains method. Next, the original rectangle was changed. That means its hash code value changed as well. But the 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 when the same situation happens in a large system. This means that the invocation of a public method can easily break completely different portions of the application.

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

Example 3

What about popular inheritance? Let's extend the Rectangle class and add some 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


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

Other Issues

Regarding consistency, POJO objects are not guaranteed to be consistent. Properties are set one by one after construction. Objects might be in an 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 the object into an invalid state again.

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

Conclusion

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

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


It's important to note that every object is guaranteed to be in a valid state and immutable for their whole lifetime. Therefore, it is safe to use such objects in collections and multi-threaded environments. The mentioned issues just don't exist. POJOs are still good as a bridge to various frameworks if 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 the open source Atomist Software Delivery Machine and start automating your delivery right there on your own laptop, today!

Topics:
java ,programming in java ,code quality ,quality assurance ,pojo ,unit testing ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}