Java 集合框架之Map集合:HashMap、LinkedHashMap、HashTable、TreeMap集合


Map 集合框架 常用实现类

java.util.Map 双列集合接口 键值对

K 和 V 都是泛型,根据传递进来的值的类型所决定

特点:

  1. 键唯一,值不唯一,但一个键必须对应一个值
  2. 键和值的类型可相同也可不相同
  3. 无序

常用实现类:

  • HashMap 底层是Hash表(查询速度快)。 此实现提供了所有可选的地图操作,并允许null的值和null键。 ( HashMap类大致相当于Hashtable ,除了它是不同步的,并允许null)。这个类不能保证地图的顺序; 特别是,它不能保证在一段时间内保持不变。
  • LinkedHashMap 继承HashMap集合,因在Hash表的基础上,用了链表记录数据顺序,所以该类可保证元素的顺序。

1、Map 接口集合 JDK 1.2

Map接口定义了很多方法。

常用方法

  • public abstract V put(K key, V value) 把键值存入到map集合中,返回该键的值,第一次插入该key,返回null
  • public abstract V remove(Object key) 把指定的键所对应的键值对删除,返回被删除元素的值,不存在返回null
  • public abstract V get(Object key) 获取指定的键的值,不存在返回null
  • public abstract boolean containsKey(Object key) 判断集合中是否包含指定的键

Map集合的遍历:

  • public abstract Set keySet() 获取Map集合中的所有键,存储到Set集合中
  • public abstarct Set> entrySet() 获取Map集合中所有的键值对对象的集合(Set集合)

2、HashMap 集合 JDK 1.2

特点:

  1. key值不能重复
  2. 无序

Map map = new HashMap<>();

  1. 工作中不用hashmap。

  2. 该创建对象默认等于new HashMap<>(16,0.75);

    16为数组默认长度,0.75为默认负载因子(当元素填满数组的75%的时候,才扩容数组长度)

常用方法 eg中代码返回值细节

public class Test{
    public static void main(String[] args){
        showOne();
        showTwo();
    }
    
    public static void showOne(){
        Map<String,String> map = new HashMap<>();
        String s1 =  map.put("a","1");
        System.out.println(s1); // 输出 null 因为存放的key为a,在map中还没有a这个key,所以返回对应值就为null
        String s2 = map.put("a","2");
        System.out.println(s2); // 输出字符串 1 因为存放key为a,由于map中已经有a这个key了,key唯一,所以将旧value替换,将旧value值返回
    }
    
    public static void showTwo(){
        Map<String,Integer> map = new HashMap<>();
        map.put("a",1);
        map.put("b",2);
        map.put("c",3);
        Integer in1 = map.remove("b");
        System.out.println(in1); // 输出 2
        int in1 = map.remove("b");
        System.out.println(in1); // 抛出异常,因为在map中已经没有b这个key,所以会返回null,由于int类型不能接收null值,所以就会报错。建议使用对象类型接收返回值。 这里可以使用int是因为自动拆箱。
    }
    
    public static void showThree(){
        Map<String,Integer> map = new HashMap<>();
        map.put("a",1);
        map.put("b",2);
        map.put("c",3);
    }
}

keySet方法遍历

Java 集合框架之Map集合:HashMap、LinkedHashMap、HashTable、TreeMap集合_第1张图片

public calss Test{
    public static void main(String[] args){
        showOne();
    }
    
    public static void showOne(){
        Map<String,Integer> map = new HashMap<>();
        map.put("a",1);
        map.put("b",2);
        map.put("c",3);
        
        Set<String> set = map.keySet();
        // 迭代器遍历
        Iterator<String> it = set.iterator();
        while(it.hasNext()){
            Integer value = map.get(it.next());
            System.out.println(it.next()+" = "+value);
        }
        // 增强for循环遍历
        for(String s: map.keySet()){
            Integer value = map.get(s);
            System.out.println(s+" = "+value);
        }
    }
}

