Java8 HashMap键与Comparable接口小结

 更新时间:2022年01月29日 10:28:59   作者:mitsuhide1992  
这篇文章主要介绍了Java8 HashMap键与Comparable接口小结,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

Java8 HashMap键与Comparable接口

最容易使 HashMap 发生哈希冲突的方法是什么呢?我们可以创建一个类,让它的哈希函数返回一个最糟糕的结果 —— 比如一个常数。

这也是我在面试的时候经常问面试者的问题

哈希方法返回常数会造成什么结果?有很多次面试者会回答说 map 集合里会有且仅有一个元素,因为 map 中的旧元素总会被新的覆盖。这个回答当然是错误的。

哈希冲突并不会导致 HashMap 覆盖一个已经存在于集合中的元素,这种情况只会在使用者试图向集合中放入两个元素,并且它们的键对于 equal() 方法是相等的时候才会发生。

键不相等但又会产生哈希冲突的不同元素最终会以某种数据结构存储在 HashMap 的同一个桶中(注意,在这种情况下,因为插入和查找的操作都要耗费更长的时间,所以整体的性能就会受到影响)。

首先,我们用一个小程序来模拟哈希冲突。下面的写法可能比较夸张,因为它造成的冲突比现实中多得多,但这个程序对于证实哈希冲突的条件还是很重要的。

我们使用一个 Person 对象作为 map 的键,以字符串作为值。下面是 Person 的具体实现,有一个 firstName 字段,一个 lastName 字段和一个 ID 属性,其中 ID 属性以 UUID 对象表示。

public class Person {
    private String firstName;
    private String lastName;
    private UUID id;
    public Person(String firstName, String lastName, UUID id) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.id = id;
    }
    @Override
    public int hashCode() {
        return 5;
    }
    @Override
    public boolean equals(Object obj) {
        // ... pertty good equals here taking into account the id field...
    }
    // ...
}

现在我们可以开始制造一些冲突。 

private static final int LIMIT = 500_000;
private void fillAndSearch() {
     Person person = null;
     Map<Person, String> map = new HashMap<>();
     for (int i=0;i<LIMIT;i++) {
        UUID randomUUID = UUID.randomUUID();
        person = new Person("fn", "ln", randomUUID);
        map.put(person, "comment" + i);
     }
     long start = System.currentTimeMillis();
     map.get(person);
     long stop = System.currentTimeMillis();
     System.out.println(stop-start+" millis");
}

上面的代码在一台高性能计算机上运行了两个半小时。其中,最后的查找操作耗费了大约 40 毫秒。现在我们对 Person 类进行修改:使它实现 Comparable 接口,并且添加了下面的方法:

@Override
public int compareTo(Person person) {
    return this.id.compareTo(person.id);
}

再一次运行之前的程序,这一次在我的机器上它耗费的时间少于 1 分钟。其中,最终的查找操作耗费的时间接近为 0 毫秒 —— 比之前提高了 150 倍!

就像前面说的,Java 8 做了很多优化,其中也包括HashMap 类。在 Java 7 中,两个不同的元素,如果它们的键产生了冲突,那么会被存储在同一个链表中。而从 Java 8 开始,如果发生冲突的频率大于某一个阈值(8),并且 map 的容量超过了另一个阈值(64),整个链表就会被转换成一个二叉树。

原来如此!所以,对于没有实现 Comparable 的键,最终的树是不平衡的;而对于实现了 Comparable 的键,其二叉树就会是高度平衡的。事实是这样吗?不是。HashMap 内部是红黑树,也就是说它总是平衡的。我通过反射机制,查看了最终的树结构。对于拥有 50000 个元素(不敢让数字更大了)的 HashMap 来说,两种不同的情况下(实现或是不实现 Comparable 接口)树的高度都是 19 。

那么,为什么之前的实验结果会有那么大的差别呢?原因在于,当 HashMap 想要为一个键找到对应的位置时,它会首先检查新键和当前检索到的键之间是否可以比较(也就是实现了 Comparable 接口)。如果不能比较,它就会通过调用 tieBreakOrder(Object a,Object b) 方法来对它们进行比较。这个方法首先会比较两个键对象的类名,如果相等再调用 System.identityHashCode 方法进行比较。这整个过程对于我们要插入的 500000 个元素来说是很耗时的。另一种情况是,如果键对象是可比较的,整个流程就会简化很多。因为键对象自身定义了如何与其它键对象进行比较,就没有必要再调用其他的方法,所以整个插入或查找的过程就会快很多。值得一提的是,在两个可比的键相等时(compareTo 方法返回 0)的情况下,仍然会调用 tieBreakOrder 方法。

总而言之,在 Java 8 的 HashMap 里,如果一个桶里存放了大量的元素,它在达到阈值时就会被转换为一棵红黑树,对于实现了 Comparable 接口的键来说,插入或删除的操作会比没有实现 Comparable 接口的键更简单。

通常,如果一个桶不会发生那么多次冲突的话,这整个机制不会带来多大的性能提升,但起码现在我们对 HashMap 的内部原理有了更多了解。

容器(LinkedList、HashMap、Compare)

HashSet的底层是HashMap,TreeSet的底层是TreeMap。

TreeSet中存放自定义类时应该先指定比较的规则(compare)

1.内部比较器|自然排序

要当前比较的类型实现一个借口Comparable接口,重写compareTo方法,方法的内部制定比较规则, 硬编码习惯,不够灵活,每次修改源代码。

