Java中对List去重Stream去重的解决⽅法
问题
当下互联⽹技术成熟,越来越多的趋向去中⼼化、分布式、流计算,使得很多以前在数据库侧做的事情放到了Java端。今天有⼈问道,如果数据库字段没有索引,那么应该如何根据该字段去重?⼤家都⼀致认为⽤Java来做,但怎么做呢?
解答
忽然想起以前写过list去重的⽂章,出来⼀看。做法就是将list中对象的hashcode和equals⽅法重写,然后丢到HashSet⾥,然后取出来。这是最初刚学Java的时候像被字典⼀样背写出来的答案。就⽐如⾯试,⾯过号称做了3年Java的⼈,问Set和HashMap的区别可以背出来,问如何实现就不知道了。也就是说,初学者只背特性。但真正在项⽬中使⽤的时候你需要确保⼀下是不是真的这样。因为背书没⽤,只能相信结果。你需要知道HashSet如何帮我做到去重了。换个思路,不⽤HashSet可以去重吗?最简单,最直接的办法不就是每次都拿着和历史数据⽐较,都不相同则插⼊队尾。⽽HashSet只是加速了这个过程⽽已。
⾸先,给出我们要排序的对象User
@Data
@Builder
@AllArgsConstructor
public class User {
private Integer id;
private String name;
}
List<User> users = wArrayList(
new User(1, "a"),
new User(1, "b"),
new User(2, "b"),
new User(1, "a"));
⽬标是取出id不重复的user,为了防⽌扯⽪,给个规则,只要任意取出id唯⼀的数据即可,不⽤拘泥id相同时算哪个。
⽤最直观的办法
这个办法就是⽤⼀个空list存放遍历后的数据。
@Test
public void dis1() {
List<User> result = new LinkedList<>();
for (User user : users) {
boolean b = result.stream().anyMatch(u -> u.getId().Id()));
if (!b) {
result.add(user);
}
}
System.out.println(result);
}
⽤HashSet
背过特性的都知道HashSet可以去重,那么是如何去重的呢?再深⼊⼀点的背过根据hashcode和equals⽅法。那么如何根据这两个做到的呢?没有看过源码的⼈是⽆法继续的,⾯试也就到此结束了。
事实上,HashSet是由HashMap来实现的(没有看过源码的时候曾经⼀直直观的以为HashMap的key是HashSet来实现的,恰恰相反)。这⾥不展开叙述,只要看HashSet的构造⽅法和add⽅法就能理解了。
public HashSet() {
map = new HashMap<>();
}
/**
* 显然,存在则返回false,不存在的返回truesortedlist
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
那么,由此也可以看出HashSet的去重复就是根据HashMap实现的,⽽HashMap的实现⼜完全依赖于hashcode和equals⽅法。这下就彻底打通了,想⽤HashSet就必须看好⾃⼰的这两个⽅法。
在本题⽬中,要根据id去重,那么,我们的⽐较依据就是id了。修改如下:
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
/
/hashcode
result = 31 * result + (element == null ? 0 : element.hashCode());
其中, Objects调⽤Arrays的hashcode,内容如上述所⽰。乘以31等于x<<5-x。
最终实现如下:
@Test
public void dis2() {
Set<User> result = new HashSet<>(users);
System.out.println(result);
}
使⽤Java的Stream去重
回到最初的问题,之所以提这个问题是因为想要将数据库侧去重拿到Java端,那么数据量可能⽐较⼤,
⽐如10w条。对于⼤数据,采⽤Stream相关函数是最简单的了。正好Stream也提供了distinct函数。那么应该怎么⽤呢?
users.parallelStream().distinct().forEach(System.out::println);
没看到⽤lambda当作参数,也就是没有提供⾃定义条件。幸好Javadoc标注了去重标准:
Returns a stream consisting of the distinct elements
(according to {@link Object#equals(Object)}) of this stream.
我们知道,也必须背过这样⼀个准则:equals返回true的时候,hashcode的返回值必须相同. 这个在背的时候略微有些逻辑混乱,但只要了解了HashMap的实现⽅式就不会觉得拗⼝了。HashMap先根据hashcode⽅法定位,再⽐较equals⽅法。
所以,要使⽤distinct来实现去重,必须重写hashcode和equals⽅法,除⾮你使⽤默认的。
那么,究竟为啥要这么做?点进去看⼀眼实现。
<P_IN> Node<T> reduce(PipelineHelper<T> helper, Spliterator<P_IN> spliterator) {
/
/ If the stream is SORTED then it should also be ORDERED so the following will also
// preserve the sort order
TerminalOp<T, LinkedHashSet<T>> reduceOp
= ReduceOps.<T, LinkedHashSet<T>>makeRef(LinkedHashSet::new, LinkedHashSet::add,                          LinkedHashSet::addAll);
de(reduceOp.evaluateParallel(helper, spliterator));
}
内部是⽤reduce实现的啊,想到reduce,瞬间想到⼀种⾃⼰实现distinctBykey的⽅法。我只要⽤reduce,计算部分就是把Stream的元素拿出来和我⾃⼰内置的⼀个HashMap⽐较,有则跳过,没有则放进去。其实,思路还是最开始的那个最直⽩的⽅法。
@Test
public void dis3() {
users.parallelStream().filter(distinctByKey(User::getId))
.forEach(System.out::println);
}
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = wKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
当然,如果是并⾏stream,则取出来的不⼀定是第⼀个,⽽是随机的。
上述⽅法是⾄今发现最好的,⽆侵⼊性的。但如果⾮要⽤distinct。只能像HashSet那个⽅法⼀样重写hashcode和equals。
⼩结
会不会⽤这些东西,你只能去⾃⼰练习过,不然到了真正要⽤的时候很难⼀下⼦就拿出来,不然就冒险⽤。⽽若真的想⼤胆使⽤,了解规则和实现原理也是必须的。⽐如,LinkedHashSet和HashSet的实现有何不同。
附上贼简单的LinkedHashSet源码:
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
补充:
Java中List集合去除重复数据的⽅法
1. 循环list中的所有元素然后删除重复
public  static  List removeDuplicate(List list) {
for ( int i =  0 ; i < list.size() -  1 ; i ++ ) {
for ( int j = list.size() -  1 ; j > i; j -- ) {
if ((j).(i))) {
}
}
}
return list;
}
2. 通过HashSet踢除重复元素
public static List removeDuplicate(List list) {
HashSet h = new HashSet(list);
list.clear();
list.addAll(h);
return list;
}
3. 删除ArrayList中重复元素,保持顺序
// 删除ArrayList中重复元素,保持顺序
public static void removeDuplicateWithOrder(List list) {
Set set = new HashSet();
List newList = new ArrayList();
for (Iterator iter = list.iterator(); iter.hasNext();) {
Object element = ();
if (set.add(element))
newList.add(element);
}
list.clear();
list.addAll(newList);
System.out.println( " remove duplicate " + list);
}
4.把list⾥的对象遍历⼀遍,⽤ain(),如果不存在就放⼊到另外⼀个list集合中public static List removeDuplicate(List list){
List listTemp = new ArrayList();
for(int i=0;i<list.size();i++){
if(!(i))){
listTemp.(i));
}
}
return listTemp;
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。