Map集合

  • Map接口实现类的特点
  1. Map和 Collection 并列存在,用于保存具有映射关系的数据 key-value
  2. Map中的key 和 value 可以是任何引用类型的数据,会封装到、HashMap$Node 对象中
  3. Map 中的 key 不允许重复,原因和 HashSet 一样,前面分析过源码。hash 值和内容一样就会取消。
  4. Map 中的 value 可以重复
  5. Map 的 key 可以为 null,value 也可以是 null ,注意 key 为 null,只能有一个,value 为 null ,可以为多个。
  6. 常用 String 类作为 Map 的 key。
  7. key 和 value 之间存在单向一对一关系,即通过指向的key总能找到对应的 value

前面已经分析过源码

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)		// 这里的hash只是key的,跟value没关系
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&		// 如hash值和当前值key一致,进行替换操作
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

结论:首次添加还是扩容16个,添加时判断的key的hash值是否一致,与value里的东西没有关系。

HashMap

HashMap添加时,会把自己的 key-value 让 Entry 进行引用

  1. put操作 最后存放在 HashMap$Node node = new Node(hash,key,value,null)
  2. 为了方便遍历,会创建 EntrySet集合,用来存放该元素的类型 就是 Entry<k,v>
  3. 在 EntrySet 中,定义的类型是 Map.Entry,但存放的还是 HashMap$Node
  4. ?为什么 HashMap&Node类型能放到 Map.Entry 中,原因是实现了接口的多态,HashMap$Node 实现了 Map.Entry类 (static class Node<K,V> implements Map.Entry<K,V>)
  5. 当把 HashMap$Node 对象存放到 entrySet 就会方便我们遍历,因为 Map.Entry 提供了非常重要的方法:K getKey(),V getValue();;
/*
* 1.K-V 最后存放在 HashMap$Node node = new Node(hash,key,value,null)
* 2.K-V 为了方便遍历,还会创建 EntrySet 集合,该集合存放元素的类型就是就 Entry,而一个Entry 就有 key-value
*            EntrySet<Entry<K,V>>   transient Set<Map.Entry<K,V>> entrySet;
* 3. 在 EntrySet中,定义的类型是 Map.Entry,但存放的还是 HashMap$Node
*       ?为什么 HashMap&Node类型能放到 Map.Entry 中,
*       原因是实现了接口的多态,HashMap$Node 实现了 Map.Entry类 (static class Node<K,V> implements Map.Entry<K,V>)
* 4. 当把 HashMap$Node对象存放到 entrySet 就会方便我们遍历,因为 Map.Entry提供了非常重要的方法:K getKey(),V getValue();;
*/
Set set = map.entrySet();
System.out.println(set.getClass());
for (Object o : set){
    /*为了从 HashMap$node 取出K-V*/
    /*先做一个向下转型*/
    Map.Entry entry = (Map.Entry) o;
    System.out.println(entry.getKey());
}
Set set1 = map.keySet();
Collection values = map.values();
Map接口的常用方法
  1. put:添加
  2. remove:根据键删除映射关系
  3. get:根据键获取值
  4. size:获取 元素个数
  5. isEmpty:判断格式是否为0
  6. clear:清除
  7. containsKey:查找键是否存在
Map接口的遍历方式
  1. containsKey:查找键是否存在
  2. keySet:获取所有的键
  3. entrySet:获取所有关系
  4. values:获取所有的值
@SuppressWarnings({"all"})
public class MapFor {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("邓超01","孙俪01");
        map.put("邓超02","孙俪02");
        map.put("邓超03","孙俪03");
        map.put("邓超04","孙俪04");
        map.put("邓超05","null");
        map.put("null","孙俪06");

        System.out.println("———————增强for循环———————");
        Set keymap = map.keySet();
        for (Object k : keymap){
            System.out.println(map.get(k));
        }

        System.out.println("———————迭代器———————");
        Iterator iterator = keymap.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next);
        }
        System.out.println("———————把所有的values取出———————");
        Collection values = map.values();
        // 这里可以使用所有Collections使用的遍历方法
//        (1) 增强for
        for (Object o : values){
            System.out.println(o);
        }

        System.out.println("———————迭代器———————");
        Iterator iterator1 = values.iterator();
        while (iterator1.hasNext()) {
            Object next = iterator1.next();
            System.out.println(next);
        }


        System.out.println("————————通过EntrySet来获取 k-v————————");
        Set entrySet = map.entrySet();
