Java 集合详解、源码分析(3)

Map接口和常用方法

image-20210927085020479

Map接口实现类的特点

2021-10-11 105032

  1. Map和Collection并列存在。用于保存具有映射关系的数据:key-value

  2. Map中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node对象中

  3. Map 中的 Key 不允许重复,原因和HashSet 一样

  4. Map 中的 value 可以重复

  5. Map 的key 可以为 null,value 也可以为null,注意 key 为null,只能有一个,

  6. 常用String类作为Map 的key

  7. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value

  8. Map存放数据的 key-value 示意图,一对 k-v 是放在一个Node中的,又因为Node实现了 Entry接口,有些书上也说,一对k-v是一个Entry

public class Map_ {

    public static void main(String[] args) {
//        map接口实现类特点

        Map map = new HashMap<>();
        map.put("no1","卡卡西");
        map.put("no2","鸣人");
        map.put("no1","火影"); // 当有相同的key,就等价于替换value, key还是原来的
        map.put("no3","火影");
        map.put(null,null);
        map.put(null,"abc"); // 当有相同的key,就等价于替换value, key还是原来的
        map.put("no4",null);
        map.put("no5",null);
        map.put(6,"博人");
        map.put(new Object(),"纲手");


        System.out.println(map);
//        通过get方法传入 key,返回对应的value
        System.out.println(map.get("no2"));
        System.out.println(map.get(6));


    }
}

image-20211011104508666

Map 接口和常用方法

Map体系的继承图

 

Map接口常用方法

  1. put:添加
  2. remove:根据键删除映射关系
  3. get:根据键获取值
  4. size:获取元素个数
  5. isEmpty:判断个数是否为0
  6. clear:清除
  7. containsKey:查找键是否存在
public class MapMethod {
    public static void main(String[] args) {

//        map 接口常用方法
        Map map = new HashMap<>();
        map.put("卡卡西",new Book("美少女战士",100));
        map.put("卡卡西","火影");
        map.put("鸣人","佐助");
        map.put("博人","佐助");
        map.put("纲手",null);
        map.put(null,"大蛇丸");
        map.put("guoxiao","sb");

        System.out.println("map="+map);

        System.out.println("=========");
//        1. put:添加

//        2. remove:根据键删除映射关系
        map.remove(null);
        System.out.println("map="+map);

//        3. get:根据键获取值
        System.out.println("=========");
        Object val = map.get("guoxiao");
        System.out.println(val);


//        4. size:获取元素个数
        System.out.println("=========");
        int size = map.size();
        System.out.println(size);


//        5. isEmpty:判断个数是否为0
        System.out.println("=========");
        boolean empty = map.isEmpty();
        System.out.println(empty);


//        6. clear:清除
        System.out.println("=========");
        map.clear();
        System.out.println("map="+map);


//        7. containsKey:查找键是否存在
        System.out.println("=========");
        map.put("lk","wkliu");
        boolean lk = map.containsKey("lk");
        System.out.println(lk);


    }
}

class Book {
    private String name;
    private int num;

    public Book(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

image-20211012102153410

Map 接口的遍历方法

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

        Map map = new HashMap<>();
        map.put("卡卡西","火影");
        map.put("鸣人","佐助");
        map.put("博人","佐助");
        map.put("纲手",null);
        map.put(null,"大蛇丸");
        map.put("guoxiao","sb");

//        第一组:先取出所有key,再 通过key取出所有 value
        Set keySet = map.keySet();
//        (1) 增强for
        System.out.println("=======第一种方式:");
        for (Object key : keySet) {
            System.out.println(key+"-"+map.get(key));
        }
//        (2) 迭代器
        System.out.println("=======第二种方式:");
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object key =  iterator.next();
            System.out.println(key+"-"+map.get(key));

        }

//        第二组:把所有的value取出
        Collection values = map.values();
//        3种方式
//        (1):增强for
        System.out.println("====第三种,增强for取出所有的value");
        for (Object value : values) {
            System.out.println(value);
        }
//        (2): 迭代器
        System.out.println("====第四种,迭代器取出所有的value");

