【JavaSE-07】:集合(2):Map接口(含源码分析)

二、Map接口

Map 接口的继承树:
在这里插入图片描述

1.Map 接口的特点(JDK8)

  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value ;
  • Map 中的 key 和 value 都可以是任何引用类型的数据 ,会封装到 HashMap$Node对象中;
  • Map 中的 key 用Set来存放,不允许重复(原因和HashSet一样),即同一个 Map 对象所对应 的类,须重写hashCode()和equals()方法 ;
  • Map 中的 Value 可以重复;
  • Map 中的key 可以为null ,value 也可以为null ,注意key 为null ,只能有一个, value 为null 可以为多个;
  • 常用String类作为Map的“键” ;
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到 唯一的、确定的 value ;
  • Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和 Properties。其中,HashMap是 Map 接口使用频率最高的实现类。

由于 HashMap$ Node 实现了 HashMap$Entry 接口,也可以说 一对 k-v 就是一个 Entry 。

 static class Node<K, V> implements Entry<K, V>{}

添加元素时,底层通过创建 HashMap$Node 类的对象实现,同时为了方便遍历,还创建了一个 EntrySet集合,该集合存放的数据是 Entry类型的,一个Entry 对象都有一个 k,v,EntrySet< Entry< K,V>> 即: transient Set<Entry<K, V>> entrySet;

newNode(hash, key, value, (HashMap.Node)null);
public class MapSource {
    public static void main(String[] args) {

        Map map= new HashMap();
        map.put("No1","Tom");

        Set set = map.entrySet();
        System.out.println(set.getClass());

        for (Object entry:set){
            System.out.println(entry.getClass());
            
			Map.Entry entry1 = (Map.Entry) entry;
            System.out.println(entry1.getKey()+"-"+entry1.getValue());
        }

    }
}

结果:
在这里插入图片描述
实际上,entrySet 里面存放的依然是 HashMap$Node 类型。使用Entry 原因在于其定义了一些方便遍历的方法,如 getKey、getValue等。
但是,需要注意的是,在底层并没有创建一块新的内存,复制存放相同的数据,Entry 指向的是同一数据。
分析如下:

public class MapSource {
    public static void main(String[] args) {

        Map map= new HashMap();
        map.put("No1","Tom");
        map.put("No2",new Car("特斯拉"));
    }
}
class Car{
    private String name;

    public Car(String name) {
        this.name = name;
    }
}

结果:它们的地址是相同的,因此是同一个对象。可以看作是,entrySet 存放的是 Node元素的引用。
在这里插入图片描述

2.Map接口 常用方法

● 添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据

● 元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等

● 元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合

3.Map接口 六种遍历方式

public class MapFor {
    public static void main(String[] args) {

        Map map = new HashMap();
        map.put("张杰","谢娜");
        map.put("邓超","孙俪");
        map.put("吴亦凡",null);
        map.put(null,"迪丽热巴");
        map.put("黄晓明","杨颖");
   
    }
}

(1)方式一:先取出所有的 key ,再根据 key 获取对应的 Value
① 使用 增强for 遍历
② 使用 iterator 迭代器遍历

 Set keyset = map.keySet();

        //使用增强for
        System.out.println("第一种方式:");
        for(Object key:keyset){
            System.out.println(key+"-"+map.get(key));
        }
        //使用迭代器
        System.out.println("第二种方式:");
        Iterator iterator1 = keyset.iterator();
        while (iterator.hasNext()) {
            Object key = iterator.next();
            System.out.println(key+"-"+map.get(key));
        }

(2)方式二:直接取出所有的 value
③ 使用 增强for 遍历
④ 使用 iterator 迭代器遍历

Collection values = map.values();

        System.out.println("第三种方式:");
        for (Object value:values){
            System.out.println(value);
        }
        
        System.out.println("第四种方式:");
        Iterator iterator2 = values.iterator();
        while (iterator.hasNext()) {
            Object value = iterator.next();
            System.out.println(value);
        }

(3)方式三 : 通过 EntrySet 来获取 k-v
⑤ 使用 增强for 遍历
⑥ 使用 iterator 迭代器遍历
此处的 entrySet() 方法获取的 entrySet 集合中存放的是 Node 类型的数据,而Node 类型是 Map.Entry 接口的实现类 ,这个接口提供了方便我们遍历的 getKey() 、getValue() 等方法,因此此处先将 Node 向下转型为 Map.Entry

