使用Set集合对List集合进行去重

时间:2023-01-02 17:53:27
使用Set集合对List集合进行去重

前段时间正好遇到这样一个需求:我们的支付系统从对方系统得到存储明细对象的List集合,存储的明细对象对象的明细类简化为如下TradeDetail类,需求是这样的,我要对称List集合进行去重,这里的去重的意思是只要对象对象中的accountNo账号是相同的,就认为明细对象是相同的,去重之后要求是List集合或者Set集合。

在进行上面的需求对象去重之前,先来看很简单的List集合去重:
package com.qdfae.jdk.collections;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

import org.junit.Test;

import com.qdfae.jdk.domain.TradeDetail;
import com.qdfae.jdk.domain.User;

/**
* 使用Set集合对List集合进行去重
*
* @author hongwei.lian
* @date 2018年3月9日 下午11:15:52
*/
public class SetTest {
/**
* List集合的泛型为Integer类型
*
* @author hongwei.lian
* @date 2018年3月9日 下午11:32:53
*/
@Test
public void testListToSet1() {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(1);
Set<Integer> set = new HashSet<>(list);
System.out.println("list的个数为:" + list.size() + "个");
list.forEach(System.out::println);
System.out.println("set的个数为:" + set.size() + "个");
set.forEach(System.out::println);
}
/**
* List集合的泛型为String类型
*
* @author hongwei.lian
* @date 2018年3月9日 下午11:34:15
*/
@Test
public void testListToSet2() {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("a");
Set<String> set = new HashSet<>(list);
System.out.println("list的个数为:" + list.size() + "个");
list.forEach(System.out::println);
System.out.println("set的个数为:" + set.size() + "个");
set.forEach(System.out::println);
}

/**
* List集合的泛型为自定义类型User
* 需求是userCode一样的便是同一个对象
*
* @author hongwei.lian
* @date 2018年3月10日 上午12:32:12
*/
@Test
public void testListToSet3() {
List<User> list = new ArrayList<>();
list.add(new User(1,"用户一","600001"));
list.add(new User(2,"用户二","600002"));
list.add(new User(3,"用户一","600001"));
list.add(new User(4,"用户一","600001"));
Set<User> set = new HashSet<>(list);
System.out.println("list的个数为:" + list.size() + "个");
list.forEach(System.out::println);
System.out.println("set的个数为:" + set.size() + "个");
set.forEach(System.out::println);
}
}
上面测试使用到的User类源码:
package com.qdfae.jdk.domain;

import java.io.Serializable;

