Removing duplicates in installed applications list in OSHI
PR: https://github.com/oshi/oshi/pull/2902
In a recent contribution to OSHI, a key improvement was made to how installed applications are fetched across OS types: deduplication of application entries while preserving their original order.
When querying Windows registry entries across both KEY_WOW64_64KEY and KEY_WOW64_32KEY, it's not uncommon to encounter duplicate entries for the same application. To solve this, we leveraged Java’s LinkedHashSet and LinkedHashMap — ensuring that the final list is unique and consistently ordered.
Problem: Duplicate Application Entries
On 64-bit Windows systems, applications may be listed in both the 64-bit and 32-bit registry views.
Solution: Deduplicate with LinkedHashSet
We changed the internal data structure used to accumulate applications to a LinkedHashSet<ApplicationInfo>:
Set<ApplicationInfo> appInfoSet = new LinkedHashSet<>();
// Populate the set
appInfoSet.add(app);
// Convert back to list for return
return new ArrayList<>(appInfoSet);
This deduplicates by using equals()/hashCode() on ApplicationInfo, while maintaining the original discovery order — something that HashSet would not guarantee.
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ApplicationInfo)) {
return false;
}
ApplicationInfo that = (ApplicationInfo) o;
return timestamp == that.timestamp && Objects.equals(name, that.name) && Objects.equals(version, that.version)
&& Objects.equals(vendor, that.vendor) && Objects.equals(additionalInfo, that.additionalInfo);
}
@Override
public int hashCode() {
return Objects.hash(name, version, vendor, timestamp, additionalInfo);
}
Why Order Matters in equals() and hashCode()
The ApplicationInfo class includes a field: Map<String, String> additionalInfo. Originally, this was a plain HashMap. But this caused problems with equals() and hashCode() because HashMap does not preserve insertion order — meaning two logically identical maps could produce different hash codes.
To ensure stable behaviour, we replaced it with a LinkedHashMap, which guarantees order preservation:
this.additionalInfo = additionalInfo != null
? new LinkedHashMap<>(additionalInfo)
: Collections.emptyMap();
This change was subtle but important. Without it, two ApplicationInfo objects with the same content might fail equals() or be incorrectly handled in Sets — breaking our deduplication logic.
Unit Tests for Deduplication and Hash Consistency
We added test cases to confirm ApplicationInfo behaves correctly in sets.
public void testEqualsAndHashCodeSameValues() {
Map<String, String> info1 = new LinkedHashMap<>();
info1.put("installLocation", null);
info1.put("installSource",
"C:\\ProgramData\\Package Cache\\{FE8C7838-D3E6-4CEA-87BE-216E42391827}v20.2.37.0\\");
ApplicationInfo app1 = new ApplicationInfo("SQL Server Management Studio", "20.2.37.0", "Microsoft Corp.",
1746576000000L, info1);
ApplicationInfo app2 = new ApplicationInfo("SQL Server Management Studio", "20.2.37.0", "Microsoft Corp.",
1746576000000L, new LinkedHashMap<>(info1));
assertEquals(app1, app2);
assertEquals(app1.hashCode(), app2.hashCode());
}
@Test
public void testEqualsAndHashCodeDifferentVersion() {
ApplicationInfo app1 = new ApplicationInfo("SQL Server Management Studio", "20.2.37.0", "Microsoft Corp.",
1746576000000L, new LinkedHashMap<>());
ApplicationInfo app2 = new ApplicationInfo("SQL Server Management Studio", "20.3.37.0", "Microsoft Corp.",
1746576000000L, new LinkedHashMap<>());
assertNotEquals(app1, app2);
}
@Test
public void testDeduplicationWithListResult() {
ApplicationInfo app1 = new ApplicationInfo("SQL Server Management Studio", "20.2.37.0", "Microsoft Corp.",
1746576000000L, new LinkedHashMap<>());
ApplicationInfo app2 = new ApplicationInfo("SQL Server Management Studio", "20.2.37.0", "Microsoft Corp.",
1746576000000L, new LinkedHashMap<>());
Set<ApplicationInfo> dedupedSet = new LinkedHashSet<>();
dedupedSet.add(app1);
dedupedSet.add(app2); // Duplicate
List<ApplicationInfo> resultList = new ArrayList<>(dedupedSet);
assertEquals(1, resultList.size());
assertEquals(app1, resultList.get(0));
}
Conclusion
This contribution showcases how use of data structures (LinkedHashSet, LinkedHashMap) can improve data quality, stability, and correctness