//        增强for
        for (Object entry:entrySet){
            Map.Entry entry1 = (Map.Entry)entry;
            System.out.println(entry1);
        }

        System.out.println("———————迭代器———————");
        Iterator iterator2 = entrySet.iterator();
        while (iterator2.hasNext()) {
            Object next =  iterator2.next();
            Map.Entry entry1 = (Map.Entry)next;
            System.out.println(entry1);
        }
    }
}
HashMap小结
  1. Map接口的常用实现类:HashMap\Hashtable\Properties
  2. HashMap是Map接口使用频率最高的实现类
  3. HashMap是以 key-val 的方式来存储数据的(HashMap$Node)
  4. key不能重复,但是值可以重复,允许使用null值和null值。
  5. 如果添加相同的key,则会覆盖原来的key-val,等同于修改。(key,不会替换,val会替换)
  6. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。
  7. HashMap没有实现同步,因此是线程不安全的。没有做同步互斥 sync
HashMap底层机制

HashMap 扩容机制就是【HashSet】

  1. HashMap底层维护了Node类型的数组table,默认为Null
  2. 当创建对象时,将加载因子(loadfactor)初始值为.075
  3. 当添加 key-value 时,通过 key 的哈希值得到在 table 的索引,然后判断该索引是否有元素,如果没有元素直接添加,如果有元素,继续判断该元素的 key 是否和准备加入的值是否相等,如果相等,直接替换,如果不相等需要判断是否为树结构还是链表结构,做出相应处理。如果添加是发现容量不够,则需要扩容。
  4. 第一次添加,则需要扩容 table 容量为 16,临界值为 12,是容量的 0.75
  5. 以后再扩容,则需要扩容 table 容量原来的2倍,临界值为原来的 2 倍,即 24,以此类推
  6. 在jdk8.如果链表超过8,并且容量大小大于等于 64,进行树化。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;		// 定义了辅助变量
    // table是HashMap的一个数组,类型是 Node[],如果为null
    if ((tab = table) == null || (n = tab.length) == 0)
        // 第一次扩容到16个空间,执行完resize()会得到长度为16的Node数组, n = 16
        n = (tab = resize()).length;
    //  根据key得到的hash,去计算该key应该存放到的table表的哪个索引位置
    	// 并把这个位置的对象赋值给p
    	// 如果 p = null,表示为没有存放过,将新节点添加到数组
    	// 如果 p != null,表示不是第一次进行添加
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {	// 如果重复,进行此语句
        Node<K,V> e; K k;		// 辅助变量
        // 如果当前索引位置对应的链表的第一个元素 和 准备 添加的key的hash值一样。 
        if (p.hash == hash && 
            // 如果要加入的key和现在的key是同一个对象,或者 key不能为空和内容相同
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;		// 将要添加的节点信息赋值给e
        else if (p instanceof TreeNode)	// 如果对象和内容都不一样,是否是一个红黑树
            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); // 则加入该链表的最后,
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 加入后面后,判断该链表已经达到8个节点,如果达到进化成红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&		// 判断每一个hash和引用的对象和对象的值是否一致
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;	// 一致就退出
                p = e;	// 将每次循环的值替换成下一个
            }
        }
        if (e != null) { 
            V oldValue = e.value;	// 获取要添加的值
            if (!onlyIfAbsent || oldValue == null)		// 如果要添加的值不等于Null
                e.value = value;
            afterNodeAccess(e);		
            return oldValue;		// 就把要添加的值返回回去
        }
    }
    ++modCount;		// 修改了一次
    if (++size > threshold)		// 如果现在的大小是否超过数组容量的0.75
        resize();	// 进行扩容
    afterNodeInsertion(evict);		// 为了让子列实现这个方法继续扩展
    return null;	// 返回 null 代表执行成功
}

    // 树化
		// 判断如果表为null,或者表的长度没有64大就进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();

