Android性能优化-HashMap

Java集合是我们使用最频繁的工具之一,但我们对它的理解仅限于使用上,而且大多数情况没有考虑过其使用规范。最近在使用阿里编程规范插件检查代码时会有一些关于HashMap使用方法纠正的提醒。那么该如何正确使用HashMap呢?

HashMap可能是我们使用最多的键值对型的集合类了,它的底层基于哈希表,采用数组存储数据,使用链表来解决哈希碰撞。在JDK1.8中还引入了红黑树来解决链表长度过长导致的查询速度下降问题。

HashMap内部是使用一个默认容量为16的数组来存储数据的,而数组中每一个元素却又是一个链表的头结点,所以,更准确的来说,HashMap内部存储结构是使用哈希表的拉链结构

HashMap的结构如下所示:

1696815-787f1bc62bc08c5c.png


性能问题-HashMap大小的问题:

 在android studio中使用阿里代码规范插件检查时会有一个关于HashMap初始化大小的检查,一般HashMap初始化大小都是2的指数幂。为什么说HashMap的实际大小总是2的指数幂?因为就算初始化的时候不是2的指数幂,roundUpToPowerOfTwo函数也会帮你转。为什么一定要是2的指数幂?这个是由于HashMap使用的散列算法,就是用key的hashCode转成对应的二进制,然后和HashMap的size-1座“&”操作。为什么这样做?举个例子,如果HashMap的设定大小为10,那么roundUpToPowerOfTwo转完大小是2^4 = 16,那么16-1=15,用二进制表示就是1111,此时如果一个实例的二进制哈希码为850873883(仅用来举例),二进制表示是110010101101110100111000011011,两者进行与运算,结果就是截取低四位1011,十进制就是11,也就是进来的key<->value的实体放在11这个位置上。试想如果HashMap的大小不是16而是10,10-1 = 9,二进制表示是1001,那么中间两个0的位置永远不会取到1,也就是2,3,4,6,5,7,这6个位置永远都都不会被算到实际填充的位置,空间利用率不足一半。这样的话就明白为什么用2的指数幂了:散列均匀。

那么问题来了,既然HashMap的实现已经帮我们做了这么多工作,我们是不是直接用就好了,不用管其他的了?明显不是。首先当HashMap的大小过小的时候,会增加Hash冲突的几率;另外如上面分析put方法说道的第三点,当当前的大小达到阀值(默认0.75*size),就会扩容,容量扩大为原来的两倍,扩容的过程会遍历原来的table,把它的元素重新计算在对应的新table中的位置,最坏时间复杂度为O(n^2);而在hash不冲突的场景下,不需要扩容的话,实际的时间复杂度为O(1)(只需要按照得到的index放进去)。所以我们最好给HashMap一个初始值,这个值是2的指数幂,并且它呈上装载因子(默认0.75)后的大小大于我们实际需要的大小。例如,我们实际需要200,那么200/0.75 约等于267,那么实际大于方向靠近267的2的指数幂为2^9 = 512。

性能问题-重写equals和hashCode方法:

首先明确这两者的关系: 
A和B对象equals方法返回true,hasCode方法返回值必然一样; 
A和B对象hashCode不一样,那么equals方法必须返回false。 
A和B对象hashCode一样,不能判定A equals B。 
所以equals方法返回true和hasCode方法返回值一样是充分非必要的关系。
从Collections.secondaryHash的方法看,最终散列的位置index是和key的hashCode有关的,如果key是引用类型对象,且没有重写hashCode,就会很容易出现hash冲突,在put的过程中,发生冲突就会沿着单链表遍历到最后并插入。这个时间复杂度也是O(n)。

同时,put和get的判定都有e.hash == hash && key.equals(e.key),如果不重写equals方法,默认用“==”判定,比较内存地址。如果key是引用对象,则必须是同一个引用才能判定是相同的对象。例如:

public class TestData {
    public String title;
    TestData(String title){
        this.title = title;
    }
    public static void main(String[] arg){
        HashMap<TestData,String> map = new HashMap<>(8);
        map.put(new TestData("title"),"title");
        String result = map.get(new TestData("title"));
        System.out.println("result="+result);
    }
}
输出的结果为null。所以不管是为了满足equals和hashCode充分非必要的关系,还是保障程序的健壮性,都应重写equals。