HashSet和HashMap

Posted lyang-a

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashSet和HashMap相关的知识,希望对你有一定的参考价值。

HashMap

概念和特征

?概念:以键值对的形式存储数据,由键映射到值,核心在于Key上。
?特征:键不能重复,值可以重复;key-value允许为null

????HashMap ? SinceJDK1.2 ? 前身是HashTable(SinceJDK1.0)
????HashMap ? 实现了Map接口

??HashMap底层是一个Entry数组,当发生hash冲突(碰撞)的时候,HashMap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。

??Key不能重复,判断是否重复的标准是:hashCode()和equals()方法, 如果hashCode相同并且equals相等就是一个重复的key。

?注意:放入HashMap集合中的Key必须要覆盖Object类型的hashCode()和equals()方法,否则就会出现重复的Key。

技术图片

put()和get()方法

?put(K,V):将键值对存储到HashMap中(放入)??get(K):根据Key获取Value,如果Key在HashMap中不存在返回null。

??场景:创建HashMap对象,将元素放入HashMap,根据Key获取Value。

import java.util.HashMap;
import java.util.Map;

/**
 * 场景:创建HashMap对象,将元素放入HashMap,根据Key获取Value
 * 小结:HashMap的数据全部都放在{key1=value1,key2=value2...}中,以key=value的形式存放,如果有多个键值对使用逗号分隔
 * HashMap与ArrayList不同,新元素放入集合中是无序的(根据Hash算法排序的)。
 */
public class TestHashMap {
    public static void main(String[] args) {
        //创建HashMap对象,编译期是Map,运行期是HashMap
        Map<String,String> map = new HashMap<>();
        //调用put(k,v)方法将键值对放入集合  首都作为Key,国家作为Value
        map.put("北京","中国");
        map.put("伯尔尼","瑞士");
        map.put("哈瓦那", "古巴");
        map.put("太子港", "海地");
        map.put("科威特城","科威特");
        System.out.println(map);
        //HashMap 集合没有ABC的Key,所以返回结果为null
        //String value = map.get("ABC");
        //System.out.println(value);
        //海地
        String value = map.get("太子港");
        System.out.println(value);
    }
}

其他方法

??size():获取HashMap的实际元素大小。

??remove(key):根据Key删除元素。

??clear():清空HashMap所有元素,但是不会释放对象占用的内存。

??containsKey(key):判断可以是否在HashMap存在,如果存在返回true,否则返回false。

import java.util.HashMap;
import java.util.Map;

/**
 * 场景:创建HashMap对象,将元素放入HashMap,根据Key获取Value
 * 小结:HashMap的数据全部都放在{key1=value1,key2=value2...}中,以key=value的形式存放,如果有多个键值对使用逗号分隔
 * HashMap与ArrayList不同,新元素放入集合中是无序的(根据Hash算法排序的)。
 */
public class TestHashMap {
    public static void main(String[] args) {
        // 创建HashMap对象,编译期是Map,运行期是HashMap
        Map<String,String> map = new HashMap<>();
        // 调用put(k,v)方法将键值对放入集合  首都作为Key,国家作为Value
        map.put("北京","中国");
        map.put("伯尔尼","瑞士");
        map.put("哈瓦那", "古巴");
        map.put("太子港", "海地");
        map.put("科威特城","科威特");
        System.out.println(map);
        // HashMap 集合没有ABC的Key,所以返回结果为null
        // String value = map.get("ABC");
        // System.out.println(value);
        String value = map.get("太子港");      // 海地
        System.out.println(value);
    }
}

??keySet():会返回集合中所有的Key。

??values():返回集合中所有的值。

??entrySet():获取HashMap中所有的Entry集合。

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 获取HashMap所有的Value
 */
public class TestHashMap3 {
    public static void main(String[] args) {
        //创建HashMap对象,编译期是Map,运行期是HashMap
        Map<String,String> map = new HashMap<>();
        //调用put(k,v)方法将键值对放入集合  首都作为Key,国家作为Value
        map.put("北京","中国");
        map.put("伯尔尼","瑞士");
        map.put("哈瓦那", "古巴");
        map.put("太子港", "海地");
        map.put("科威特城","科威特");
        System.out.println(map.size());
        //Collection以及Collection下面的子类,数据以[]存储
        //Map以及Map下面的子类,数据以{}存储
        //[瑞士, 海地, 古巴, 科威特, 中国]
        //返回HashMap集合所有的Value
        Collection<String> values = map.values();
        System.out.println(values);
        //遍历Collection
        for(String value: values) {
            System.out.print(value+"	");
        }
    }
}