Set entrySet = map.entrySet();

        System.out.println("第五种方式:");
        for (Object entry:entrySet){
            //将entry转成 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey()+"-"+m.getValue());
        }

        System.out.println("第六种方式:");
        Iterator iterator3 = entrySet.iterator();
        while (iterator3.hasNext()) {
            Object entry = iterator3.next();
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey()+"-"+m.getValue());
        }

★ 4.Map实现类之一:HashMap ★

● HashMap是 Map 接口使用频率最高的实现类。
● 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
● 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写: equals()和hashCode()
● 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类 要重写:equals()
● 一个key-value构成一个entry
● 所有的entry构成的集合是Set:无序的、不可重复的
● HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true, hashCode 值也相等。
● HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
● HashMap 没有实现同步,因此是线程不安全的。

HashMap 的底层机制和源码剖析

扩容机制和 HashSet 相同。

底层机制:
1)HashMap 底层维护了 Node类型的数组table ,默认为 null ;
2)当创建对象时,将加载因子(loadfactor)初始化为0.75;
3)当添加 k-v 键值对时,通过 key 计算 哈希值得到在table 表中存放位置的索引。然后判断该索引位置是否有元素,如果没有元素直接添加。
如果该索引位置有元素,判断该元素的 key 和准备添加的 key是否相等,如果相等 ,则直接替换 value ;如果不相等,需要判断是树结构还是链表结构,做出相应的处理。如果添加时发现容量不够,则需要扩容。
4)在Java8中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认值为8),并且 table的大小 >= MIN_TREEIFY_CAPACITY(默认值64),就会进行树化(红黑树)

源码解析:

public class HashMapSource {
    public static void main(String[] args) {

        HashMap hashMap = new HashMap();

        hashMap.put("java",10);
        hashMap.put("php",10);
        hashMap.put("java",20); //添加时替换value
        System.out.println("hashMap="+hashMap);
    }
}

1.执行构造器 new HashMap() ,初始化加载因子 loadFactor =0.75,此时HashMap$Node[ ] table=null

 public HashMap() {
        this.loadFactor = 0.75F;
    }

在这里插入图片描述

2.执行 put 方法 ,
3.进一步执行 putVal 方法(原理和 HashSet中的分析一样)

 public V put(K key, V value) {
        return this.putVal(hash(key), key, value, false, true);
    }
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        HashMap.Node[] tab;  //定义了一些辅助变量,tab是HashMap 的一个数组,类型是Node
        int n;
        // if 语句判断当前tab 是null或者大小为空的情况,通过调用 resize() 方法,返回一个容量为16的数组,缓存极限为12
        if ((tab = this.table) == null || (n = tab.length) == 0) {
            n = (tab = this.resize()).length;
        }

        Object p;
        int i;
        /** (1)根据 key ,上一步(3)中调用 hash(key) 返回key对应的hash值 得到key应该存放到tab表的索引位置(即算法 i = n - 1 & hash ),
                将这个位置的对象,赋值给 p
         (2)此时判断 p 是否为 null 
              2.1 若为null,说明当前位置没有元素,则调用下面的方法,创建一个Node对象  Node(key=“java”,value=PRESENT,next=null)
                   HashMap.Node<K, V> newNode(int hash, K key, V value, HashMap.Node<K, V> next) {
                         //此处传入hash 是为了判断下一次添加时是否重复
                  		 return new HashMap.Node(hash, key, value, next);  
                    }
                    然后将该对象放到这个位置;
              2.2 若不为空,见下
        */
      
        if ((p = tab[i = n - 1 & hash]) == null) {
            tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
        } else {
            Object e;
            Object k;
            /** 在 p 不为空的情况下,
                一、如果当前索引位置对应的链表中第一个元素和准备添加的元素hash值相同 ((HashMap.Node)p).hash == hash
                      并且满足一下条件之一时,都不能加入
                      1)准备添加的key 和 p 指向的Node节点的 key是同一个对象
                      2) p 指向的Node节点的 key使用equals 方法和准备加入的 key比较后相同
           */
            if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
                e = p;
            // 二、然后判断 p 是否是一棵红黑树,如果是一棵红黑树,则调用 putTreeVal 进行添加(细节较复杂,此处不详述)
            } else if (p instanceof HashMap.TreeNode) {
                e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
            } else {
                int binCount = 0;
               
               /**三、以上情况都不满足时,需要使用循环依次和当前索引位置对应的链表中的元素逐一比较
                         (1)循环结束后,如果不和其中的元素相同,则添加到链表的最后
                             注意: 添加新的元素后,立即判断 此链表是否已经达到8个节点,
                              是就调用 treeifyBin 方法对当前链表树化,在转化为红黑树时,会进行判断:
                              tab != null && (n = tab.length) >= 64,满足时,先进行table扩容,
                              只有不满足才进行树化
                         (2)循环过程中,发现有相同的元素时,就break跳出
                */
                while(true) {
                    if ((e = ((HashMap.Node)p).next) == null) { //此处是判断整个链表没有和该value 相同时,就添加到最后
                        ((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
                        if (binCount >= 7) {  //此处是添加后判断当前链表的的个数,是否到达8个,是就进行树化
                            this.treeifyBin(tab, hash);
                        }
                        break;
                    }
                    // 此处是发现链表中有相同的元素,则break 不添加,只是替换该value
                    if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
                        break;
                    }

                    p = e;
                    ++binCount;
                }
            }

            if (e != null) {
                V oldValue = ((HashMap.Node)e).value; //当添加的元素key相同, value被替换为新的value
                if (!onlyIfAbsent || oldValue == null) {
                    ((HashMap.Node)e).value = value;
                }

                this.afterNodeAccess((HashMap.Node)e);  //空方法
                return oldValue;
            }
        }

        ++this.modCount;
        if (++this.size > this.threshold) {  //此处判断是否要扩容
            this.resize();
        }

        this.afterNodeInsertion(evict);//此方法在HashMap中是一个空方法,它主要是用来给HashMap的子类去实现这个方法,做一些操作
        return null;
    }