Entry键值对对象以及遍历Map

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

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

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

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

  • public Set> entrySet() 获取到Map中所有键值对对象的集合(Set集合)

    Map.Entry< K, V > 通过Map外部类找到内部类确定一个类型( Entry是Map的一个内部类 )
    Java 集合框架之Map集合:HashMap、LinkedHashMap、HashTable、TreeMap集合_第2张图片

public class Test{
    public static void main(String[] args){
        
    }
    
    public static void showForEntry(){
        Map<String,Integer> map = new HashMap<>();
        map.put("a",1);
        map.put("b",2);
        map.put("c",3);
        
        Set< Map.Entry<String,Integer> > set = map.entrySet();
        // 迭代器遍历
        Iterator< Map.Entry<String,Integer> > it = set.iterator();
        while(it.hasNext()){
            Map.Entry<String,Integer> entry = it.next();
            String s = entry.getKey();
            Integer i = entry.getValue();
            System.out.println(s+" = "+v); // 输出每一个键值对
        }
        // for增强循环
        for(Map.Entry<String,Integer> entry : set){
            String s = entry.getKey();
            Integer i = entry.getValue();
            System.out.println(s+" = "+v); // 输出每一个键值对
        }
    }
}

HashMap存储自定义类型键值

练习:每位学生(姓名,年龄)都有自己的家庭住址。将学生对象和家庭住址存储到map集合中。学生作为key,家庭作为value。

注意,学生的姓名和年龄都相同,则为同一名学生。

Student.java

public class Student{
    private String name;
    private int age;
    
    public Person() {
    }

    public Person(String name, String age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
    
    // 因为不能存入同一名学生,所以必须重写hashCode和equals方法
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name) &&
                Objects.equals(age, person.age);
    }

}

Test.java

public class Test{
    public static void main(String[] args){
        show();
    }
    
    public static void show(){
        Map<Student,String> map = new HashMap<>();
        map.put(new Student("a",18), "成都市武侯区");
        map.put(new Student("b",38), "成都市青羊区");
        map.put(new Student("c",28), "成都市武侯区");
        map.put(new Student("c",28), "成都市武侯区");
        
        Set< Map.Entry<Student,String> > set = map.entrySet();
        for(Map.Entry<Student,String> entry : set){
            Student stu = entry.getKey();
            String str = entry.getValue();
            System.out.println(stu+" --- "+str);
        }
    }
}

3、LinkedHashMap 集合 JDK 1.2

LinkedHashMap类继承了HashMap类,该类可以保证元素的存取的顺序。底层原理是,Hash表+链表(记录元素的顺序)。

特点:

  1. key值不能重复
  2. 存取有顺序
public class Test{
    public static void main(String[] args){
        test();
    }
    
    public static void test(){
        HashMap<String,String> hashMap = new HashMap<>();
        hashMap.put("a","1");
        hashMap.put("c","3");
        hashMap.put("b","2");
        System.out.println("hashMap:"+hashMap); // 输出结果是没有任何顺序的
        
        LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("a","1");
        linkedHashMap.put("c","3");
        linkedHashMap.put("b","2");
        System.out.println("linkedHashMap:"+linkedHashMap); // 输出结果与存入的顺序一致
    }
}

4、Hashtable 集合 JDK 1.0

线程安全

java.util.Hashtable 最早的双列集合。它与新集合实现不同, Hashtable是线程安全的集合,同步的(单线程)。键和值不能为null。在JDK1.2之后, 才改造为实现Map接口。

Hashtable存入null值,则会报异常:NullPointerException

public static void main(String[] args){
    HashMap<String,String> hashMap = new HashMap<>();
    hashMap.put(null,"a");
    hashMap.put("c","d");
    hashMap.put(null,null);
    System.out.println(hashMap); // 输出 {null=null,c=d}
    
    Hashtable<String,String> hashTable = new Hashtable<>();
    hashTable.put(null,"a");
    System.out.println(hashTable); // 报异常 NullPointerException 空指针异常
    //hashTable.put("b",null);
    //System.out.println(hashTable); // 报异常 NullPointerException 空指针异常
}