public class Student implements Comparable<Student>{ 
    .........
    //重写compareTo方法
    @Override
    public int compareTo(Student o) {
        return this.stuName.length()-o.stuName.length();
    }
}

2.外部比较器|自定义排序

使用任何一个实现类实现一个接口Comparator,重写compare方法,方法的内部制定比较规则。

public class CompareDemo {
    public static void main(String[] args) {
        Comparator<Student> comparator=new Home();
        //lambda表达式
        comparator=(Student o1, Student o2)->{return o1.getStuName().length()-o2.getStuName().length();};
        //指定使用参数比较器
        //TreeSet(Comparator<? super E> comparator)
        TreeSet treeSet=new TreeSet(comparator);
        treeSet.add(new Student("张三爸",40,158));
        treeSet.add(new Student("张三",20,168));
        treeSet.add(new Student("张三爷爷",60,178));
        System.out.println(treeSet);
    }
}
class Home implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getStuName().length()-o2.getStuName().length();
    }
}

HashMap应该注意的事项 :

在这里插入图片描述

手写HashMap应该注意对key值进行比较,如果key相同则新的value覆盖旧的value。table是上图中横向的数组,默认初始值为16,计算key在数组中的位置时使用:key.hashcode%table.length,这种方式效率较低,一般使用 key.hashcode&(table.length-1)。

class MyHashMap{
    private Node [] table;
    private int size;
    public void put(Object key, Object value) {
        if(table==null){
            table=new Node[16];
        }
        int hash=getHash(key);
        Node node=table[hash];//获取他在数组上的位置
        Node newNode=new Node(hash,key,value,null);
        if(node==null){
            table[hash]=newNode;
            size++;
        }else{
            while(true){
                //进行key值的比较
                if(node.getKey().equals(key)){
                    node.setValue(value);
                    break;
                }
                if(node==null){
                    break;
                }
                node=node.getNext();
            }
            node.setNext(newNode);
            size++;
        }
    }
    public int getHash(Object key){
        return key.hashCode()&(table.length-1);
    }
    @Override
    public String toString() {
        StringBuilder sbr=new StringBuilder("{");
        for(Node node:table){
            while(node!=null){
                sbr.append(node.getKey()+"--->"+node.getValue()+",");
                node=node.getNext();
            }
        }
        sbr.setCharAt(sbr.length()-1,'}');
        return sbr.toString();
    }
}
  • Properties:用来代替hashMap,实现了Map接口,线程安全的
  • HashTable:可以代替hashMap,线程安全

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java中四种线程池的使用示例详解

    Java中四种线程池的使用示例详解

    这篇文章主要给大家介绍了关于Java中四种线程池的使用方法,四种线程池分别包括FixedThreadPool、CachedThreadPool、ScheduledThreadPool以及SingleThreadExecutor,文中给出了详细的示例代码供大家参考,需要的朋友们下面来一起看看吧。
    2017-08-08
  • 最简单的MyBatis Plus的多表联接、分页查询实现方法

    最简单的MyBatis Plus的多表联接、分页查询实现方法

    这篇文章主要介绍了最简单的MyBatis Plus的多表联接、分页查询实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • 三道java新手入门面试题,通往自由的道路--多线程

    三道java新手入门面试题,通往自由的道路--多线程

    这篇文章主要为大家分享了最有价值的3道多线程面试题,涵盖内容全面,包括数据结构和算法相关的题目、经典面试编程题等,对hashCode方法的设计、垃圾收集的堆和代进行剖析,感兴趣的小伙伴们可以参考一下
    2021-07-07
  • Mybatis延迟加载原理和延迟加载配置详解

    Mybatis延迟加载原理和延迟加载配置详解

    这篇文章主要介绍了Mybatis延迟加载原理和延迟加载配置详解,MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询,需要的朋友可以参考下
    2023-10-10
  • JavaEE通过response实现请求重定向

    JavaEE通过response实现请求重定向

    这篇文章主要介绍了JavaEE通过response实现请求重定向的方法,非常的简单实用,有需要的朋友可以参考下
    2014-10-10
  • java Socket UDP实例详解

    java Socket UDP实例详解

    这篇文章主要介绍了java Socket UDP实例详解的相关资料,需要的朋友可以参考下
    2017-02-02
  • 简单谈谈Java垃圾回收

    简单谈谈Java垃圾回收

    本文是看了James Gosling的<<Java程序设计语言>>后结合自己的一些项目经验,简单总结下关于java的垃圾回收问题的看法,有需要的小伙伴可以参考下
    2016-05-05
  • java编程实现求解八枚银币代码分享

    java编程实现求解八枚银币代码分享

    这篇文章主要介绍了java编程实现求解八枚银币代码分享,具有一定参考价值,需要的朋友可以了解下。
    2017-11-11
  • Java并发编程之volatile与JMM多线程内存模型

    Java并发编程之volatile与JMM多线程内存模型

    这篇文章主要介绍了Java并发volatile与JMM多线程内存模型,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05
  • 如何在java文件中设置文字颜色:setTextColor()

    如何在java文件中设置文字颜色:setTextColor()

    这篇文章主要介绍了如何在java文件中设置文字颜色:setTextColor(),文末补充介绍了在java代码中设置字体颜色方法总结,结合实例代码介绍的非常详细,需要的朋友可以参考下
    2023-09-09

最新评论