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 list = new ArrayList<>();
list.add(rect1);
System.out.println(list.contains(rect1)); // true
System.out.println(list.contains(rect2)); // false
Set 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 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 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 list = new ArrayList<>();
list.add(rect1);
System.out.println(list.contains(rect1)); // true
System.out.println(list.contains(rect2)); // true
Set 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 expected = Arrays.asList(... insert the rectangles...);
List 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 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.