5、TreeMap 集合 JDK1.2

一个红黑树基于NavigableMap实现。该类不同步,线程不安全。

Java 集合框架之Map集合:HashMap、LinkedHashMap、HashTable、TreeMap集合_第3张图片

LinkedHashMap是按照插入顺序排序,而TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序。在实现原理上LinkedHashMap是双向链表,TreeMap是红黑树。TreeMap还有个好兄弟叫TreeSet,实现原理是一样的。


6、重要负载因子,数组长度在2的次方,当链表长度>=8时扩容成红黑树?

  • 负载因子

    当我们将负载因子不定为0.75的时候(两种情况):

    1、 假如负载因子定为1(最大值),那么只有当元素填满组长度的时候才会选择去扩容,虽然负载因子定为1可以最大程度的提高空间的利用率,但是会增加hash碰撞,以此可能会增加链表长度,因此查询效率会变得低下(因为链表查询比较慢)。hash表默认数组长度为16,好的情况下就是16个空间刚好一个坑一个,但是大多情况下是没有这么好的情况。

    结论:所以当加载因子比较大的时候:节省空间资源,耗费时间资源

    2、加入负载因子定为0.5(一个比较小的值),也就是说,直到到达数组空间的一半的时候就会去扩容。虽然说负载因子比较小可以最大可能的降低hash冲突,链表的长度也会越少,但是空间浪费会比较大。

    结论:所以当加载因子比较小的时候:节省时间资源,耗费空间资源

    但是我们设计程序的时候肯定是会在空间以及时间上做平衡,那么我们能就需要在时间复杂度和空间复杂度上做折中,选择最合适的负载因子以保证最优化。所以就选择了0.75这个值,Jdk那帮工程师一定是做了大量的测试,得出的这个值吧~

  • hash表的数组长度总在2的次方

    1:

    // WeakHashMap.java 源码:
    /**
    * Returns index for hash code h.
    */
    private static int indexFor(int h, int length) {
        return h & (length-1);
    }
    

    扩容也是以2的次方进行扩容,是因为2的次方的数的二进制是10…0,在二的次方数进行减1操作之后,二进制都是11…1,那么和hashcode进行与操作时,数组中的每一个空间都可能被使用到。

    如果不是2的次方,比如数组长度为17,那么17的二进制是10001,在indexFor方法中,进行减1操作为16,16的二进制是10000,随着进行与操作,很明显,地址二进制数末尾为1的空间,不会得到使用,比如地址为10001,10011,11011这些地址空间永远不会得到使用。因此就会造成大量的空间浪费。

    所以必须得是2的次方,可以合理使用数组空间。

    2:

    扩容临界值 = 负载因子 * 数组长度
    

    负载因子是0.75即3/4,又因为数组长度为2的次方,那么相乘得到的扩容临界值必定是整数,这样更加方便获得一个方便操作的扩容临界值。

  • 当链表长度>=8时构建成红黑树

Java 集合框架之Map集合:HashMap、LinkedHashMap、HashTable、TreeMap集合_第4张图片

利用泊松分布计算出当链表长度大于等于8时,几率很小很小

当put进来一个元素,通过hash算法,然后最后定位到同一个桶(链表)的概率会随着链表的长度的增加而减少,当这个链表长度为8的时候,这个概率几乎接近于0,所以我们才会将链表转红黑树的临界值定为8。

tips:了解红黑树,请移步至Java数据结构与算法:红黑树 AVL树.md


7、重要为什么jdk8,hashmap底层会用红黑树,而不使用AVL树?

首先需要了解什么是红黑树,什么是AVL树。请移步至Java数据结构与算法:红黑树 AVL树.md

红黑树和AVL树增删改查的时间复杂度平均和最坏情况都是在O(lgN),包括但不超过。