/**
* User实体类
*
* @author hongwei.lian
* @date 2018年3月10日 上午12:33:22
*/
public class User implements Serializable {
private static final long serialVersionUID = -7629758766870065977L;

/**
* 用户ID
*/
private Integer id;
/**
* 用户姓名
*/
private String userName;
/**
* 用户代码
*/
private String userCode;
public User() {}
public User(Integer id, String userName, String userCode) {
this.id = id;
this.userName = userName;
this.userCode = userCode;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getUserCode() {
return userCode;
}

public void setUserCode(String userCode) {
this.userCode = userCode;
}

@Override
public String toString() {
return "User [id=" + id + ", userName=" + userName + ", userCode=" + userCode + "]";
}
}
依次运行上面三个方法的结果是:
testListToSet1()方法结果:
使用Set集合对List集合进行去重
testListToSet2()方法结果:
使用Set集合对List集合进行去重
testListToSet3()方法结果:
使用Set集合对List集合进行去重
上面的testListToSet1()方法和testListToSet2()方法可以去重,那为什么testListToSet3()方法就不能去重呢?仔细想想就会知道,两个对象的地址值不一样,怎么会认为是相同的去重呢,再往深处想,就会想到Object类的hashCode()方法和equals()方法,这两个方法决定了两个对象是否相等。Integer类和String类之所以可以进行去重,是因为这两个类都重写了父类Object类中的hashCode()方法和equals()方法,具体的代码可以去查看JDK源码,这里不再赘述。到这里我们就知道User对象不能去重的原因所在,那么我们根据需求在User类中重写hashCode()方法和equals()方法,重写后的User类源码如下:
package com.qdfae.jdk.domain;

import java.io.Serializable;

/**
* User实体类
*
* @author hongwei.lian
* @date 2018年3月10日 上午12:33:22
*/
public class User implements Serializable {
private static final long serialVersionUID = -7629758766870065977L;

/**
* 用户ID
*/
private Integer id;
/**
* 用户姓名
*/
private String userName;
/**
* 用户代码
*/
private String userCode;
public User() {}
public User(Integer id, String userName, String userCode) {
this.id = id;
this.userName = userName;
this.userCode = userCode;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getUserCode() {
return userCode;
}

public void setUserCode(String userCode) {
this.userCode = userCode;
}
/**
* 针对userCode重写hashCode()方法
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((userCode == null) ? 0 : userCode.hashCode());
return result;
}

/**
* 针对userCode重写equals()方法
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (userCode == null) {
if (other.userCode != null)
return false;
} else if (!userCode.equals(other.userCode))
return false;
return true;
}

@Override
public String toString() {
return "User [id=" + id + ", userName=" + userName + ", userCode=" + userCode + "]";
}
}
我们再次运行testListToSet3()方法结果:
使用Set集合对List集合进行去重
这一次符合我们的需求,接下里再来看开头提出的需求。
准备:
TradeDetail类源码:
package com.qdfae.jdk.domain;

import java.io.Serializable;
import java.math.BigDecimal;

/**
* 交易明细
*
* @author hongwei.lian
* @date 2018年3月10日 下午2:44:35
*/
public class TradeDetail implements Serializable {
private static final long serialVersionUID = 3386554986241170136L;

/**
* 交易明细主键
*/
private Integer id;
/**
* 账号
*/
private String accountNo ;
/**
* 账户名称
*/
private String accountName;
/**
* 交易金额(+表示入金,-表示出金)
*/
private BigDecimal balance;

public TradeDetail() {}
public TradeDetail(Integer id, String accountNo, String accountName, BigDecimal balance) {
this.id = id;
this.accountNo = accountNo;
this.accountName = accountName;
this.balance = balance;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}
public String getAccountNo() {
return accountNo;
}

public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}

public String getAccountName() {
return accountName;
}

public void setAccountName(String accountName) {
this.accountName = accountName;
}

public BigDecimal getBalance() {
return balance;
}

public void setBalance(BigDecimal balance) {
this.balance = balance;
}
/**
* 针对accountNo重写hashCode()方法
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accountNo == null) ? 0 : accountNo.hashCode());
return result;
}

/**
* 针对accountNo重写equals()方法
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
TradeDetail other = (TradeDetail) obj;
if (accountNo == null) {
if (other.accountNo != null)
return false;
} else if (!accountNo.equals(other.accountNo))
return false;
return true;
}

@Override
public String toString() {
return "TradeDetail [id=" + id + ", accountNo=" + accountNo + ", accountName=" + accountName + ", balance="
+ balance + "]";
}

}
我们首先来按照上面的想法根据需求重写TradeDetail类的hashCode()方法和equals()方法,上面已经给出重写后的TradeDetail类。
我有三种实现方案如下:
package com.qdfae.jdk.collections;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.junit.Before;
import org.junit.Test;

import com.qdfae.jdk.domain.TradeDetail;

/**
* List集合去重
*
* @author hongwei.lian
* @date 2018年3月11日 下午8:54:57
*/
public class DuplicateListTest {
/**
* 存储没有去重的明细对象的List集合
*/
private List<TradeDetail> tradeDetailList;
/**
* 存储去重后的明细对象的List集合
*/
private List<TradeDetail> duplicateTradeDetailList;
/**
* 存储去重后的明细对象的Set集合
*/
private Set<TradeDetail> tradeDetailSet;
/**
* 初始化tradeDetailList
*
* @author hongwei.lian
* @date 2018年3月11日 下午9:04:45
*/
@Before
public void InitTradeDetailList() {
tradeDetailList = new ArrayList<>();
tradeDetailList.add(new TradeDetail(1, "600010", "账户一", new BigDecimal(100.00)));
tradeDetailList.add(new TradeDetail(2, "600011", "账户二", new BigDecimal(100.00)));
tradeDetailList.add(new TradeDetail(3, "600010", "账户一", new BigDecimal(-100.00)));
tradeDetailList.add(new TradeDetail(4, "600010", "账户一", new BigDecimal(-100.00)));
}

/**
* 使用Set接口的实现类HashSet进行List集合去重
*
* HashSet实现类
* 构造方法:
* public TreeSet(Comparator<? super E> comparator)
*
* @author hongwei.lian
* @date 2018年3月11日 下午9:37:51
*/
@Test
public void testDuplicateListWithHashSet() {
//-- 前提是TradeDetail根据规则重写hashCode()方法和equals()方法
tradeDetailSet = new HashSet<>(tradeDetailList);
tradeDetailSet.forEach(System.out::println);
}
/**
* 使用Map集合进行List集合去重
*
* @author hongwei.lian
* @date 2018年3月11日 下午9:05:49
*/
@Test
public void testDuplicateListWithIterator() {
duplicateTradeDetailList = new ArrayList<>();
Map<String, TradeDetail> tradeDetailMap = tradeDetailList.stream()
.collect(Collectors.toMap(
tradeDetail -> tradeDetail.getAccountNo(),
tradeDetail -> tradeDetail,
(oldValue, newValue) -> newValue));
tradeDetailMap.forEach(
(accountNo, tradeDetail) -> duplicateTradeDetailList.add(tradeDetail)
);
duplicateTradeDetailList.forEach(System.out::println);
//-- 参考文章
//http://blog.jobbole.com/104067/
//https://www.cnblogs.com/java-zhao/p/5492122.html
}
/**
* 使用Set接口的实现类TreeSet进行List集合去重
*
* TreeSet实现类
* 构造方法:
* public TreeSet(Comparator<? super E> comparator)
*
* @author hongwei.lian
* @date 2018年3月11日 下午9:37:48
*/
@Test
public void testDuplicateListWithTreeSet() {
tradeDetailSet = new TreeSet<>(
(tradeDetail1, tradeDetail2)
->
tradeDetail1.getAccountNo().compareTo(tradeDetail2.getAccountNo())
);
tradeDetailSet.addAll(tradeDetailList);
tradeDetailSet.forEach(System.out::println);
}

}
运行上面三个方法的结果都是:
使用Set集合对List集合进行去重
方案一:根据需求重写自定义类的hashCode()方法和equals()方法
这种方案的不足之处是根据需求重写后的hashCode()方法和equals()方法不一定满足其他需求,这样这个TradeDetail类的复用性就会相当差。
方案二:遍历List集合,取出每一个明细对象,将明细对象的accountNo属性字段作为Map集合key,明细对象作为Map集合的value,然后再遍历Map集合,得到一个去重后的List集合或者Set集合。
这种方案的不足之处是消耗性能,首先是List集合去重转换为Map集合,Map集合再次转换为List集合或者Set集合,遍历也会消耗性能。
方案三:使用TreeSet集合的独有的构造方法进行去重,如下:
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
这种方案目前为止是我使用的比较多的方案,不足之处暂时没有发现,TreeSet集合实际上是利用TreeMap的带有一个比较器参数的构造方法实现,看JDK源码很清晰,最重要的是这个参数Comparator接口,这个接口的源码:
Comparator接口部分源码:
@FunctionalInterface
public interface Comparator<T> {

int compare(T o1, T o2);

}
这个compare()方法需要自己根据需求去实现,仔细看上面去重的原理实际上还是使用String类的compareTo()方法,String类的compareTo()方法源码:
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;

int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
基本就想到了这些,如果有好的实现方法,自己还会补充。