        Iterator iterator1 = values.iterator();
        while (iterator1.hasNext()) {
            Object value =  iterator1.next();
            System.out.println(value);
        }

//        第三组:通过 EntrySet 获取 k-v
        Set entrySet = map.entrySet();
//        (1) 增强 for
        System.out.println("=======第五种,entryset 增强for");
        for (Object entry : entrySet) {
//            将 entry 转成 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey()+"-"+m.getValue());

        }

//        (2) 迭代器
        System.out.println("=======第五种,entryset 迭代器");
        Iterator iterator2 = entrySet.iterator();
        while (iterator2.hasNext()) {
            Object entry =  iterator2.next();
//            向下转型
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey()+"-"+m.getValue()); 
        }
    }
}

HashMap

  1. Map接口的常用实现类:HashMap、HashTable和Properties
  2. HashMap是Map接口使用频率最高的实现类。
  3. HashMap是以key-value对的方式来存储数据
  4. Key不能重复,但是值可以重复,允许使用null键和null值
  5. 如果添加相同的key,则会覆盖原来的key-value,等同于修改(key不会替换,val会替换)
  6. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的
  7. HashMap没有实现同步,因此是线程不安全的,方法上没有做同步互斥操作,没有synchronized

HashMap 扩容机制及源码分析’

扩容机制 【同HashSet相同】

  1. HashMap底层维护了Node类型的数组table,默认为null
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75
  3. 当添加 key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key是否和准备加入的key相同,如果相等,则直接替换value;如果不相等需要判断是树结构还是链表,做出相应处理。如果添加时发现容量不够,则需扩容。
  4. 第一次添加,则需要扩容table容量为16,临界值(threshold)为12 (16 *0.75)
  5. 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍,即24,依次类推
  6. 在Java8种,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIEF_CAPACITY(默认是64),就会进行树化(红黑树)

put源码分析

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        
        // 定义辅助变量 tab、p、n、i
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        
        // table 是hashMap的一个属性,类型是Node[]  存放 node节点的数组
        // 如果 table为null或者大小为0,执行resize()方法,table第一次扩容16个空间
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        
        // 取出hash值对应的table的索引位置的Node,如果为null,就直接把加入的k-v,创建成一个Node,加入该位置即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果 table的索引位置的key的hash和新的key的hash相同,
            // 并且满足(table现有的节点的key和准备添加的可以是同一个对象) 或者 equals返回真
            // 就认为不能加入新的 k-v
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode) // 如果当前table的node已经是红黑树,就按照红黑树的方式处理
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 如果找到的节点,后面是链表,就循环比较
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {  // 如果整个链表,没有和他相同的,就加在该链表后
                        p.next = newNode(hash, key, value, null);
                        // 判断当前链表个数是否已经到8个,到8个后,进行红黑树的树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&   // 如果在循环比较过程种,如果有相同的,就直接break
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 替换 value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 操作次数,每增加一个Node,就size++
        ++modCount;
        
        // 判断是否需要扩容,threshold为扩容操作临界值,默认为12,tab超过12执行扩容
        if (++size > threshold)
            resize();
        
        //空方法,是HashMap 给其子类留的 让其子类执行一些操作
        afterNodeInsertion(evict);
        
        // 放回 null,代表成功
        return null;
    }

Hashtable

Hashtable基本介绍

  1. 存放的元素是键值对:K-V
  2. Hashtable的键和值都不能为null,否则会抛出NullPointerException
  3. Hashtable使用方法基本上和 hashMap一样
  4. Hashtable是线程安全的(synchorized),hashMap 是线程不安全的
public class HashTableExercise {
    public static void main(String[] args) {


        Hashtable table = new Hashtable();
        table.put("john",100);
//        table.put(null,100);   //NullPointerException
//        table.put("join",null);  //NullPointerException
        table.put("lucy",100);
        table.put("lic",100);
        table.put("lic",80); // 替换
        table.put("hello1",1);
        table.put("hello2",1);
        table.put("hello3",1);
        table.put("hello4",1);
        table.put("hello5",1);
        table.put("hello6",1);
        System.out.println(table);

//        Hashtable底层
//        1. 底层有一个数组 Hashtable$Entry[]  初始化大小为11
//        2. 临界值 threshold 8 = 11 * 0.75
//        3. 扩容:
//        4. 执行 addEntry(hash, key, value, index); 添加 k-v 封装到 Entry
//        5. 当 if(count >= threshold) 满足时,就进行扩容
//        6. 按照 int newCapacity = (oldCapaticy << 1 ) + 1;  [old*2+1]的大小扩容

    }
}

Hashtable 和HashMap 对比