红黑树性质:

  1. 节点不是黑色就是红色
  2. 根节点必须为黑色
  3. 不能有两个连续红色节点
  4. 叶子节点是黑色
  5. 从根节点到叶子节点经过的黑节点数量相同

特点:最长路径不会超过最短路径的2倍。

AVL性质:

  1. 任何节点的两个子树的高度最大差别为1

在jdk8中hashmap的hash表桶中的链表长度大于8时,会将链表转为红黑树。虽然红黑树与AVL树的时间复杂度都为O(lgN),但是在调整树上面花费的时间相差很大。因为AVL树是平衡二叉树,要求严苛,任何节点的两个子树的高度最大差别为1,因此每次插入一个数或者删除一个数,最坏情况下,会使得AVL树进行很多次调整,为了保证符合AVL树的规则,调整时间花费较多。而红黑树,在时间复杂度上与AVL树相持平,但是在调整树上没有AVL树严苛,它允许局部很少的不完全平衡,但最长路径不会超过最短路径的2倍,这样以来,最多只需要旋转3次就可以使其达到平衡,调整时间花费较少。

最重要的一点,在JUC中有一个CurrentHashMap类,该类为线程同步的hashmap类,当高并发时,需要在意的是时间,由于AVL树在调整树上花费的时间相对较多,因此在调整树的过程中,其他线程需要等待的时间就会增长,这样导致效率降低,所以会选择红黑树。

总结:在增加、删除的时间复杂度相同的情况下,调整时间相对花费较少的是红黑树,因此选择红黑树。


8、重要既然红黑树那么好,为什么不一来就使用红黑树?

因为经过泊松定律知道,一个在负载因子为0.75时,出现的hash冲突,在一个桶中的链表长度大于8的几率是很少很少几乎为0,如果一来就使用红黑树,由于增删频繁,从而会调整树的结构,反而增加了负担,浪费时间,而直接使用链表增删反而比红黑树快很多,因此为了增加效率,而只是在长度大于8时使用红黑树。


练习:计算一个字符串每个字符出现的次数

Java 集合框架之Map集合:HashMap、LinkedHashMap、HashTable、TreeMap集合_第5张图片

public class Test{
    public static void mian(String[] args){
        forTest01();
    }
    
    public static void forTest01(){
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        
        char[] charArray = str.toCharArray();
        HashMap<Character,Integer> hashMap = new HashMap<>();
        for(int i = 0; i < charArray.length(); i++){
            char c = charArray.get(i);
            if( hashMap.containsKey(c) ){
                Integer in = hashMap.get(c);
                ++in;
                hashMap.put(c,in);
            }else{
                hashMap.put(c,1);
            }
        }
        
        Set<Map.Entry<Character,Integer>> set = hashMap.entrySet();
        for(Map.Entry<Character,Integer> entry : set){
            System.out.println(entry.getKey()+"---"+entry.getValue());
        }
    }
    
    public static void forTest02(){
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        HashMap<Character,Integer> hashMap = new HashMap<>();
        for(char c : str.toCharArray()){
            if( hashMap.containsKey(c) ){
                Integer in = hashMap.get(c);
                ++in;
                hashMap.put(c,in);
            }else{
                hashMap.put(c,1);
            }
        }
        
        for(Character key : hashMap.keySet()){
            Integer value = hashMap.get(key);
            System.out.println(key+"---"+value);
        }
    }
}

JDK9 的新特性之一 of()方法

JDK9的新特性:

List接口,Set接口,Map接口:里面增加了一个静态方法of,可以给集合一次性添加多个元素

当集合中存储的元素的个数已经确定了,要确定元素已经不能再改变的情况下使用。

注意:

  1. of方法只适用于List接口,Set接口,Map接口,不适用于接口的实现类
  2. of方法的返回值是一个不能改变的集合,集合不能再使用add、put方法添加元素,会抛出UnsupportedOperationException异常
  3. Set接口和Map接口调用of方法的时候,不能有重复的值或key值,否则会抛出IllegalArgumentException异常
