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
withTreeSet
- 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()
andhashCode()
(forHashSet
/LinkedHashSet
)compareTo()
or aComparator
(forTreeSet
)
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:
- Implement
equals()
andhashCode()
to define uniqueness. - 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 Case | Best Structure | Why |
---|---|---|
Small set, occasional updates | HashSet + manual replace | Easy to implement |
Need sorted order AND uniqueness | TreeSet + custom Comparator | Sorting built-in |
Frequent updates, large collections | HashMap or TreeMap | Fast lookups & updates |
6. Best Practices
- Implement
equals
andhashCode
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.