版本线程安全(同步)效率允许null键null值
HashMap1.2不安全可以
Hashtable1.0安全较低不可以

TreeMap

  • 唯一
  • 有序(按照升序或降序排列)
  • 底层:二叉树,key遵照二叉树特点
  • key对应的类型内部一定要实现比较器(内部比较器,外部比较器自选)
public class TreeMap_ {
    public static void main(String[] args) {

//       使用默认构造器,创建TreeMap
//        TreeMap treeMap = new TreeMap();

//
        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
//                按照传入的 k(String) 的大小进行排序
//                return ((String) o2).compareTo((String) o1);
//                按照字符串长度大小排序
                return ((String) o1).length() - ((String) o2).length();


            }
        });
        treeMap.put("jack","杰克");
        treeMap.put("tom","汤姆");
        treeMap.put("wkliul","拉拉");
        treeMap.put("smith","史密斯");
        treeMap.put("smith","123");
        System.out.println(treeMap.size());
        System.out.println(treeMap);
    }
}

image-20211029181402971

原理

通过节点对象属性,按照排序规则判断元素的对象,指定子节点的位置

image-20211029182030983

源码

  • 节点类型: private transient Entry<K,V> root;
static final class Entry<K,V> implements Map.Entry<K,V> {
        // 属性
    	K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
}    
  • 构造器

     public TreeMap() {
            comparator = null;   // 如果使用空构造器,那么底层就不使用外部构造器
     }
    public TreeMap(Comparator<? super K> comparator) {
            this.comparator = comparator;   // 如果使用有参构造器,就相当于指定了外部比较器
        }
    
    
  • put方法

    public V put(K key, V value) {   // k,v的类型在创建对象时就指定了
        // 首次添加元素 ,t的值为null    
        Entry<K,V> t = root; // 第二次添加节点时,root已经是跟节点了
    
        	if (t == null) {
                // 自己跟自己比
                compare(key, key); // type (and possibly null) check
    			//跟节点确定为root
                root = new Entry<>(key, value, null);
                size = 1;
                modCount++;
                return null;
            }
            int cmp;
            Entry<K,V> parent;
            // split comparator and comparable paths
        	// 将外部比较器 赋给 cpr
            Comparator<? super K> cpr = comparator;
        //  cpr 不等于 null, 意味着创建对象时调用了有参构造器,指定了外部比较器
            if (cpr != null) {
                do {
                    parent = t;
                    cmp = cpr.compare(key, t.key);
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else
                        return t.setValue(value);
                } while (t != null);
            }
            //  cpr 等于 null, 意味着创建对象时调用了 空构造器,没有指定外部比较器,会使用内部比较器
            else {
                if (key == null)
                    throw new NullPointerException();
                @SuppressWarnings("unchecked")
                    Comparable<? super K> k = (Comparable<? super K>) key;
                do {
                    parent = t;
                    cmp = k.compareTo(t.key);  //将元素的key值作比较
                    // cpm 返回的值为int类型的值
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else  // cmp == 0 
                        // 表示两个key 一样,key不变(key唯一),将原来的value替换为新的value
                        return t.setValue(value); 
                } while (t != null);
            }
            Entry<K,V> e = new Entry<>(key, value, parent);
            if (cmp < 0)
                parent.left = e;
            else
                parent.right = e;
            fixAfterInsertion(e);
            size++;
            modCount++;
            return null;
        }
    

Properties

Properties基本介绍

  1. Properties类继承自HashTable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
  2. 他的使用特点和Hashtable类似
  3. Properties 还可以 从 xxx.properties 文件中,加载数据到Properties类对象,并进行读取和修改
  4. 说明:xxx.properties 文件通常作为配置文件
public class Properties_ {
    public static void main(String[] args) {

//        1. Properties 继承自 Hashtable
//        2. 可以通过 k-v 存放数据, key和value不能为空

        Properties properties = new Properties();
        properties.put("john",100);
//        properties.put(null,100);  //NullPointerException
//        properties.put("john",null); //NullPointerException
        properties.put("lucy",100);
        properties.put("lic",100);
        properties.put("lic",80);

        System.out.println(properties);

//        通过 key 获取对应的值
        System.out.println(properties.get("lic"));
//        删除
        properties.remove("lic");
        System.out.println(properties);

//        修改
        properties.put("john","wkliu");
        System.out.println(properties);
    }
}

image-20211013154643865

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值