拓展:
HashMap 源码中重要的常量:
在这里插入图片描述

5.Map实现类之二:Hashtable

5.1 Hashtable 的基本介绍

1)Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap, Hashtable是线程安全的。
2)Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询 速度快,很多情况下可以互用。
3)与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value ;
4)与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序 ;
5)Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
HashTable 的底层数组 HashTable$Entry [ ] 初始化大小为 11
临界值为 threshold 为8 =11*0.75F

   public Hashtable() {
        this(11, 0.75F);
    }

5.2 Hashtable 的扩容机制

HashTable 使用 put() 方法添加元素,底层调用的是 addEntry() 方法

public synchronized V put(K key, V value) {
this.addEntry(hash, key, value, index);
            return null;
        }

addEntry() 方法 执行时,会判断元素个数是否超过阈值 threshold,超过会触发扩容机制 rehash()

private void addEntry(int hash, K key, V value, int index) {
        Hashtable.Entry<?, ?>[] tab = this.table;
        if (this.count >= this.threshold) {
            this.rehash();
            tab = this.table;
            hash = key.hashCode();
            index = (hash & 2147483647) % tab.length;
        }

        Hashtable.Entry<K, V> e = tab[index];
        tab[index] = new Hashtable.Entry(hash, key, value, e);
        ++this.count;
        ++this.modCount;
    }

扩容机制源码:
int newCapacity = (oldCapacity << 1) + 1;
扩容为原来的 2倍+1 的大小。

protected void rehash() {
        int oldCapacity = this.table.length;
        Hashtable.Entry<?, ?>[] oldMap = this.table;
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - 2147483639 > 0) {
            if (oldCapacity == 2147483639) {
                return;
            }

            newCapacity = 2147483639;
        }

5.3 Hashtable 和 HashMap 的对比:

在这里插入图片描述

6.Map实现类之三:LinkedHashMap

● LinkedHashMap 是 HashMap 的子类
● 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
● 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代 顺序:迭代顺序与 Key-Value 对的插入顺序一致

7.Map实现类之四:Properties

● Properties 类是 Hashtable 的子类,该对象用于处理属性文件 ;
● 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型 ;
● 存取数据时,建议使用 setProperty(String key,String value) 方法和 getProperty(String key) 方法。

Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);

8.Map实现类之五:TreeMap

● TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。 TreeMap 可以保证所有的 Key-Value 对处于有序状态。
● TreeSet底层使用红黑树结构存储数据
● TreeMap 的 Key 的排序:

  1. 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有 的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    final int compare(Object k1, Object k2) {
        return this.comparator == null ? ((Comparable)k1).compareTo(k2) : this.comparator.compare(k1, k2);
    }

