2021/11/07 西安 集合、map集合

Map常用方法

Collection和Map

集合里又有俩大派,单方式存储和键值对存储,Collection和Map

  1. java.util.Collection 单方式存储的超级父接口
  2. java.util.Map 键值对存储的超级父接口


Map是用于操作成对对象的集合,具有key-value映射关系的集合。

HashMap
    --LinkedHashMap
HashTable
   --Properties【操作属性文件】
TreeMap


无序和排序

且注意:无序和排序是不一样的无序有序指的是存进来的顺序和取出的顺序是否一致。排序是按照key大小排序。

set集合和map集合

1.HashSet底层是HashMap.
2.由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。


Map 添加/删除

  • Object put(Object key,Object value) //key相同的,直接value替换原来的value,key不换。
  • Object remove(Object key)
  • void putAll(Map t)
  • void clear()

map集合的put()返回值细节

    @Test
    public void test3(){
        HashMap hashMap = new HashMap();
        // 使用put方法时,若指定的键(key)在集合中没有,
        // 则没有这个键对应的值,返回null,并把指定的键值添加到集合中
        Object p1 = hashMap.put("1", "2");//p1=null
        
        // 若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值)
        // 并把指定键所对应的值,替换成指定的新值
        Object p2 = hashMap.put("1", "3");//p2=2
        System.out.println("p1="+p1+","+"p2="+p2);//p1=null,p2=2
    }

Map 元素获取

  • Object get(Object key)
  • boolean containsKey(Object key)
  • boolean containsValue(Object value)
  • int size();//获取map中有几对
  • boolean isEmpty()
  • boolean equals(Object obj);

map键相同时,换value不换key验证【自己编写的,骄傲】

重写equals和hashcode()故意只用了name,是为了方便测试

public class Car {
    private String name;
    private Integer price;

    public Car() {
    }

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

