zoukankan      html  css  js  c++  java
  • Map

    主要内容

    • Map集合

    学习目标

    • [ ] 能够说出Map集合特点
    • [ ] 使用Map集合添加方法保存数据
    • [ ] 使用”键找值”的方式遍历Map集合
    • [ ] 使用”键值对”的方式遍历Map集合
    • [ ] 能够使用HashMap存储自定义键值对的数据
    • [ ] 了解HashMap底层原理
    • [ ] 能够使用HashMap编写斗地主洗牌发牌案例

    第一章 Map集合

    1.1 概述

    现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象,即java.util.Map接口。

    我们通过查看Map接口描述,发现Map接口下的集合与Collection接口下的集合,它们存储数据的形式不同,如下图。

    • Collection中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
    • Map中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
    • Collection中的集合称为单列集合,Map中的集合称为双列集合。
    • 需要注意的是,Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。

    1.2 Map常用子类

    通过查看Map接口描述,看到Map有多个子类,这里我们主要讲解常用的HashMap集合、LinkedHashMap集合。

    • HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
    • LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

    tips:Map接口中的集合都有两个泛型变量<K,V>,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量<K,V>的数据类型可以相同,也可以不同。

    1.3 Map接口中的常用方法

    Map接口中定义了很多方法,常用的如下:

    • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
    • public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
    • public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
    • boolean containsKey(Object key) 判断集合中是否包含指定的键。
    • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
    • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

    Map接口的方法演示

    public class MapDemo {
        public static void main(String[] args) {
            //创建 map对象
            HashMap<String, String>  map = new HashMap<String, String>();
    
            //添加元素到集合
            map.put("黄晓明", "杨颖");
            map.put("文章", "马伊琍");
            map.put("邓超", "孙俪");
            System.out.println(map);
    
            //String remove(String key)
            System.out.println(map.remove("邓超"));
            System.out.println(map);
    
            // 想要查看 黄晓明的媳妇 是谁
            System.out.println(map.get("黄晓明"));
            System.out.println(map.get("邓超"));    
        }
    }
    

    tips:

    使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;

    若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

    1.4 Map集合遍历键找值方式

    键找值方式:即通过元素中的键,获取键所对应的值

    分析步骤:

    1. 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法提示:keyset()
    2. 遍历键的Set集合,得到每一个键。
    3. 根据键,获取键所对应的值。方法提示:get(K key)

    代码演示:

    public class MapDemo01 {
        public static void main(String[] args) {
            //创建Map集合对象 
            HashMap<String, String> map = new HashMap<String,String>();
            //添加元素到集合 
            map.put("胡歌", "霍建华");
            map.put("郭德纲", "于谦");
            map.put("薛之谦", "大张伟");
    
            //获取所有的键  获取键集
            Set<String> keys = map.keySet();
            // 遍历键集 得到 每一个键
            for (String key : keys) {
              	//key  就是键
                //获取对应值
                String value = map.get(key);
                System.out.println(key+"的CP是:"+value);
            }  
        }
    }
    

    遍历图解:

    1.5 Entry键值对对象

    我们已经知道,Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)Entry将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。

    既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:

    • public K getKey():获取Entry对象中的键。
    • public V getValue():获取Entry对象中的值。

    在Map集合中也提供了获取所有Entry对象的方法:

    • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

    1.6 Map集合遍历键值对方式

    键值对方式:即通过集合中每个键值对(Entry)对象,获取键值对(Entry)对象中的键与值。

    操作步骤与图解:

    1. 获取Map集合中,所有的键值对(Entry)对象,以Set集合形式返回。方法提示:entrySet()

    2. 遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象。

    3. 通过键值对(Entry)对象,获取Entry对象中的键与值。 方法提示:getkey() getValue()

    public class MapDemo02 {
        public static void main(String[] args) {
            // 创建Map集合对象 
            HashMap<String, String> map = new HashMap<String,String>();
            // 添加元素到集合 
            map.put("胡歌", "霍建华");
            map.put("郭德纲", "于谦");
            map.put("薛之谦", "大张伟");
    
            // 获取 所有的 entry对象  entrySet
            Set<Entry<String,String>> entrySet = map.entrySet();
    
            // 遍历得到每一个entry对象
            for (Entry<String, String> entry : entrySet) {
               	// 解析 
                String key = entry.getKey();
                String value = entry.getValue();  
                System.out.println(key+"的CP是:"+value);
            }
        }
    }
    

    遍历图解:

    tips:Map集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。

    1.7 HashMap存储自定义类型键值

    练习:每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。

    注意,学生姓名相同并且年龄相同视为同一名学生。

    编写学生类:

    public class Student {
        private String name;
        private int age;
    
        public Student() {
        }
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            Student student = (Student) o;
            return age == student.age && Objects.equals(name, student.name);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    }
    

    编写测试类:

    public class HashMapTest {
        public static void main(String[] args) {
            //1,创建Hashmap集合对象。
            Map<Student,String>map = new HashMap<Student,String>();
            //2,添加元素。
            map.put(newStudent("lisi",28), "上海");
            map.put(newStudent("wangwu",22), "北京");
            map.put(newStudent("zhaoliu",24), "成都");
            map.put(newStudent("zhouqi",25), "广州");
            map.put(newStudent("wangwu",22), "南京");
            
            //3,取出元素。键找值方式
            Set<Student>keySet = map.keySet();
            for(Student key: keySet){
                Stringvalue = map.get(key);
                System.out.println(key.toString()+"....."+value);
            }
        }
    }
    
    • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。
    • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

    1.8 LinkedHashMap

    我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

    在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。

    public class LinkedHashMapDemo {
        public static void main(String[] args) {
            LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
            map.put("邓超", "孙俪");
            map.put("李晨", "范冰冰");
            map.put("刘德华", "朱丽倩");
            Set<Entry<String, String>> entrySet = map.entrySet();
            for (Entry<String, String> entry : entrySet) {
                System.out.println(entry.getKey() + "  " + entry.getValue());
            }
        }
    }
    

    结果:

    邓超  孙俪
    李晨  范冰冰
    刘德华  朱丽倩
    

    1.9 Map集合练习

    需求:

    计算一个字符串中每个字符出现次数。

    分析:

    1. 获取一个字符串对象
    2. 创建一个Map集合,键代表字符,值代表次数。
    3. 遍历字符串得到每个字符。
    4. 判断Map中是否有该键。
    5. 如果没有,第一次出现,存储次数为1;如果有,则说明已经出现过,获取到对应的值进行++,再次存储。
    6. 打印最终结果

    代码:

    public class MapTest {
    public static void main(String[] args) {
            //友情提示
            System.out.println("请录入一个字符串:");
            String line = new Scanner(System.in).nextLine();
            // 定义 每个字符出现次数的方法
            findChar(line);
        }
        private static void findChar(String line) {
            //1:创建一个集合 存储  字符 以及其出现的次数
            HashMap<Character, Integer> map = new HashMap<Character, Integer>();
            //2:遍历字符串
            for (int i = 0; i < line.length(); i++) {
                char c = line.charAt(i);
                //判断 该字符 是否在键集中
                if (!map.containsKey(c)) {//说明这个字符没有出现过
                    //那就是第一次
                    map.put(c, 1);
                } else {
                    //先获取之前的次数
                    Integer count = map.get(c);
                    //count++;
                    //再次存入  更新
                    map.put(c, ++count);
                }
            }
            System.out.println(map);
        }
    }
    

    第二章 HashMap详解

    2.1 什么是哈希表

    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

    给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

    比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。

    插入过程如下图:

    查找操作同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。

    哈希冲突

    然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀,但是我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

    2.2 HashMap的实现原理

    HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合)

    //HashMap的主干数组,可以看到就是一个Entry数组,初始值为空数组{},主干数组的长度一定是2的次幂。
    //至于为什么这么做,后面会有详细分析。
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    

    Entry是HashMap中的一个静态内部类。代码如下

        static class Entry<K,V> implements Map.Entry<K,V> {
        	final K key;
        	V value;
        	Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
        	int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
    
        	/**
         	* Creates new entry.
         	*/
        	Entry(int h, K k, V v, Entry<K,V> n) {
            	value = v;
            	next = n;
            	key = k;
            	hash = h;
        } 
    

    所以,HashMap的总体结构如下:

    简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以性能考虑,HashMap中的链表出现越少,性能才会越好

    其他几个重要字段:

    /**实际存储的key-value键值对的个数*/
    transient int size;
    
    /**阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
    threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到*/
    int threshold;
    
    /**负载因子,代表了table的填充度有多少,默认是0.75
    加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。
    所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
    */
    final float loadFactor;
    
    /**HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
    如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),
    需要抛出异常ConcurrentModificationException*/
    transient int modCount;
    

    HashMap有4个构造函数,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值。

    initialCapacity默认为16,loadFactory默认为0.75

    我们看下其中一个

    从上面这段代码我们可以看出,在常规构造函数中,没有为数组table分配内存空间(有一个参数为指定Map的构造函数例外),而是在执行put操作的时候才真正构建table数组

    OK,接下来我们来看看put操作的实现

    public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,
        //此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);//获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i);//新增一个entry
        return null;
    }
    

    inflateTable方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。

    private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
        /**此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,
        capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1 */
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
    

    roundUpToPowerOf2中的这段处理使得数组长度一定为2的次幂,Integer.highestOneBit是用来获取最左边的bit(其他bit位为0)所代表的数值。

     private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    

    hash函数

    /**这是一个神奇的函数,用了很多的异或,移位等运算
    对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀*/
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
    
        h ^= k.hashCode();
    
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    

    以上hash函数计算出的值,通过indexFor进一步处理来获取实际的存储位置。

    /**
     * 返回数组下标
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    

    h&(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为index=2。位运算对计算机来说,性能更高一些(HashMap中有大量位运算)

    所以最终存储位置的确定流程是这样的:

    再来看看addEntry的实现:

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
    
        createEntry(hash, key, value, bucketIndex);
    }
    

    通过以上代码能够得知,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。

    2.3 为何HashMap的数组长度一定是2的次幂?

    我们来继续看上面提到的resize方法

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
    
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    

    如果数组进行扩容,数组长度发生变化,而存储位置 index = h&(length-1),index也可能会发生变化,需要重新计算index,我们先来看看transfer这个方法

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
    	//for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去(数组不存储实际数据,所以仅仅是拷贝引用而已)
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                //将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    

    这个方法将老数组中的数据逐个链表地遍历,扔到新的扩容后的数组中,我们的数组索引位置的计算是通过 对key值的hashcode进行hash扰乱运算后,再通过和 length-1进行位运算得到最终数组索引位置。

    HashMap的数组长度一定保持2的次幂,比如16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。从下图可以我们也能看到这样会保证低位全为1,而扩容后只有一位差异,也就是多出了最左位的1,这样在通过 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致(大大减少了之前已经散列良好的老数组的数据位置重新调换)。

    还有,数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引index更加均匀

    我们看到,上面的&运算,高位是不会对结果产生影响的(hash函数采用各种位运算可能也是为了使得低位更加散列),我们只关注低位bit,如果低位全部为1,那么对于h低位部分来说,任何一位的变化都会对结果产生影响,也就是说,要得到index=21这个存储位置,h的低位只有这一种组合。这也是数组长度设计为必须为2的次幂的原因。

    如果不是2的次幂,也就是低位不是全为1此时,要使得index=21,h的低位部分不再具有唯一性了,哈希冲突的几率会变的更大,同时,index对应的这个bit位无论如何不会等于1了,而对应的那些数组位置也就被白白浪费了。

    get方法

     public V get(Object key) {
    	//如果key为null,则直接去table[0]处去检索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
    

    get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。我们再看一下getEntry这个方法

    final Entry<K,V> getEntry(Object key) {
            
        if (size == 0) {
            return null;
        }
        //通过key的hashcode值计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && 
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }    
    

    可以看出,get方法的实现相对简单,key(hashcode)–>hash–>indexFor–>最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。要注意的是,有人觉得上面在定位到数组位置之后然后遍历链表的时候,e.hash == hash这个判断没必要,仅通过equals判断就可以。其实不然,试想一下,如果传入的key对象重写了equals方法却没有重写hashCode,而恰巧此对象定位到这个数组位置,如果仅仅用equals判断可能是相等的,但其hashCode和当前对象不一致,这种情况,根据Object的hashCode的约定,不能返回当前对象,而应该返回null,后面的例子会做出进一步解释。

    2.4 重写equals方法需同时重写hashCode方法

    最后我们再聊聊老生常谈的一个问题,各种资料上都会提到,“重写equals时也要同时覆盖hashcode”,我们举个小例子来看看,如果重写了equals而不重写hashcode会发生什么样的问题

    public class MyTest {
    	private static class Person{
        	int idCard;
        	String name;
    
        	public Person(int idCard, String name) {
            	this.idCard = idCard;
            	this.name = name;
        	}
        	@Override
        	public boolean equals(Object o) {
            	if (this == o) {
                	return true;
            	}
            	if (o == null || getClass() != o.getClass()){
                	return false;
            	}
            	Person person = (Person) o;
            	//两个对象是否等值,通过idCard来确定
            	return this.idCard == person.idCard;
        	}
    
    	}
    	public static void main(String []args){
        	HashMap<Person,String> map = new HashMap<Person, String>();
        	Person person = new Person(1234,"乔峰");
        	//put到hashmap中去
        	map.put(person,"天龙八部");
        	//get取出,从逻辑上讲应该能输出“天龙八部”
        	System.out.println("结果:"+map.get(new Person(1234,"萧峰")));
    	}
    }
    
    
    实际输出结果:null
    

    如果我们已经对HashMap的原理有了一定了解,这个结果就不难理解了。尽管我们在进行get和put操作的时候,使用的key从逻辑上讲是等值的(通过equals比较是相等的),但由于没有重写hashCode方法,所以put操作时,key(hashcode1)–>hash–>indexFor–>最终索引位置 ,而通过key取出value的时候 key(hashcode1)–>hash–>indexFor–>最终索引位置,由于hashcode1不等于hashcode2,导致没有定位到一个数组位置而返回逻辑上错误的值null(也有可能碰巧定位到一个数组位置,但是也会判断其entry的hash值是否相等,上面get方法中有提到。)

    所以,在重写equals的方法的时候,必须注意重写hashCode方法,同时还要保证通过equals判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发生哈希冲突,应尽量避免)。

    2.5 JDK1.8中HashMap的性能优化

    假如一个数组槽位上链上数据过多(即拉链过长的情况)导致性能下降该怎么办?
    JDK1.8在JDK1.7的基础上针对增加了红黑树来进行优化。即当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

    第三章 模拟斗地主洗牌发牌

    3.1 案例介绍

    按照斗地主的规则,完成洗牌发牌的动作。

    具体规则:

    1. 组装54张扑克牌将
    2. 54张牌顺序打乱
    3. 三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。
    4. 查看三人各自手中的牌(按照牌的大小排序)、底牌

    规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3

    3.2 案例需求分析

    1. 准备牌:

    完成数字与纸牌的映射关系:

    使用双列Map(HashMap)集合,完成一个数字与字符串纸牌的对应关系(相当于一个字典)。

    1. 洗牌:

    通过数字完成洗牌发牌

    1. 发牌:

    将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。

    存放的过程中要求数字大小与斗地主规则的大小对应。

    将代表不同纸牌的数字分配给不同的玩家与底牌。

    1. 看牌:

    通过Map集合找到对应字符展示。

    通过查询纸牌与数字的对应关系,由数字转成纸牌字符串再进行展示。

    3.3 实现代码步骤

    public class Poker {
        public static void main(String[] args) {
            /*
             * 1组装54张扑克牌
             */
            // 1.1 创建Map集合存储
            HashMap<Integer, String> pokerMap = new HashMap<Integer, String>();
            // 1.2 创建 花色集合 与 数字集合
            ArrayList<String> colors = new ArrayList<String>();
            ArrayList<String> numbers = new ArrayList<String>();
    
            // 1.3 存储 花色 与数字
            Collections.addAll(colors, "♦", "♣", "♥", "♠");
            Collections.addAll(numbers, "2", "A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3");
            // 设置 存储编号变量
            int count = 1;
            pokerMap.put(count++, "大王");
            pokerMap.put(count++, "小王");
            // 1.4 创建牌 存储到map集合中
            for (String number : numbers) {
                for (String color : colors) {
                    String card = color + number;
                    pokerMap.put(count++, card);
                }
            }
            /*
             * 2 将54张牌顺序打乱
             */
            // 取出编号 集合
            Set<Integer> numberSet = pokerMap.keySet();
            // 因为要将编号打乱顺序 所以 应该先进行转换到 list集合中
            ArrayList<Integer> numberList = new ArrayList<Integer>();
            numberList.addAll(numberSet);
    
            // 打乱顺序
            Collections.shuffle(numberList);
    
            // 3 完成三个玩家交替摸牌,每人17张牌,最后三张留作底牌
            // 3.1 发牌的编号
            // 创建三个玩家编号集合 和一个 底牌编号集合
            ArrayList<Integer> noP1 = new ArrayList<Integer>();
            ArrayList<Integer> noP2 = new ArrayList<Integer>();
            ArrayList<Integer> noP3 = new ArrayList<Integer>();
            ArrayList<Integer> dipaiNo = new ArrayList<Integer>();
    
            // 3.2发牌的编号
            for (int i = 0; i < numberList.size(); i++) {
                // 获取该编号
                Integer no = numberList.get(i);
                // 发牌
                // 留出底牌
                if (i >= 51) {
                    dipaiNo.add(no);
                } else {
                    if (i % 3 == 0) {
                        noP1.add(no);
                    } else if (i % 3 == 1) {
                        noP2.add(no);
                    } else {
                        noP3.add(no);
                    }
                }
            }
    
            // 4 查看三人各自手中的牌(按照牌的大小排序)、底牌
            // 4.1 对手中编号进行排序
            Collections.sort(noP1);
            Collections.sort(noP2);
            Collections.sort(noP3);
            Collections.sort(dipaiNo);
    
            // 4.2 进行牌面的转换
            // 创建三个玩家牌面集合 以及底牌牌面集合
            ArrayList<String> player1 = new ArrayList<String>();
            ArrayList<String> player2 = new ArrayList<String>();
            ArrayList<String> player3 = new ArrayList<String>();
            ArrayList<String> dipai = new ArrayList<String>();
    
            // 4.3转换
            for (Integer i : noP1) {
                // 4.4 根据编号找到 牌面 pokerMap
                String card = pokerMap.get(i);
                // 添加到对应的 牌面集合中
                player1.add(card);
            }
    
            for (Integer i : noP2) {
                String card = pokerMap.get(i);
                player2.add(card);
            }
            for (Integer i : noP3) {
                String card = pokerMap.get(i);
                player3.add(card);
            }
            for (Integer i : dipaiNo) {
                String card = pokerMap.get(i);
                dipai.add(card);
            }
    
            //4.5 查看
            System.out.println("令狐冲:"+player1);
            System.out.println("石破天:"+player2);
            System.out.println("鸠摩智:"+player3);
            System.out.println("底牌:"+dipai);
        }
    }
    
  • 相关阅读:
    背水一战 Windows 10 (90)
    背水一战 Windows 10 (89)
    背水一战 Windows 10 (88)
    背水一战 Windows 10 (87)
    背水一战 Windows 10 (86)
    背水一战 Windows 10 (85)
    背水一战 Windows 10 (84)
    背水一战 Windows 10 (83)
    背水一战 Windows 10 (82)
    背水一战 Windows 10 (81)
  • 原文地址:https://www.cnblogs.com/p1ng/p/12257281.html
Copyright © 2011-2022 走看看