遍历HashMap里面的元素

??HashMap 可以理解为一张平面表,由两列多行组成。

?

?场景:遍历HashMap集合的每个元素

package com.whsxt.day7.map;

import java.util.HashMap;
import java.util.Map;

/**
 * @author caojie
 *场景:遍历HashMap集合的每个元素
 */
public class TestHashMap4 {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("北京","中国");
        map.put("伯尔尼","瑞士");
        map.put("哈瓦那", "古巴");
        map.put("太子港", "海地");
        map.put("科威特城","科威特");
        //map.entrySet() 获取HashMap中所有的Entry对象
        //Map.Entry<String,String> entry 表示HashMap集合中的每一个Entry对象
        //entry编译期是一个接口类型(Map.Entry),运行期是一个HashMap$Node类型
        //遍历HashMap集合的每个元素,每个元素都以Entry类型存在
        for(Map.Entry<String,String> entry : map.entrySet()) {
            //获取HashMap 的key
            String key =entry.getKey();
            //获取Hash Map的Value
            String value =entry.getValue();
            System.out.println(key+"			"+value);
        }
    }
}

HashMap和缓存

??缓存特征:牺牲空间换取时间。

??缓存是内存的一种形式:经常需要获取并且不轻易改变的数据就会放入缓存中,缓存中的数据只加载一次。

??HashMap中的数据在工作中通常以缓存的形式存在,也就是说工作中HashMap通常当做缓存使用。

?

??场景1:存储全国所有的城市信息,城市名称作为Key,城市相关的信息作为value[id,cityName,cityCode,cityDesc],要求根据Key,获取城市相关的信息

??步骤:1. 创建一个缓存(HashMap),将所有的城市信息的加载工作放入static块

???2. 提供一个公有的静态的方法给外界,外界可以根据Key获取缓存中的value信息

public class City {
    private int id;
    
    private String cityName;
    
    /**
     * 城市的区号
     */
    private String cityCode;
    
    /**
     * 城市的描述(介绍信息)
     */
    private String cityDesc;

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getCityName() {
        return cityName;
    }
    public void setCityName(String cityName) {
        this.cityName = cityName;
    }
    public String getCityCode() {
        return cityCode;
    }
    public void setCityCode(String cityCode) {
        this.cityCode = cityCode;
    }
    public String getCityDesc() {
        return cityDesc;
    }
    public void setCityDesc(String cityDesc) {
        this.cityDesc = cityDesc;
    }
    
    public City() {}

    public City(int id, String cityName, String cityCode, String cityDesc) {
        super();
        this.id = id;
        this.cityName = cityName;
        this.cityCode = cityCode;
        this.cityDesc = cityDesc;
    }   

    @Override
    public String toString() {
        return "City [id=" + id + ", cityName=" + cityName + ", cityCode=" + cityCode + ", cityDesc=" + cityDesc + "]";
    }
}

定义缓存

import java.util.HashMap;
import java.util.Map;

/**
 * 使用HashMap作为缓存
 */
public class CityCache {
    /**
     * 存储所有的城市信息
     */
    private static Map<String,City> cityCache = new HashMap<>();
    static {
        cityCache.put("北京",new City(1, "北京","010","帝都"));
        cityCache.put("上海",new City(2, "上海","021","魔都"));
        cityCache.put("广州",new City(3, "广州","020","羊城"));
        cityCache.put("深圳",new City(4, "深圳","0755","鹏程"));
        cityCache.put("武汉",new City(5, "武汉","027","江城"));
    }
    
    /**
     * 根据Key获取对应的城市信息
     * @param key 城市的名称
     * @return 城市相关的信息City
     */
    public static City getCityByKey(String key) {
        return cityCache.get(key);
    }
}

使用缓存

public class TestCache {
    public static void main(String[] args) {
        // 获取缓存中的数据
        /*
         * 第一步:将CityCache加载到JVM
         * 第二步:检查CityCache.class有没有静态数据,如果有初始化静态数据
         * 第三步:调用静态方法getCityByKey(key)获取缓存中的数据
         * 注意:静态方法调用之前缓存中所有的数据初始化完毕
         *      缓存中的数据只会初始化一次,在第一次调用静态方法之前初始化
         *      所以说:第一次调用静态方法相对较慢(加载静态数据),后面会很快
         */
        City city = CityCache.getCityByKey("深圳");
        System.out.println(city);
        // 第二次调用静态方法不会初始化CityCache缓存中的数据
        City city2 = CityCache.getCityByKey("广州");
        System.out.println(city2);
    }
}