    public String getName() {
        return name;
    }

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

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

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

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public String toString() {
        return "Car{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

测试类:由结果可知,是换了value没换key

    @Test
    public void test5() {
        HashMap hashMap= new HashMap();
        hashMap.put(new Car("小白",18),"999");
        hashMap.put(new Car("小白",19),"1000");
        //Map集合key相同时,value值会替换,但是key值不会被替换
        System.out.println(hashMap);//{Car{name='小白', price=18}=1000}
    }


HashMap

HashMap集合是一个存取无序的集合,存储元素和取出元素的顺序有可能不一致

HashMap允许空key空value并且是线程不安全的

哈希表

哈希表(Hash Table),也被称作散列表,是一种重要的数据结构。

哈希表是基于键(Key)的映射存储机制,通过哈希函数(Hashing Function)将键转换为数组的一个索引位置,从而能够快速定位到数据


哈希函数

理想哈希函数有两个特性:

  1. 对于同一个输入值,产生相同的哈希值;
  2. 对于不同的输入值,产生不同的哈希值。

哈希冲突: 对于不同的输入值,产生了相同的哈希值,这就叫冲突,冲突越少,哈希算法的质量越高。

每个对象都有一个hashCode值,类似于自身的身份证号码。

该hashCode值默认出厂来自于Object类的这个方法,返回的是一个int整数

//发现只有方法的声明,没有方法的实现,也即java需要通过Native关键字调用系统底层函数,给我返回值。
//native方法,表示调用底层第3方函数,C语言系统生成的东西
public native int hashCode();

哈希冲突:两个不同的对象居然碰撞出来同一个hashCode值,即 hashCode哈希值冲突了

-------------------------

演示哈希冲突1

//演示哈希冲突1:
System.out.println("Aa".hashCode());//2112
System.out.println("BB".hashCode());//2112

演示哈希冲突2:上万次的计算后,hash值会冲突

      
//演示哈希冲突2:上万次的计算后,hash值会冲突
Set set = new HashSet();
int hashCode;
for (int i = 1; i <=110000 ; i++) {
    hashCode = new Object().hashCode();
    if(set.contains(hashCode))
    {
        System.out.println("----出现了hash冲突,在第几次:"+i+"\t hashCode: "+hashCode);
        continue;
    }
    set.add(hashCode);
}
System.out.println(set.size());

哈希冲突,每个人的环境下出现的哈希冲突在第几次一般是固定的。

何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?

只要两个元素的key计算的哈希码值相同就会发生哈希碰撞。jdk8前使用链表解决哈希碰撞。jdk8之后使用链表+红黑树解决哈希碰撞。


HashMap中常见2种算法

HashMap中的Hash算法:   key的HashCode值无符号右移16位做异或运算

//HashMap源码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

计算索引的算法:数组长度减一上hash值

tab[i = (n - 1) & hash]

jdk7 HashMap的底层实现原理

HashMap底层结构:jdk1.7 中由数组+链表实现

HashMap map= new HashMap();

在实例化以后,底层创建了长度是16的一维数组Entry[] table


....可能已经执行过多次put...
map.put(key1,value1)
首先,调用key1所在类的hashcode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空。此时的key1-value1添加成功。---情况1


如果此位置上的数据不为空,比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不同,此时key1-value1添加成功。----情况2


如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在
类的equals(key2)
   如果equals()返回false:此时key1-value1添加成功。----情况3
   如果equals()返回true:使用value1替换value2.


补充:关于情况2和情况3;此时key1-value1和原来的数据以链表的方式存储,在不断的添加过程,会涉及到扩容的问题,默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来


jdk8 HashMap底层实现原理

HashMap底层结构:jdk1.8 中由数组+链表+红黑树实现

假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化。

HashMap jdk8存储过程

说明:

1.size表示 HashMap中K-V的实时数量 , 注意这个不等于数组的长度 。

2.threshold( 临界值) =capacity(容量) * loadFactor( 加载因子 )。这个值是当前已占用数组长度的最大值。size超过这个临界值就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍 。

 相较于jdk7在底层实现方面的不同:

1.new HashMap():底层没有创建一个长度为16的数组
2.jdk8 底层的数组是:Node[],而非Entry[]
3.首次调用put()方法的时候,底层创建长度为16的数组

HashMap map = new HashMap() 在内存中占几个字节
占16位,采用懒加载

4.jdk7底层结构只有:数组+链表,jdk8中底层结构:数组+链表+红黑树
    当数组的某一个索引位置上的元素以链表形式存在的数据个数>8 且当前数组长度>64时,
    此时此索引位置上的所有数据改为使用红黑树存储。

将链表转换成红黑树前会判断,即使阈值大于 8,但是数组长度小于 64,
此时并不会将链表变为红黑树。而是选择进行数组扩容

链表阈值为什么是8

选择8因为符合泊松分布,超过8的时候,概率已经非常小了,所以我们选择8这个数字。

一个bin中链表长度达到8个元素的概率为0.00000006,

============

hashmap放入的是k,v键值对,你说是数组,请问什么类型的数组?

Node<K,V>类型数组
Node 是HashMap自己定义的静态内部类

//以下是HashMap源码部分
    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;
        }
     }

HashMap扩容初始容量

设置初始容量大小的必要性

HashMap中的扩容机制决定了每次扩容都需要重建hash表,是非常影响性能的。 随着元素的不断增加,HashMap会有可能发生多次扩容

应该设置初始化容量为多少?

当我们明确知道HashMap中元素的个数(initialCapacity )的时候,
把默认容量(构造器的参数)设置成 initialCapacity/ 0.75F + 1.0F

反例:

JDK并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运算,最终得到一个2的幂。

如果我们设置的默认值是7(明确要存储7个元素,然后构造器的参数也就傻乎乎的传了7),经过Jdk处理之后,会被设置成8,但是,这个HashMap在元素个数达到 8*0.75 = 6的时候就会进行一次扩容,这明显是我们不希望见到的。

使用公式 initialCapacity/ 0.75F + 1.0F