//****************************************************************************
	// 执行扩容的方法
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;		// 获取HashMap的table数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;		// 获取Node数组的长度
        int oldThr = threshold;		// 将临界点赋值给oldThr
        int newCap, newThr = 0;		// 创建辅助变量
        if (oldCap > 0) {		// 如果数组长度大于0,表示不是第一次添加则进入此通道
            if (oldCap >= MAXIMUM_CAPACITY) {	// 判断长度是否大于等于 1 << 30
                threshold = Integer.MAX_VALUE;	// 数组最大值赋值给临界值
                return oldTab;		// 将数组长度返回
            }
            // 如果扩容以后小于 1 << 30的值并且数组长度大于默认的数组长度(16)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
               
                newThr = oldThr << 1;  // 进行扩容操作,临界点*2赋值给新的临界点
        }
        // 如果数组长度不大于0,而且临界点大于0
        else if (oldThr > 0) 
            newCap = oldThr;	// 就将旧的临界点赋值给新的数组长度
        else {               // 如果临界点不大于0,临界点也不小于0,进入此通道
            newCap = DEFAULT_INITIAL_CAPACITY;	// 默认长度赋值给新长度
            // 新长度 * 0.75获得临界点
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果新临界点是0,进入此通道
        if (newThr == 0) {
            // 将新长度 * 临界点(临界点可以自定义,在构造函数中定义)赋值给ft
            float ft = (float)newCap * loadFactor;
            // 设置新临界点:如果新长度小于1 << 30 并且 如果临界点 小于1 << 30,返回 ft,否则返回Integer最大长度)
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 将新临界点赋值给全局变量的临界点
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 创建辅助变量
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 判断如果旧表的内容不等空
        if (oldTab != null) {
            // 循环此表数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;	// 创建辅助变量
                if ((e = oldTab[j]) != null) {	// 如果 每个数组的内容不为null
                    oldTab[j] = null;	// 就将内容变为null
                    if (e.next == null)		// 如果每个节点没有下一个节点,代表最后一次执行
                        newTab[e.hash & (newCap - 1)] = e;	// 将此节点添加到新表数组中
                    else if (e instanceof TreeNode)		// 如果不是最后一个节点,判断是否是树形,如果是树形
                        // 将循环的这个节点进行分解,将旧的表长度添加到新表中
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // 如果不是树型并且不是循环到最后一位。
                        // 创建局部变量辅助操作
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {	// 循环
                            next = e.next;		// 节点的下一个节点,
                            if ((e.hash & oldCap) == 0) {	// 判断循环的这个节点的hash值是否==0
                                // 如果 为空
                                if (loTail == null)
                                    loHead = e;	// 将节点赋值给 loHead
                                else	// 否则
                                    loTail.next = e;		// 将loTail的下个节点指向 e
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;		// 向扩容的新表结构返回回去
    }
Hashtable
  1. 存放的元素是键值对,即k-v
  2. hashtable的键和值都不能为null
  3. hashtable 使用的方法基本和 hashMap 一样
  4. hashtable是线程安全的,hashMap 是线程不安全的。
构造方法
// 无参构造
public Hashtable() {
    this(11, 0.75f);		// 参数为默认容量11,默认临界值比例0.75倍
    
}
// 有参构造,参数自定义默认容量
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}
// 有参构造,参数自定义默认容量和临界值的比例
public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)	// 如果容量小于0抛异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))	// 如果临界值小于等于0或者为空抛异常
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

    if (initialCapacity==0)		// 如果容量等于0,自动让容量+1
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    table = new Entry<?,?>[initialCapacity];	// 创建Entry集合
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
put源码解析
public synchronized V put(K key, V value) {
    // 判断value是否为空,为空抛异常
    if (value == null) {
        throw new NullPointerException();
    }

    // 将构造函数时创建的table放到tab中
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();	// 获取key的hash值
    int index = (hash & 0x7FFFFFFF) % tab.length;	// 将长度*临界值的比例得到临界值
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {		// 循环tab
        if ((entry.hash == hash) && entry.key.equals(key)) {	//如果出现相等的hash和key 
            V old = entry.value;	// 进行更新操作
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);	// 程序走到这代表没有为null,并且没有重复时,执行添加
    return null;
}
———————————————————————————————————真正执行添加———————————————————————————————————
    private void addEntry(int hash, K key, V value, int index) {
        modCount++;		// 修改次数+1

        Entry<?,?> tab[] = table;	// 创建局部变量获取tab值,复制操作为了避免修改真正的数据
        if (count >= threshold) {		// 如果插入后的数量大于等于 临界值
            // 进行扩容操作
            rehash();

            tab = table;	// 扩容以后将扩容的信息复制到局部变量中
            hash = key.hashCode();		// 获取key的hashCode
            index = (hash & 0x7FFFFFFF) % tab.length;		// 获取下标索引
        }

        //执行添加操作
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];		// 将tab数组向下转型到Entry
        tab[index] = new Entry<>(hash, key, value, e);
        count++; 存在值的长度++
    }
扩容机制
protected void rehash() {
    int oldCapacity = table.length;		// 	获取表长度
    Entry<?,?>[] oldMap = table;		// 将表内容存放到Entry集合中

    // 进行扩容,2倍+1
    int newCapacity = (oldCapacity << 1) + 1;
    if (newCapacity - MAX_ARRAY_SIZE > 0) {		// 如果扩容后比数组最大的长度大。
        if (oldCapacity == MAX_ARRAY_SIZE)	// 如果旧长度最大的长度
            // 返回
            return;
        newCapacity = MAX_ARRAY_SIZE;// 如果扩容后比数组最大的长度大。 就设置成数组最大的长度
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];	// 创建Entry集合

    modCount++;	// 次数++
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;

    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}
Properties
  1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据
  2. 他的使用特点和Hashtable类似
  3. Properties 还可以用于 从 xxx.properties 文件中,加载数据到Properties类对象,并进行读取和修改

特点

  1. Properties,**继承的是 Hashtable,**所以机制都用的是 Hashtable
  2. 可以通过key - value存放数据。当前 key-value 不能为 null.

增(put)、删(remove)、改(put相同key)、查(get())。

TreeMap

传入比较器,决定添加的顺序【如果比较的值一样就放弃,不会进行替换】

TreeMap treeMap = new TreeMap(new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        // return ((String) o1).compareTo((String) o2);
        return ((String) o1).length() - ((String) o2).length();
    }
});
构造器
// 无参构造
public TreeMap() {
    comparator = null;
}
// 有参构造(把匿名比较器传入,进行比较)
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}
添加时进行比较
Comparator<? super K> cpr = comparator;		// 获取比较器
if (cpr != null) {// 比较器不为空
    do {
        parent = t;		// 循环每个值
        cmp = cpr.compare(key, t.key);		// 获取比较器的值
        if (cmp < 0)	// 如果比0小,
            t = t.left;		// 放左边
        else if (cmp > 0)	// 如果比0大,
            t = t.right;	// 放右边
        else				// 如果等于0
            return t.setValue(value);	// 放弃
    } while (t != null);
}