List<String> list = List.of("1","2","3");
Set<String> set = Set.of("a","b","c");
Map<String,Integer> map = Map.of("a",1,"b",2,"c",3);

练习:斗地主发牌案例

Java 集合框架之Map集合:HashMap、LinkedHashMap、HashTable、TreeMap集合_第6张图片

package com.LFJava.doudizhu;

import java.util.*;

/**
 * Created with IntelliJ IDEA
 * User: heroC
 * Date: 2020/2/24
 * Time: 18:00
 * Description:
 * Version: V1.0
 */
public class Doudizhu {
    public static void main(String[] args) {
        // 准备牌的花色以及数字
        List<String> listType = new ArrayList<>();
        List<String> listNmuber = new ArrayList<>();
        Collections.addAll(listType,"♣","♦","♥","♠");
        Collections.addAll(listNmuber,"2","A","K","Q","J","10","9","8","7","6","5","4","3");

        // 将花色与数字拼接在一起,存入到map双列集合中
        Map<Integer,String> poker = new HashMap<>();
        int index = 0;
        poker.put(index,"大王");
        ++index;
        poker.put(index,"小王");
        ++index;
        for (String number : listNmuber) {
            for(String type : listType){
                poker.put(index, type + number);
                ++index;
            }
        }
        //System.out.println(poker);

        // 将map中的key值取出来,存入到list集合中,并将key值的顺序打乱
        List<Integer> numbers = new ArrayList<>();
        Set<Integer> set = poker.keySet();
        for (Integer nums: set) {
            numbers.add(nums);
        }
        Collections.shuffle(numbers);
        //System.out.println(numbers);

        // 将打乱的key值,存放到每个玩家的list集合中
        List<Integer> diPai = new ArrayList<>();
        List<Integer> player01 = new ArrayList<>();
        List<Integer> player02 = new ArrayList<>();
        List<Integer> player03 = new ArrayList<>();
        for (int i = 0; i < numbers.size() ; i++) {
            if( i >= 51){ // 一定要先将底牌存进去
                diPai.add(numbers.get(i));
            }else if( i % 3 == 0){
                player01.add(numbers.get(i));
            }else if( i % 3 == 1){
                player02.add(numbers.get(i));
            }else if( i % 3 == 2){
                player03.add(numbers.get(i));
            }
        }
        /*System.out.println(diPai);
        System.out.println(player01);
        System.out.println(player02);
        System.out.println(player03);*/

        // 发牌
        show("heroC",poker,player01);
        show("yikeX",poker,player02);
        show("Vincent",poker,player03);
        show("底牌",poker,diPai);

    }

    // 该方法,将每个玩家的存key值的list集合,先将key值排序,再将每个key值遍历出来,找到对应的map里的value值
    public static void show(String playerName, Map<Integer,String> poker, List<Integer> playerList){
        System.out.print(playerName+":");
        Collections.sort(playerList);
        for(Integer i : playerList){
            String s = poker.get(i);
            System.out.print("  "+s+"  ");
        }
        System.out.println();
    }
}

输出结果:

heroC: 大王 小王 ♦A ♠A ♣J ♥10 ♠10 ♦9 ♥9 ♣8 ♦7 ♣6 ♠6 ♣5 ♠5 ♣3 ♦3  
yikeX: ♣2 ♥2 ♦K ♣Q ♦Q ♥Q ♠Q ♦J ♥J ♦10 ♣9 ♠9 ♦8 ♦5 ♠4 ♥3 ♠3  
Vincent: ♦2 ♠2 ♣A ♥A ♥K ♠K ♠J ♣10 ♥8 ♣7 ♥7 ♠7 ♦6 ♥6 ♥5 ♦4 ♥4  
底牌: ♣K ♠8 ♣4 

你可能感兴趣的:(Java学习)