Set针对复杂对象去重问题

Stella981
• 阅读 688

Set针对复杂对象去重问题

​ 在项目中我们经常使用set,因其可以去重特性,平时使用较多的是基础数据类型,Set, Set等,这些在使用中都没碰到什么问题。最近在项目中碰到自定义对象去重,用后

创建的对象去覆盖set中type相同的对象,于是想到Set这个集合类型,并且重写了自定义对象的equals()和hashCode()方法,但调试阶段发现结果并非所想。

​ 以下代码是自定义的Bean:

@AllArgsConstructor
@ToString
public class Foo implements Serializable {

    private static final long serialVersionUID = -3968061057233768716L;
    @Getter
    @Setter
    private int type;

    @Getter
    @Setter
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Foo foo = (Foo) o;
        return type == foo.type;
    }

    @Override
    public int hashCode() {
        return Objects.hash(type);
    }
}

​ 接下来写个单元测试吧

public class SetTest {

    @Test
    public void test1() {
        Set<Foo> set = new HashSet<>();
        Foo foo = new Foo(1, "1");
        set.add(foo);
        foo = new Foo(2, "2");
        set.add(foo);
        foo = new Foo(1, "3");
        set.add(foo);
        // 输出结果是什么
        System.out.println("set = " + set);

    }

​ 第一感觉会输出什么,set.size()=2是毫无疑问的,重点是type=1的对象对应的name是1还是3?当时我用脑子意想出来的name=3。可能会有童鞋认为输出的结果name=1,恭喜你答对了,确实

是name=1。

set = [Foo(type=1, name=1), Foo(type=2, name=2)]

​ 为什么不是3呢,3呢,3呢?接下来我们分析下,我们都知道当我们向HashMap中放入相同key和不同val放入到map中,map会保留最新的val(hashMap源码分析网上有很多解析,这里不再赘

述了,自行Google下),可见map其实也是保证key唯一的,不会出现key对应多个val的值(限:jdk中自带HashMap,Guava中Multimap是可以key对应多个val的,get(key)时返回的是val的集

合),HashSet就是利用HashMap中key唯一不重复的特性来实现的。

​ jdk1.8源码

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }
    
    -----------add部分 begin--------------------
    
     /**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element <tt>e</tt> to this set if
     * this set contains no element <tt>e2</tt> such that
     * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns <tt>false</tt>.
     *
     * @param e element to be added to this set
     * @return <tt>true</tt> if this set did not already contain the specified
     * element
     */
    // 其实这里是有返回值的
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    -----------add部分 end--------------------



调用add()实际就是调用HashMap的put方法,注意此时put的val是固定式<font color="red">**PRESENT**</font>是final修饰的一个对象,针对所有key都一样,因为我们是set所有val对我们来说没啥用。但是我们注意到其

实add()方法是有返回值的,当调用map.put()方法返回的对象是null的时候则代表新增成功,set中以前不存在此元素。当返回的对象not null的时候代表set中有此对象,而我们关注的其实只是key,

并不关注这个val。

  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

-----------
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 重点在这里
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 当我们第二次加入type=1时代码肯定就走到了这里,return oldValue其实也就是returun我们传入的PRESENT not nul
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

​ 重点在这里

​ 重点在这里

​ 重点在这里

if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

​ hash相等是肯定的,因为我们重写了Foo对象的hashCode方法,只跟type有关,所有p.hash==hash为true,大括号中(k = p.key) == key部门为false因为对象地址不同,但是

key != null && key.equals(k)是为true的,因为重写了equals()方法也只跟type有关,都为1,所以e = p了,流程判断语句结束,return一个not null的value。其实putVal()流程控制来看,只要我们的key

是存在的都会返回一个not null的val,而新插入的key返回值都是null。

​ 代码调试截图走一波

Set针对复杂对象去重问题

最后没办法先remove()后add()。。。

注:代码中使用了lombok插件,非常好用哈哈,墙裂推荐。一个@Data搞定所有,不要告诉我idea代码自动生成,想一下,当你开发代码时给对象增加一个属性是不是还要给新属性生成get和set方法,还要toString()是不是也要改,而你加了@Data属性,这些都不需要考虑了。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
2年前
Redis都有哪些数据类型
string这是最基本的类型了,就是普通的set和get,做简单的kv缓存hash这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,可以操作hash里的某个字段。key150value{“id”:
Wesley13 Wesley13
2年前
oracle游标的例子
declare    cursor ca is select id_no, name from user where ym201401;begin    for cb in ca loop        update path set enamecb.name where id_nocb.id
Stella981 Stella981
2年前
List stream 对象 属性去重
单值去重不写了,记录对象去重随手一个对象:@Data@AllArgsConstructorpublicclassMilk{privateIntegerkey;privateStringvalue;}操作:packagecom.yus.util;
Stella981 Stella981
2年前
ES6的Set()方法实现数组去重
letarr1,1,2,5,2;lets1newSet();s1.add(arr0);s1.add(arr1);s1.add(arr2);s1.add(arr3);s1.add(arr4);letarr1;
Wesley13 Wesley13
2年前
Java方面技术点小整理
Java中的集合吗?java中的集合分为value、keyvalueg两种存储值有分为list和setList有序的,可以重复Set是序的,不可以重复的根据equals和hashCode判断如果一个对象要存储在set中,必须重写equals和hashCode的方法;存储keyvalue的为map
Wesley13 Wesley13
2年前
Java容器——Set和顺序存储
  当Set使用自己创建的类型时,存储的顺序如何维护,在不同的Set实现中会有不同,而且它们对于在特定的Set中放置的元素类型也有不同的要求:Set(interface)存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set和Collection具有完全一样的接口,但S
3A网络 3A网络
1年前
Redis 存储对象信息是用 Hash 还是 String
Redis存储对象信息是用Hash还是StringRedis内部使用一个RedisObject对象来表示所有的key和value,RedisObject中的type,则是代表一个value对象具体是何种数据类型,它包含字符串(String)、链表(List)、哈希结构(Hash)、集合(Set)、有序集合(Sortedset)。
京东云开发者 京东云开发者
4个月前
为啥不建议用BeanUtils.copyProperties拷贝数据 | 京东云技术团队
在实际的业务开发中,我们经常会碰到VO、BO、PO、DTO等对象属性之间的赋值,当属性较多的时候我们使用get,set的方式进行赋值的工作量相对较大,因此很多人会选择使用spring提供的拷贝工具BeanUtils的copyProperties方法完成对象