需要 Key 类实现 Comparable 接口:

public interface Comparable<T> {
    int compareTo(T var1);
}
  1. 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口

● TreeMap判断两个key相等的标准:两个key通过compareTo()方法或 者compare()方法返回0。
代码示例:

public class TreeMapTest {
    public static void main(String[] args) {

        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o, Object t1) {
                //按照传入的 k(String) 的大小排序
//                return ((String)o).compareTo((String)t1);
                //照传入的 k(String) 长度的大小排序
                return ((String)o).length()-((String)t1).length();
            }
        });

        treeMap.put("Jack","杰克");
        treeMap.put("Mary","玛丽");
        treeMap.put("Tom","汤姆");
        treeMap.put("Alice","爱丽丝");

        System.out.println("treeMap="+treeMap);
    }
}

输出结果为:
这里的Jack 和Mary,在指定的 Compare 方法中,判断是相等的 key ,因此会被替换。

源码分析:
1)构造器,将传入的实现了Comparator接口的匿名内部类,传给了TreeMap 的comparator

public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

2)第一次使用 put 方法添加元素时,即 t == null 将 k-v 键值对封装到 Entry对象赋给 root;
3)此后添加时,会遍历所有的 key ,给当前的 key找到合适的位置,动态绑定到 comparator。
遍历过程中,按照Compare 方法, 如果 判断 key 相等,则使用新的 value 替换原来的。

    public V put(K key, V value) {
    //第一次添加元素时,将 k-v 键值对封装到 Entry对象赋给 root
        TreeMap.Entry<K, V> t = this.root;
        if (t == null) {
            this.compare(key, key);
            this.root = new TreeMap.Entry(key, value, (TreeMap.Entry)null);
            this.size = 1;
            ++this.modCount;
            return null;
        } else {
            Comparator<? super K> cpr = this.comparator;
            int cmp;
            TreeMap.Entry parent;
            if (cpr != null) {
                do {
                    parent = t;
                    cmp = cpr.compare(key, t.key);
                    if (cmp < 0) {
                        t = t.left;
                    } else {
                        if (cmp <= 0) {
                            return t.setValue(value); //判断 key 相等时,则使用新的 value 替换
                        }

                        t = t.right;
                    }
                } while(t != null);
            } else {
                if (key == null) {
                    throw new NullPointerException();
                }

                Comparable k = (Comparable)key;

                do {
                    parent = t;
                    cmp = k.compareTo(t.key);
                    if (cmp < 0) {
                        t = t.left;
                    } else {
                        if (cmp <= 0) {
                            return t.setValue(value);
                        }

                        t = t.right;
                    }
                } while(t != null);
            }

            TreeMap.Entry<K, V> e = new TreeMap.Entry(key, value, parent);
            if (cmp < 0) {
                parent.left = e;
            } else {
                parent.right = e;
            }

            this.fixAfterInsertion(e);
            ++this.size;
            ++this.modCount;
            return null;
        }
    }

★三、总结:如何选择集合实现类 ★

在实际开发中,主要取决于业务操作特点,根据集合实现类的特性选择,分析如下:
1)先判断存储的类型:一组对象 [单列] 或一组键值对 [双列]
2)一组对象 [单列] :Collection 接口
允许重复: List
增删多:LinkedList (底层维护了一个双向链表)
改查多:ArrayList (底层维护了 Object 类型的可变数组)
不允许重复时:
无序: HashSet (底层是 HashMap ,维护了一个哈希表,即:数组+链表+红黑树)
排序: TreeSet
插入和取出顺序一致: LInkedHashSet ,维护数组+双向链表
3)一组键值对 [双列] :Map
键无序:HashMap
键排序:TreeMMap
键插入和读取顺序一致: LinkedHashMap
读取文件: Properties

四、 Collections工具类

● Collections 是一个操作 Set、List 和 Map 等集合的工具类
● Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作, 还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

● 排序操作:(均为static方法)
reverse(List):反转 List 中元素的顺序
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

Collections常用方法

● 查找、替换

Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回 给定集合中的最大元素 Object min(Collection)
Object min(Collection,Comparator) int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

● Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集 合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
在这里插入图片描述

相关面试题:

1.负载因子值的大小,对HashMap有什么影响?
● 负载因子的大小决定了HashMap的数据密度。
● 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长, 造成查询或插入时的比较次数增多,性能会下降。
● 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
● 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此 时平均检索长度接近于常数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值