?

?场景2:使用HashMap作为缓存,来存储环境变量的信息,根据Key获取环境变量的值

import java.util.Map;

/**
 * 环境变量的缓存
 */
public class EnvCache {

    /**
     * 获取系统所有的环境变量信息,存储到HashMap缓存中
     */
    private static Map<String,String> envMap = System.getenv();
    
    /**
     * 遍历缓存中的每个键值对
     */
    public static void iteratorEnv() {
        for(Map.Entry<String,String> entry: envMap.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+"			"+value);
        }
    }
    
    /**
     * 根据Key获取环境变量对应的值
     * @param key
     * @return
     */
    public static String getValueByKey(String key) {
        return envMap.get(key);
    }
}

获取缓存数据:

public class TestEnv {
    public static void main(String[] args) {
        // 遍历缓存的数据
        // EnvCache.iteratorEnv();
        // 根据Key获取对应的环境变量值
        String value =EnvCache.getValueByKey("Path");
        System.out.println(value);
    }
}

补充:HashMap中的坑

import java.util.HashMap;
import java.util.Map;

/**
 * HashMap的陷阱
 */
public class TestHashMapTrap {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        //HashMap键必须唯一,如果将相同的Key放入HashMap,后面的key-value会覆盖前面的key-value
        map.put("北京","中国");
        map.put("北京","CHINA");
        map.put("北京","CHINESE");
        // A.1   B.3   C.编译错误   D.编译成功,运行失败
        System.out.println(map.size());     // 1
    }
}

?问题:为什么String作为HashMap的Key,不能重复,而Student作为HashMap的Key,却可以重复?

??因为String定义了判断是否重复的逻辑,而Student没有。

??String作为Key放入HashMap之前,会首先调用hashCode()方法,判断集合中有没有相同的hashCode,如果有相同的hashCode,然后再调用equals()方法,判断有没有相等的值,如果hashCode相同并且equals相等,说明有重复的Key,不会放入,只会将新的key-value对覆盖旧的key-value对。

以下代码:将Student作为Key,String作为Value,由于Student没有判断Key是否重复,所以size=2;

public class Student {
    private int id;
    private String stuName;

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getStuName() {
        return stuName;
    }
    public void setStuName(String stuName) {
        this.stuName = stuName;
    }
    
    public Student() {}
    public Student(int id, String stuName) {
        this.id = id;
        this.stuName = stuName;
    }

    @Override
    public String toString() {
        return "Student [id=" + id + ", stuName=" + stuName + "]";
    }
}
import java.util.HashMap;
import java.util.Map;

/**
 * HashMap的陷阱
 */
public class TestHashMapTrap {
    public static void main(String[] args) {
        //Student作为Key,String作为Value
        Map<Student,String> map = new HashMap<>();
        map.put(new Student(1,"Tom"), "Tom");
        map.put(new Student(1,"Tom"), "Tom");
        System.out.println(map.size());     // 2
        System.out.println(map);
    }
}

??注意:HashMap中的Key必须要覆盖Object类型的hashCode()和equals()方法,这两个方法用来判断HashMap集合中是否存在相同的Key。

HashMap的存储机制

??问题:HashMap集合中的key-value对没有规律(没有顺序),但是HashMap会根据Key的hashCode进行排序

???HashMap 存储机制:第一次 put(k,v) 会初始化 Node[] table = new Node[16]; ,然后将 hashCode&table.length-1 作为数组的下标,最后创建 Node类型对象 , Node newNode = new Node(hash,key,value,null); 将newNode对象放入到table数组中,下标就是hashCode&table.length-1

??HashMap核心静态内部类Node,实现了Entry接口,每当你put元素到HashMap,都会创建一个Node对象

?注意1的hashCode为49
??12的hashCode为1569

??1和12的 hashCode& table.length-1 的索引下标都是1,就会造成hashCode码的碰撞,由于1是先put,12后put,那么将12放入到1的下一个节点中(一个槽中放入两个数值,单链表的形式存储)。

?场景:将 "北京","中国"作为键值对放入HashMap集合。

??步骤:1. 获取“北京”的hashCode ,hashCode 获取方式如下:(hashCode ^ (hashCode >>> 16))。

???2. 检查内存表table[] 是否为空,如果第一次做put(k,v)操作,table就是null值,需要创建table[],然后分配空间,空间默认大小是16,默认负载因子是0.75,默认的阀值是12。

