Managing Unique Objects in Java Collections: Update Instead of Ignore

When working with Java applications, you’ll often maintain collections of objects — user accounts, transactions, balances, etc. A common challenge arises when you’re using a Set to enforce uniqueness, but later need to update an existing object when a “duplicate” arrives, rather than ignore it.

In this article, we’ll explore several ways to achieve this:

  • Using HashSet with manual updates
  • Using a Comparator with TreeSet
  • Switching to a Map for more efficient updates

By the end, you’ll know which strategy best fits your use case.


1. Why a Set Alone Isn’t Enough

Java’s Set interface (HashSet, LinkedHashSet, TreeSet) guarantees that no duplicate elements exist. Duplicates are defined by either:

  • equals() and hashCode() (for HashSet / LinkedHashSet)
  • compareTo() or a Comparator (for TreeSet)

However, when you add a “duplicate” object to a Set, the existing object stays untouched and the new one is discarded.
There’s no automatic “update” feature — you must handle it yourself.


2. Updating Objects in a HashSet

If you want a HashSet to hold the newest version of an object, you can:

  1. Implement equals() and hashCode() to define uniqueness.
  2. Before adding a new object, remove the old one, then add the new one.
Set<MyObject> mySet = new HashSet<>();

public void addOrUpdate(MyObject newObj) {
    if (mySet.contains(newObj)) {
        mySet.remove(newObj); // Remove the old one
    }
    mySet.add(newObj); // Add the new or updated one
}

This is the simplest way to “replace” an element in a HashSet.


3. Using a Comparator with a TreeSet

A TreeSet uses a Comparator (or the natural order) to determine uniqueness. You can define the uniqueness of an object without including the “latest date” field in the comparison, then handle the update manually.

Example comparator (by account type, credit limit, and currency):

public class AccountBalanceComparator 
        implements Comparator<AccountBalance> {

    @Override
    public int compare(AccountBalance b1, AccountBalance b2) {
        int typeCompare = b1.getBalance().getBalanceType()
            .compareTo(b2.getBalance().getBalanceType());
        if (typeCompare != 0) return typeCompare;

        int limitCompare = b1.getBalance().getCreditLimitIncluded()
            .compareTo(b2.getBalance().getCreditLimitIncluded());
        if (limitCompare != 0) return limitCompare;

        return b1.getBalance().getBalanceAmount().getCurrency()
            .compareTo(b2.getBalance().getBalanceAmount().getCurrency());
        // Note: we’re not comparing referenceDate here
    }
}

And then, when adding:

Set<AccountBalance> balances = 
    new TreeSet<>(new AccountBalanceComparator());

public void addOrUpdateBalance(AccountBalance newBalance) {
    AccountBalanceComparator comp = new AccountBalanceComparator();

    for (AccountBalance existing : balances) {
        if (comp.compare(newBalance, existing) == 0) {
            // Replace only if new date is later
            if (newBalance.getBalance().getReferenceDate()
                    .isAfter(existing.getBalance().getReferenceDate())) {
                balances.remove(existing);
                balances.add(newBalance);
            }
            return;
        }
    }
    balances.add(newBalance); // Add if no duplicate found
}

This ensures:

  • Uniqueness based on fields other than date.
  • Latest version kept automatically when dates differ.

4. Using a Map for Faster Updates

If you frequently update existing records, a Map is more efficient. The key represents the unique identity of the object; the value is the object itself.

Map<String, AccountBalance> balanceMap = new HashMap<>();

public void addOrUpdate(AccountBalance newBalance) {
    String key = buildKey(newBalance); // e.g. type+limit+currency
    AccountBalance existing = balanceMap.get(key);

    if (existing == null || 
        newBalance.getBalance().getReferenceDate()
           .isAfter(existing.getBalance().getReferenceDate())) {
        balanceMap.put(key, newBalance);
    }
}

private String buildKey(AccountBalance balance) {
    return balance.getBalance().getBalanceType() + "|" +
           balance.getBalance().getCreditLimitIncluded() + "|" +
           balance.getBalance().getBalanceAmount().getCurrency();
}

This approach gives O(1) lookups and updates on average and is the cleanest for large data sets.


5. When to Use Each Approach

Use CaseBest StructureWhy
Small set, occasional updatesHashSet + manual replaceEasy to implement
Need sorted order AND uniquenessTreeSet + custom ComparatorSorting built-in
Frequent updates, large collectionsHashMap or TreeMapFast lookups & updates

6. Best Practices

  • Implement equals and hashCode correctly. Incorrect implementations cause “phantom” duplicates.
  • Don’t include mutable fields in hashCode — especially if they can change after insertion.
  • Use immutable keys for maps. For complex uniqueness, concatenate multiple fields into one key.
  • Unit-test your update logic to ensure only newer objects replace older ones.

7. Key Takeaways

  • A Set alone cannot “update” an existing element — you must remove then add.
  • A Comparator helps define uniqueness, but you still need to handle updates manually.
  • A Map is often the best fit for scenarios where you need to store unique items and replace them efficiently.
This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back