Reputation: 3184
I have an entity with a list:
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)
@JoinColumn(name="orderId", nullable=false)
private List<Item> items;
}
@Entity
@Data
public class Item {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@EqualsAndHashCode.Exclude
private Long id;
private String description;
}
I have a service that checks if two orders have the same items and if so returns the items; otherwise it returns null:
public List<Item> getItemsIfSame(Order order1, Order order2) {
if (order1.getItems() != null && order1.getItems().equals(order2.getItems())) {
return order1.getItems();
}
return null;
}
I have a unit test where order1 and order2 have the same items. And as expected the list of items are returned from the getItemsIfSame
method.
But when I run my application and it is passed two orders with the same items, null is returned . After debugging and research, I found that the actual type returned by the Order
method getItems
is org.hibernate.collection.internal.PersistentBag
. Its documentation states:
Bag does not respect the collection API and do an JVM instance comparison to do the equals. The semantic is broken not to have to initialize a collection for a simple equals() operation.
And confirming in the source code, it just calls Object
's equals method (even though it implements List
).
I suppose I could copy all elements from PersistentBag
to ArrayList
and then compare but sometimes I'm checking equality on a object that has some nested property with a list. Is there some better way to check equality of lists between entities?
Upvotes: 13
Views: 7423
Reputation: 192
Solution #1: Using Guava's Iterables#elementsEqual
Iterables.elementsEqual(
order1.getItems() != null ? order1.getItems() : new ArrayList<>(),
order2.getItems() != null ? order2.getItems() : new ArrayList<>());
Solution #2: Using java.util.Objects#deepEquals
Objects.deepEquals(
order1.getItems() != null ? order1.getItems().toArray() : order1,
order2.getItems() != null ? order2.getItems().toArray() : order2);
Solution #3: Using new ArrayList
objects
(order1.getItems() != null ? new ArrayList(order1.getItems()) : new ArrayList())
.equals(order2.getItems() != null ? new ArrayList(order2.getItems()) : new ArrayList());
Solution #4 Using Apache's CollectionUtils#isEqualCollection
CollectionUtils.isEqualCollection(
order1.getItems() != null ? order1.getItems() : new ArrayList(),
order2.getItems() != null ? order2.getItems() : new ArrayList());
Note that the Javadocs for the List#toArray
method state the following:
Returns an array containing all of the elements in this list in proper sequence (from first to last element). The returned array will be "safe" in that no references to it are maintained by this list. (In other words, this method must allocate a new array). The caller is thus free to modify the returned array.
As such, comparing the lists in-place using Iterables
may use less memory than solutions 2, 3, and 4 which all allocate new Lists or Arrays either implicitly or explicitly.
The null check could also be moved out of the ternary but would need to be performed on both of the order
objects because all of these solutions involve invoking methods that are not null-safe (Iterables#elementsEqual
, Lists#toArray
, new ArrayList(Collection<?> collection)
, CollectionUtils.isEqualCollection
will all throw NullPointerExceptions when invoked with null).
Side note: this issue is tracked by a long-standing hibernate bug
Upvotes: 3