Java实现数据去重的几种方案及其去重原理
背景
之前就有写过实现根据对象的某个字段去重的一个方案,利用Java8的流,不过实际上使用的还是TreeSet。
不过,当时对于这个去重原理没有过多深究,只是想着Set结构天生就是去重的,今天再次用到,稍微了解一下原理。
1.TreeSet去重
TreeSet底层是基于红黑树实现的,这意味着存在TreeSet的数据是有序的。
这个排序规则分两种情况:
1.如果在创建TreeSet的时候使用的是无参构造函数,则要求添加到TreeSet中的元素必须实现Comparable接口,并重写compareTo方法,否则添加元素的时候会报如下错误。
实现根据对象的特定字段去重,不推荐使用该方案。因为一般情况下,compareTo判断相等的对象,equals判断最好也要相等。
2.如果在创建TreeSet的时候传入了一个比较器,则该TreeSet通过传入的比较器去对数据进行排序,也就是传入的元素不需要实现Comparable接口也可以。
TreeSet<User> set= new TreeSet<>(Comparator.comparing(User::getAge));
通过源码层面去分析,TreeSet在调用构造函数的时候,底层都是创建TreeMap。不同的是有比较器作为入参的会将比较器存起来,如下。
有参构造函数创建的TreeSet,则使用传入的比较器去比较排序。
而无参构造函数的TreeSet在添加新元素时通过所添加元素的compareTo方法去比较:(返回0则替换值)
最前面的代码例子就属于情况2,逻辑上系统会根据用户的age去对数据进行排序,在使用比较器比较返回0的时候就认为两个对象是一样的,后一个就会替代前一个。这也意味着如果根据比较器逻辑有重复的数据,最终只会保留最后出现的那一个数据。
也就是说TreeSet里面不会存在age相同的对象。
使用有参的TreeSet+流是比较简洁的一种数据去重方案。(推荐)
public class Test {
public static void main(String[] args) {
List<User> list=new ArrayList<>();
List<User> distinctList=list.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(()->new TreeSet<>(Comparator.comparing(User::getAge))),ArrayList::new));
}
}
2.HashMap去重
HashMap,从名字其实就可以看出,他是基于哈希进行数据存储的,而上面TreeSet很明显就是基于树结构。
HashMap底层是数组+链表+红黑树,要求加入到HashMap的元素必须要重写hashcode和equals方法。
因为在进行比较的时候会先通过对key的hashcode进行取模获得数组下标。然后访问数组获得链表或者树的头结点,之后遍历,通过对象地址比较,hashcode比较,equals比较等最终判断是否是同一个对象。(判断对象相同要求hashcode一样,并且equals比较也要返回true)
可以通过创建一个HashMap,使用想要作为去重标志的多个字段拼接起来作为key,value存储对象,从而实现数据去重。
public class Test {
public static void main(String[] args) {
List<User> list=new ArrayList<>();
Map<Integer,User> userMap=new HashMap<>();
for (User user:list){
userMap.put(user.getAge(),user);
}
Collection<User> values = userMap.values();
}
}
3.使用循环+逻辑判断去重
就是直接遍历数据,把需要去重的条件记录下来,然后用另一个列表来存储去重后的数据。
这是最简单易懂的写法,通俗易懂,不过也比较繁琐,一些简单的根据某些字段去重,使用上面的方法会更简洁。
public class Test {
public static void main(String[] args) {
List<User> list=new ArrayList<>();
List<User> distinctUserList=new ArrayList<>();
HashSet<Integer> ageSet=new HashSet<>();
for (User user:list){
if(!ageSet.contains(user.getAge())){
distinctUserList.add(user);
ageSet.add(user.getAge());
}
}
}
}
结论
上述方案其实可以大概归纳为使用Tree结构和使用Hash结构的不同。因为Set在底层上本质用的还是对应的Map。
树结构通过比较器判断元素是否重复,hash通过hashcode和equals比较是否重复。
如果了解流的使用,推荐使用有参的TreeSet+流操作方案实现去重。(推荐)
public class Test {
public static void main(String[] args) {
List<User> list=new ArrayList<>();
List<User> distinctList=list.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(()->new TreeSet<>(Comparator.comparing(User::getAge))),ArrayList::new));
}
}