Collections方法

  • reverse:反转
Collections.reverse(arrayList);
  • shuffle:打乱(抽奖)
Collections.shuffle(arrayList);
  • sort:自然排序(字符串的升序)
Collections.sort(arrayList);
  • max:最大
  • min:最小
  • frequency:查看出现的次数
Collections.frequency(arrayList, "misad0")
  • copy:拷贝
//         拷贝
        ArrayList arrayList1 = new ArrayList();
        for (int i = 0; i < arrayList.size(); i++) {
            arrayList1.add("");
        }
//       前提是要 复制和被复制的长度一致
        Collections.copy(arrayList1,arrayList);
//        遍历数组
        Iterator iterator = arrayList1.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println(next);
        }
  • replaceAll:修改值
Collections.replaceAll(arrayList,"misad0","asdas");// 要修改的集合,旧值,新值

开发如何选择集合实现类

在开发中,选择什么集合实现类,主要取决于业务的操作特点。

  1. 先判断存储类型(一组对象【单列】或一组键值对【双列】)
  2. 一组对象:Collection 接口
  • 允许重复:List
  • 增删多:LinkedList【底层维护了一个双向链表】
  • 查改多:ArrayList【底层维护Object类型的可变数组】
  • 不允许重复:Set
  • 无序:HashSet【底层是HashMap,维护了一个哈希表,极(数组+链表+红黑树)】
  • 排序:TreeSet
  • 插入和取出的顺序一致【LinkedHashSet】,维护数组 + 双向链表
  • 一组键值对:Map
  • 键无序:HashMap【底层是Hash表】
  • 键排序:TreeMap
  • 检查如和取出顺序一致:LinkedHashMap
  • 读取文件:Properties