7/0.75 + 1 = 10 ,10经过Jdk处理之后,会被设置成16,这就大大的减少了扩容的几率。

为什么initialCapacity必须是2的n次幂?如果输入值不是2的幂比如10会怎么样?

因为2的n次方可以使得元素尽量均匀分配到数组中,避免hash冲突

在确定要存储的元素在数组中的具体位置时,HashMap用某种算法尽量把数据分配均匀,这样每个链表长度大致相同,这个算法实际就是取模。

但是:计算机中直接求余效率不如位移运算

实际上hash%length等于hash&(length-1)的前提是length是2的n次幂

2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1

20%16=4

20&(2^4-1)=20&15= 4

   0001 0100

& 0000 1111

------------------

   0000 0100

默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(3->4、7->8、9->16)

//HashMap类源码
//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

HashMap加载因子

加载因子的大小是主要就是决定了HashMap的数据密度,加载因子为什么默认是0.75

//以下是HashMap的源码部分
    public HashMap() {
        //DEFAULT_LOAD_FACTOR = 0.75f
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

加载因子越大,填满的数据就越多,空间利用率就高(唯一好处),但哈希冲突的几率就变大。数组中的链表也会越容易长,这样的话造成查询和插入时的比较次数增多,性能会下降。

加载因子越小,填满的数据就越少,哈希冲突的几率就减少了,但浪费了空间,而且还会提高扩容的触发几率,扩容会很影响性能

触发扩容:会把所有元素rehash之后再放在扩容后的容器中,这是一个相当耗时的操作,所以扩容是件影响性能的。

默认情况下:

第一次扩容的时候:16*0.75 =12,
扩容机制:2倍。  2^4,2^5,2^6

//以下是HashMap的源码部分
if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // double threshold
}


HashMap设计过程中是否有错误?

虚线:接口实现;实线:继承

HashMap直接父类 Map,此时HashMap与Map 是父子关系
HashMap直接父类abstractMap,而abstractMap直接父类map 此时HashMap 与Map 是爷孙关系  


Map接口其余实现类

1、LinkedHashMap

LinkedHashSet 底层是 LinkedHashMap完成的

LinkedHashMap集合是一个有序的集合,存储元素和取出元素的顺序一致


2、HashTable(古老实现)

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
}

HashMap和HashTable底层都是哈希表数据结构

HashTable线程安全,效率低,属于悲观锁
HashMap线程不安全,效率高

HashMap集合:可以存储null值,null键
Hashtable集合,不能存储null值,null键


3、TreeMap

TreeMap 是根据key排序的。
TreeSet底层TreeMap。若使用自定义类作为TreeMap的key,所属类需要重写equals()和hashCode()方法,且equals()方法返回true时,compareTo()方法应返回0


4、Properties集合

Properties是线程安全的,采用key,value存储数据,但是key和value都只能是string类型。

Properties集合是一个唯一和IO流相结合的集合

Properties 类是 Hashtable 的子类,该对象用于处理属性文件

public class Properties extends Hashtable<Object,Object> {
}

在当前模块右键创建文件: hello.properties【因为test测试用例默认当前路径是moudle,main方法默认当前路径是项目路径】,hello.properties文件内容如下:

username=admin
password=123456
    @Test
    public void test() throws IOException {
//        1、获取properties实例
        Properties props=new Properties();
//        2、通过load()加载属性文件
        props.load(new FileInputStream("hello.properties"));
//        3、getProperty(StrIng  key);拿到用户名,密码,
        System.out.println("用户名: "+props.getProperty("username"));
        System.out.println("密码: "+props.getProperty("password"));
    }


properties文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

也就是说hello.properties文件这么写,运行的结果也是一样的

username admin
password:123456

Collections和Collection

1、排序操作