???3. 如果是第一次put(k,v), 会创建一个静态内部类的对象Node,存储key的hashCode,key,value。

???4. 需要将Node对象放入到table[]数组中,放到数组的那个slot(槽位)呢?

???5. 将key的hashCode& table.length-1,得到槽位的索引下标。

???6. 根据索引下表将Node对象放入到table[]数组中。

???7. ++size。

???8. 判断size有没有超过阀值(12),如果超过了就会重新扩容。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    /**
     * 默认容量  1<<4=16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;     // aka 16
    
    /**
    默认负载因子0.75
    阀值=(int)DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR
    超过阀值HashMap会自动扩容
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
}

HashSet(掌握)

??HashSet的底层实际就是一个HashMap(是一个简化版的HashMap),因此,查询效率和增删效率都比较高。每当将新元素放入HashSet实际上是将新元素放入HashMap,key(键)必须唯一,值是一个哑巴值。

??HashSet集合实现了Set接口,存储HashSet的值,不能重复。

???注意:存储HashSet集合中的元素,必须覆盖Object类型的hashCode()方法和equals()方法,这样才能保证对象唯一。

HashSet常用方法

??add():将元素(对象)放入容器

??size():获取HashSet集合的元素大小

??remove(obj):删除元素

import java.util.HashSet;
import java.util.Set;

public class TestHashSet {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("Tom");
        set.add("Tom");
        set.add("Tom");
        set.add("Tom");
        set.add("Tom");
        System.out.println(set.size());     // 1
        set.remove("Tom");
        System.out.println(set.size());     // 0
    }
}

??HashSet特征:不会有重复的元素。

??第一次调用add()方法将Tom放入HashSet集合,由于集合中没有元素直接放入。第二次调用add()方法将Tom放入HashSet集合,会判断有没有重复的Tom,如果没有就放入,如果有就不会放入,只会将新的Tom覆盖旧的Tom。判断元素是否重复的依据是hashCode()和equals()方法, hashCode相同并且equals()相等说明HashSet中有重复的元素。

??元素不能重复,所以存储对象(元素)的信息使用ArrayList或者LinkedList,因为Set中的元素不能重复(例如:有可能班上有两个学生都叫做张三)。
??HashSet元素之间缺乏联系,所以不能使用get()方法获取元素。

?场景1:有一个ArrayList集合[1,2,2,3,5,7,7,8],需要将重复的元素清除掉

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class TestHashSet {
    static List<Integer> list = new ArrayList<>();
    static {
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(3);
        list.add(5);
        list.add(5);
    }
    public static void main(String[] args) {
        Set<Integer> set =new HashSet<>(list);
        list.clear();
        list.addAll(set);
        System.out.println(list);
    }
}

??contains():判断元素是否在集合中存在,true存在,false不存在。

?场景2:输入一个手机号码,判断该号码是否在HashSet集合中存在,如果存在就不执行后面的业务逻辑,如果不存在就下发垃圾短信。

import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;

/**
 * 白名单:将手机号码存入到白名单中,如果输入的手机号不在白名单就下发垃圾短信
 * 1 创建HashSet对象
 * 2 加载白名单
 * 3 创建Scanner对象
 * 4 输入手机号码
 * 5 判断输入的手机号码是否在白名单中,如果不在下发短信
 */
public class TestHashSetWhite {
    /**
     * 存储白名单的HashSet
     */
    private final static Set<String> MOBILE_SET= new HashSet<>();
    
    static {
        MOBILE_SET.add("15609898765");
        MOBILE_SET.add("15609898761");
        MOBILE_SET.add("15609898769");
    }
    
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("请输入手机号码");
        String mobile = input.nextLine();
        //条件成立:手机号码不在白名单中,下发短信
        if(!MOBILE_SET.contains(mobile)) {
            System.out.println("下发短信。。。。。。。。。");
        }else {
            System.out.println("不下发短信");
        }
    }
}

?注意List集合Set集合都有contains(key) 方法,但是List里面有重复的元素,所以调用该方法效率低下,工作中contains(key)方法,通常用于set集合。

以上是关于HashSet和HashMap的主要内容,如果未能解决你的问题,请参考以下文章

HashSet与HashMap源代码深度剖析

java中HashMap详解

Java提高篇——通过分析 JDK 源代码研究 Hash 存储机制

HashMap和HashSet的区别?

HashSet和HashMap的区别

HashMap与HashSet的区别