(均为Collections中的static方法)

  • reverse(List):反转 List 中元素的顺序
  • shuffle(List):对 List 集合元素进行随机排序
  • sort(List):根据元素的自然顺序【自然排序】对指定 List 集合元素按升序排序
  • sort(List,Comparator):根据指定的 Comparator 产生的顺序【定制排序】对 List 集合元素进行排序
  • swap(List,int i, int j):将指定 list 集合中的 i 处元素和 j 处元素进行交换

2、查找、替换

(均为Collections中的static方法)

  • 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中。要满足:dest.size>=src.size
  • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

Vector 和 Enumeration 接口

Vector采用数组结构存储元素,是线程安全的,效率低
Enumeration 接口是 Iterator 迭代器的 “古老版本”

    @Test
    public void test1() {
        Vector vec = new Vector();
        vec.addElement("AA");
        vec.addElement("BB");
        vec.addElement("CC");
        Enumeration elements = vec.elements();
        while (elements.hasMoreElements()) {
            Object element = elements.nextElement();
            System.out.println(element);
        }
    }


Map集合的遍历

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

Map中的key:无序的、不可重复的,使用set存储所有的key。key所在的类要重写equals()和hashCode()
Map中的Value:无序的、可重复的,使用Collection存储所有的value。value所在的类要重写equals()
一个键值对:key-value 构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry

//获取map中所有的key,是自定义的一个类KeySet实现了Set接口,再增强for,迭代器
Set keySet()
//获取map中所有的value,没办法根据value获取key。返回的是 new Values()
Collection values();
//Map.Entry对应一个key-value 是一个封装的内部类
Set entrySet() 

1、map集合的遍历: values()

  演示很容易出错的写法

    public void test3() {
        HashMap hashMap = new HashMap();
        hashMap.put("1", "a");
        hashMap.put("2", "b");
        hashMap.put("3", "c");
        Collection collection = hashMap.values();
        System.out.println(collection.getClass());//class java.util.HashMap$Values
        List list= (List) collection;//抛出异常ClassCastException
    }

  正确的写法:

    @Test
    public void test6() throws IOException {
        HashMap hashMap = new HashMap();
        hashMap.put("1", "a");
        hashMap.put("2", "b");
        hashMap.put("3", "c");
        Collection collection = hashMap.values();
        ArrayList list = new ArrayList();
        list.add(collection);
        System.out.println(list);
    }

 


2、map集合的遍历:keySet()

Set<Interger> keys=map.keySet() //获取所有的key,返回一个set集合
for(Interger key:keys){
	system.out.println("value="+map.get(key)) //调用map.get(key),获取对应key的value值
}

3、map集合的遍历:entrySet()

 把(key-value)作为一个整体一对一对地存放到Set集合当中的。 

public Set<Map.Entry<K,V>> entrySet() { }
    @Test
    public void test3() {
        HashMap<String, String> hashMap = new HashMap();
        hashMap.put("1", "a");
        hashMap.put("2", "b");
        hashMap.put("3", "c");
        //遍历 map
        for (Map.Entry<String, String> entry : hashMap.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            //格式化输出
            System.out.printf("key= %s and value= %s \n",key,value);
        }
    }

-------------------------------------------------

比较keyset()和entrySet()

keySet(): 迭代后只能通过get()取key 
entrySet():迭代后可以e.getKey(),e.getValue()取key和value。返回的是Entry接口 

最要命的是:entrySet要比keySet快一倍左右


总结一波每个集合的底层所用到的数据结构 

java中有很多的集合。不同的集合,底层会对应不同的数据结构。

  • ArrayList: 底层是数组。
  • LinkedList:底层是双向链表
  • Vector:底层是数组,线程安全的,效率低
  • HashSet:底层是HashMap,放到HashSet集合的元素等同于放到HashMap集合key部分了。
  • TreeSet: 底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合key部分了
  • HashMap:底层是哈希表
  • HashTable:底层也是哈希表,只不过线程安全的,效率低
  • Properties:是线程安全的,并且key和value只能存储字符串String
  • TreeMap:底层是二叉树。TreeMap集合key可以自动按照大小顺序排序。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值