Java学习基础笔记——集合

第十二章 集合

12.1 集合框架体系

12.2 Collection

12.2.1 List

12.2.1.1 *ArrayList底层结构

12.2.1.1.1 ArrayList 的注意事项

12.2.1.1.2 ArrayList 的底层操作机制源码分析

12.2.1.2 *LinkedList底层结构

12.2.1.2.1 LinkedList 的全面说明

12.2.1.2.2 LinkedList 的底层操作机制

12.2.1.2.3 LinkedList 的增删改查案例

12.2.1.2.4 ArrayList 和 LinkedList 的比较

12.2.1.3 *Vector底层结构

12.2.1.3.1 Vector 的基本介绍

12.2.2 Set

12.2.2.1 *HashSet

12.2.2.1.1 HashSet 的全面说明

12.2.2.1.2 HashSet 底层机制说明

12.2.2.2 LinkedHashSet

12.2.2.2.1 LinkedHashSet的全面说明

12.2.2.2.2 LinkedHashSet 源码解读

12.2.2.3 *TreeSet

12.3 Map

12.3.1 *HashMap

12.3.1.1 HashMap 底层机制及源码分析

12.3.1.2 HashMap 扩容树化触发

12.3.1.3 HashMap 小结

12.3.2 *Hashtable

12.3.2.1 Hashtable 的基本介绍

12.3.3 *TreeMap

12.3.4 *Properties

12.3.4.1 基本介绍

12.4 Collections

12.4.1 Collections 工具类介绍

12.5 总结

12.5.1 开发中如何选择集合实现类

第十二章 集合

12.1 集合框架体系

  • 可以动态保存任意多个对象,使用比较方便

  • 提供了一系列方便的操作对象的方法:add、remove、set、get等

  • 使用集合添加,删除新元素

12.2 Collection

  • Collection 接口实现类的特点 public interface Collection<E> extends Iterable<E>

    • Collection 实现子类可以存放多个元素,每个元素可以是Object

    • 有些Collection 的实现类,可以存放重复的元素,有些不可以

    • 有些Collection 的实现类,有些是有序的(List),有些不是有序(Set)

    • Collection 接口没有直接的实现子类,是通过它的子接口Set 和 List 来实现的

  • Collection 接口和常用方法 -> 由于接口不能实例化,因此用实现了该接口的子类ArrayList对象来演示

    • add:添加单个元素

    • remove:删除指定元素

    • contains:查找元素是否存在

    • size:获取元素个数

    • isEmpty:判断是否为空

    • clear:清空

    • addAll:添加多个元素

    • removeAll:删除多个元素

    • containsAll:查找多个元素是否都存在

  • Collection 接口遍历元素方式1 -> 使用Iterable(迭代器)

    • 基本介绍

      • Iterable对象称为迭代器,主要用于遍历 Collection 集合中的元素

      • 所有实现了 Collection 接口的集合类都有一个iterable()方法,用以返回一个实现了Iterable 接口的对象,既可以返回一个迭代器

      • Iterable 的结构图

      • Iterable 仅用于遍历集合, Iterable 本身并不存放对象

  • Collection col = new ArrayList();
    ​
            col.add(new Book("三国演义", "罗贯中", 10.1));
            col.add(new Book("小李飞刀", "古龙", 7.2));
            col.add(new Book("红楼梦", "曹雪芹", 37.5));
    ​
            //System.out.println("col=" + col);
            //现在希望能够遍历 col 集合
            //1. 先得到 col 对应的 迭代器
            Iterator iterator = col.iterator();
            //2. 使用 while 循环 遍历
            while (iterator.hasNext()) {//判断是否还有数据
                //返回下一个元素,类型是 Object
                Object obj = iterator.next();
                System.out.println("obj=" + obj);
            }
            while (iterator.hasNext()) {
                Object obj = iterator.next();
                System.out.println("obj=" + obj);
            }
            //3. 当迭代器退出 while 循环后 , 这时 iterator迭代器,指向最后的元素
            //iterator.next();//NoSuchElementException
            //4. 如果希望再次遍历,需要重置我们的迭代器
            iterator = col.iterator();//重置迭代器
            System.out.println("====第二次遍历====");
            while (iterator.hasNext()) {
                Object obj = iterator.next();
                System.out.println("obj=" + obj);
  • Collection 接口遍历元素方式2 -> for循环增强

    • 基本介绍

      • 增强for循环,可以代替iterator 迭代器,特点:增强 for 就是简化版的 iterator,本质一样。只能用于遍历集合或数组

    • 基本语法

      for ( 元素类型 元素名:集合名或数组名 ) {

      访问元素

      }

  • //1. 使用增强for,在Collection 集合
    //2. 增强for, 底层仍然是迭代器
    //3. 增强for 可以理解成  就是简化版本的  迭代器遍历I
    System.out.println("====第三次遍历====");
    for (Object obj : col) {
        System.out.println("obj=" + obj);
    }

12.2.1 List

  • List 接口基本介绍 -> List 接口是 Collection 接口的子接口

    • List 集合类中元素有序(即 添加顺序和取出顺序一致)、且可重复

    • List 集合中的每个元素都有其对应的顺序索引,即 支持索引

    • List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素

    • JDK API 中 List 接口 的实现类有 :ArrayList、LinkedList、Vector

  • List 接口的常用方法

    • void add(int index,Object ele):在index 位置插入ele元素

    • boolean addAll(int index,Collection eles):从index 位置开始将eles 中的所有元素添加进来

    • Object get(int index):获取指定index 位置的元素

    • int indexOf(Object obj):返回 obj 在集合中首次出现的位置

    • int lastIndexOf(Object obj):返回 obj 在集合中最后一次出现的位置

    • Object remove(int index):移除指定 index 位置的元素,并返回此元素

    • Object set(int index,Object ele):设置指定 index 位置的元素位 ele,相当于是替换

    • List subList(int fromIndex,int toIndex):返回从 fromIndex 到 toIndex 位置的子集合

  • List list = new ArrayList();
    list.add("张三丰");
    list.add("贾宝玉");
    //void add(int index,Object ele):在index 位置插入ele元素
    list.add(1, "Andy");
    System.out.println("list=" + list);
    ​
    //boolean addAll(int index,Collection eles):从index 位置开始将eles 中的所有元素添加进来
    List list2 = new ArrayList();
    list2.add("Wink");
    list2.add("zhang");
    list.addAll(1, list2);
    System.out.println("list=" + list);
    ​
    //Object get(int index):获取指定index 位置的元素
    ​
    //int indexOf(Object obj):返回 obj 在集合中首次出现的位置
    System.out.println(list.indexOf("Andy"));
    ​
    //int lastIndexOf(Object obj):返回 obj 在集合中最后一次出现的位置
    System.out.println(list.lastIndexOf("Andy"));
    ​
    //Object remove(int index):移除指定 index 位置的元素,并返回此元素
    list.remove(3);
    System.out.println("list=" + list);
    ​
    //Object set(int index,Object ele):设置指定 index 位置的元素为 ele,相当于是 替换
    list.set(0, "张无忌");
    System.out.println("list=" + list);
    ​
    //List subList(int fromIndex,int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
    //注意 返回的子集合  fromIndex <= subList < toIndex
    System.out.println(list.subList(0, 2));//返回索引为0,1的元素
  • 集合中按照价格交换元素顺序

  • int listSize = list.size();
    for (int i = 0; i < listSize - 1; i++) {
        for (int j = 0; j < listSize - i - 1; j++) {
            //取出对象Book   向下转型----
            Book book1 = (Book) list.get(j);
            Book book2 = (Book) list.get(j + 1);
            if (book1.getPrice() > book2.getPrice()) {
                list.set(j, book2);
                list.set(j + 1, book1);
            }
        }
    }
12.2.1.1 *ArrayList底层结构
12.2.1.1.1 ArrayList 的注意事项
  • permits all elements, including null, ArrayList 可以加入null,并且多个

  • ArrayList 是由数组来实现数据存储的

  • ArrayList 基本等同于Vector,除了 ArrayList 是线程不安全(执行效率高)看源码,在多线程情况下,不建议使用 ArrayList

12.2.1.1.2 ArrayList 的底层操作机制源码分析
  • ArrayList 中维护了一个Object类型的数组elementData。 transient Object[] elementData;//transient 表示瞬间,短暂的;表示该属性不会被序列化

  • 当创建ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0 ,第1次添加,则扩容 elementData 为10,如需要再次扩容,则扩容elementData 为 1.5 倍

  • 如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5 倍

12.2.1.2 *LinkedList底层结构
12.2.1.2.1 LinkedList 的全面说明
  • LinkedList 底层实现了双向链表和双端队列特点

  • 可以添加任意元素(元素可以重复),包括null

  • 线程不安全,没有实现同步

12.2.1.2.2 LinkedList 的底层操作机制
  • LinkedList 底层维护了一个双向链表

  • LinkedList 中维护了两个属性first 和 last 分别指向 首节点和尾节点

  • 每个节点 (Node对象),里面又维护了prev、next、item三个属性,其中通过prev 指向前一个,通过next 指向后一个节点。最终实现双向链表

  • 所以LinkedList 的元素的添加和删除,不是通过数组完成的,相对来说效率较高

  • 模拟一个简单的双向链表

  • public class LinkedList01 {
        public static void main(String[] args) {
    ​
            //模拟一个简单的双向链表
    ​
            Node jack = new Node("jack");
            Node tom = new Node("tom");
            Node andy = new Node("Andy");
    ​
            //连接三个结点,形成双向链表
            //jack -> tom -> andy
            jack.next = tom;
            tom.next = andy;
    ​
            //andy -> tom -> jack
            andy.pre = tom;
            tom.pre = jack;
    ​
            Node first = jack;//让 first 引用指向 jack,就是双向链表的  头  结点
            Node last = andy;//让 last 引用指向 andy,就是双向链表的  尾  结点
    ​
            //演示从头到尾进行遍历
            System.out.println("====演示从头到尾进行遍历====");
            while (true) {
                if (first == null) {
                    break;
                }
                //输出first 信息
                System.out.println(first);
                first = first.next;
            }
    ​
            //演示从尾到头进行遍历
            System.out.println("====演示从尾到头进行遍历====");
            while (true) {
                if (last != null) {
                    //输出last 信息
                    System.out.println(last);
                    last = last.pre;
                } else {
                    break;
                }
            }
    ​
            //要求,是在  tom--Andy 之间,插入一个对象  Wink
    ​
            //1.先创建一个 Node 结点, name  就是  Wink
            Node wink = new Node("Wink");
    ​
            //2.下面就把 Wink 加入到双向链表了
            tom.next = wink;
            wink.next = andy;
    ​
            andy.pre = wink;
            wink.pre = tom;
    ​
            //让 first 再次指向 jack
            first = jack;//让 first 重新指向头结点
            System.out.println("====演示从头到尾进行遍历====");
            while (true) {
                if (first == null) {
                    break;
                }
                //输出first 信息
                System.out.println(first);
                first = first.next;
            }
    ​
            last = andy;//让 last 重新指向尾结点
            //演示从尾到头进行遍历
            System.out.println("====演示从尾到头进行遍历====");
            while (true) {
                if (last != null) {
                    //输出last 信息
                    System.out.println(last);
                    last = last.pre;
                } else {
                    break;
                }
            }
    ​
        }
    }
    ​
    //定义一个Node类 ,Node对象表示双向链表的一个结点
    class Node {
        public Object item;//真正存放数据
        public Node next;//指向后一个结点
        public Node pre;//指向前一个结点
    ​
        public Node(Object name) {
            this.item = name;
        }
    ​
        public String toString() {
            return "Node name=" + item;
        }
    }
12.2.1.2.3 LinkedList 的增删改查案例
public class LinkedListCRUD {
    public static void main(String[] args) {
​
        LinkedList linkedList = new LinkedList();
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
​
        System.out.println("linkedlist=" + linkedList);
​
        //演示一个删除结点的
        linkedList.remove();//默认删除第一个结点
        //linkedList.remove(2);
​
        System.out.println("linkedlist=" + linkedList);
​
        //修改某个结点对象
        linkedList.set(1, 999);
​
        System.out.println("linkedlist=" + linkedList);
​
        //得到某个结点对象
        linkedList.get(1);//get(1) 是双向链表的第二个对象
​
        System.out.println("linkedlist=" + linkedList.get(1));
​
        //因为LinkedList 是 实现了 List接口,遍历方式一样
        System.out.println("====LinkedList遍历迭代器====");
        Iterator iterator = linkedList.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            System.out.println(obj);
        }
​
        System.out.println("====LinkedList遍历增强for====");
        for (Object o : linkedList) {
            System.out.println(o);
        }
​
        System.out.println("====LinkedList遍历传统for====");
        for (int i = 0; i < linkedList.size(); i++) {
            System.out.println(linkedList.get(i));
        }
​
​
        /**
         * 源码
         * 1.  LinkedList linkedList = new LinkedList();
         *     public LinkedList() { }
         * 2.  这时  linkedlist 的属性 first = null  last = null
         * 3.  执行  添加
         *       public boolean add(E e) {
         *           linkLast(e);
         *           return true;
         *     }
         * 4. 将新的结点,加入到双向链表的最后
         *     void linkLast(E e) {
         *         final Node<E> l = last;
         *         final Node<E> newNode = new Node<>(l, e, null);
         *         last = newNode;
         *         if (l == null)
         *             first = newNode;
         *         else
         *             l.next = newNode;
         *         size++;
         *         modCount++;
         *     }
         *
         */
    }
}
12.2.1.2.4 ArrayList 和 LinkedList 的比较
底层结构增删的效率改查的效率
ArrayList可变数组较低,数组扩容较高
LinkedList双向链表较高,通过链表追加较低

如何选择 ArrayList 和 LinkedList:

  • 如果我们改查的操作多,选择ArrayList

  • 如果我们增删的操作多,选择LinkedList

  • 一般来说,在程序中,80%~90%都是查询,因此大部分情况下会选择ArrayList

12.2.1.3 *Vector底层结构
12.2.1.3.1 Vector 的基本介绍
  • Vector 类的定义说明

    public class Vector<E>
        extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • Vector 底层也是一个对象数组,protected Object[] elementData;

  • Vector 是线程同步的,即线程安全,Vector 类的操作方法带有 synchronized

  • public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
    ​
        return elementData(index);
    }
  • 在开发中,需要线程同步安全时,考虑使用 Vector

  • Vector 和 ArrayList 的比较

    底层结构版本线程安全(同步)效率扩容倍数
    ArrayList可变数组jdk1.2不安全,效率高如果有参构造,每次直接按1.5倍扩;如果是无参,第一次是10个,第二次开始就按1.5倍扩容
    Vector可变数组jdk1.0安全,效率不高如果有参构造,每次直接按2倍扩;如果是无参,默认10个,满后,就按2倍扩容

12.2.2 Set

Set 接口基本介绍

  • 无序(添加和取出的顺序不一致),没有索引

  • 不允许重复元素,所以最多包含一个 null

Set 接口常用方法

  • 和 List 接口一样,Set 接口也是Collection 的子接口,因此,常用方法和 Collection 接口一样

Set 接口的遍历方式

  • 可以使用迭代器

  • 可以使用增强for

  • 不能使用索引的方式来获取

12.2.2.1 *HashSet
12.2.2.1.1 HashSet 的全面说明
  • HashSet 实现了 Set 接口

  • HashSet 实际上是HashMap,看源码

    public HashSet() {
        map = new HashMap<>();
    }
  • 可以存放null值,但是只能有一个null

  • HashSet 不保证元素是有序的,取决于hash后,再确定索引的结果(即不能保证存放元素的顺序和取出的顺序一致)

  • 不能有重复元素/对象,前面 Set 接口使用已经讲过

12.2.2.1.2 HashSet 底层机制说明

分析HashSet 底层是 HashMap,HashMap底层是(数组+链表+红黑树)

分析HashSet 的添加元素底层是如何实现(hash()+equals())

  • HashSet 底层是 HashMap

  • 添加一个元素时,先得到hash值,会转成  ->  索引值

  • 找到存储数据表 table,看这个索引位置是否已经存放的有元素

  • 如果没有,直接加入

  • 如果有,调用 equals 比较,如果相同,就放弃添加如果不相同,则添加到最后

  • 在Java8 中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是 8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树)

  • /*
    HashSet源码解读
    1. 执行 HashSet()
            public HashSet() {
                map = new HashMap<>();
            }
    2. 执行 add()
            public boolean add(E e) {
                return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();
            }
    3. 执行 put(), 该方法会执行  hash(key) 得到 key 对应的hash值  算法h = key.hashCode()^(h >>> 16)
           public V put(K key, V value) {//key = "java" value = PRESENT 共享
                return putVal(hash(key), key, value, false, true);
            }
    4. 执行  putVal()
           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[]
                //if 语句表示如果当前 table  是 null,或者 大小=0
                //就是第一次扩容,到16个空间.
                if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;
                //(1)根据key,得到的hash值, 去计算该key 应该存放到table 表的哪个索引位置
                //   并且把这个位置的对象,赋给 p
                //(2)判断p 是否为 null
                //(2.1)如果 p为 null,表示还没有存放元素,就创建一个Node(key = "java",value = PRESENT)
                //(2.2)就放在该位置  tab[i] = newNode(hash, key, value, 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值一样
                    // 并且满足 下面两个条件之一:
                    //(1) 准备加入的 key 和 p指向的Node 结点的 key 是同一个对象
                    //(2) p指向的Node 结点的 key 的equals() 和准备加入的 key比较后相同
                    //就不能加入
                    if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        e = p;
                    //再判断 p 是不是一颗红黑树,
                    //如果是一颗红黑树,就调用 putTreeVal , 来进行添加
                    else if (p instanceof TreeNode)
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                    //如果table 对应的索引位置,已经是一个链表,就是用for 循环比较
                    //(1) 依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
                    //    注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点
                    //    , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
                    //    注意, 再转成红黑树时,要进行判断,判断条件如下
                    //    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                    //            resize();
                    //    如果上面条件成立,先 table 扩容.
                    //    只有上面条件不成立时,才进行转成红黑树
                    //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
    ​
                    else {
                        for (int binCount = 0; ; ++binCount) { //无限循环
                            if ((e = p.next) == null) {
                                p.next = newNode(hash, key, value, null);
                                if (binCount >= TREEIFY_THRESHOLD(8) - 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;
                // size 就是我们每加入一个结点Node(h,k,v,next),size++
                   if (++size > threshold)
                       resize();//扩容       
                afterNodeInsertion(evict);
                return null;
            }
     */

分析HashSet的扩容和转成红黑树机制

  • HashSet 底层是HashMap,第一次添加时,table 数组扩容到 16,临界值 (threshold)是 16*加载因子(loadFactor) 是0.75 = 12

  • 如果 table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是 32*0.75 = 24,依此类推

  • 在Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树),否则仍然采用数组扩容机制

  • /*
    当我们向 hashSet 增加一个元素, -> Node -> 加入table ,就算是增加了一个 size-----当size 达到 12 时,数组便会进行扩容
     */
    ​
    for (int i = 1; i <= 7; i++) {//在 table 表的某一条链表上添加了 7个A对象
        hashSet.add(new A(i));
    }
    ​
    for (int i = 1; i <= 7; i++) {//在 table 表的某一条链表上添加了 7个B对象
        hashSet.add(new B(i));
    }
12.2.2.2 LinkedHashSet
12.2.2.2.1 LinkedHashSet的全面说明
  • LinkedHashSet 是 HashSet 的子类

  • LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组 + 双向链表

  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的

  • LinkedHashSet 不允许添加重复元素

12.2.2.2.2 LinkedHashSet 源码解读
  • 在 LinkedHashSet 中维护了一个 hash表和双向链表(LinkedHashSet 有 head 和 tail)

  • 每一个结点有 before 和 after 属性,这样可以形成双向链表

  • 在添加一个元素时,先求 hash值,再求索引. 确定该元素在table 的位置,然后将添加的元素加入到双向链表(如果已经存在,则不添加)

  • tail.next = newElement //示意代码
    newElement.pre = tail
    tail = newElement
  • 遍历 LinkedHashSet 也能确保插入顺序和遍历顺序一致

  • //1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致
    //2. LinkedHashSet 底层维护的是一个 LinkedHashMap(是HashMap的子类)
    //3. LinkedHashSet 底层结构 (数组+双向链表)
    //4. 添加第一次时,直接将 数组 table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
    //5. 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry类型
    /*
        //继承关系是在内部类完成.
        static class Entry<K,V> extends HashMap.Node<K,V> {
            Entry<K,V> before, after;
            Entry(int hash, K key, V value, Node<K,V> next) {
                super(hash, key, value, next);
            }
        }
     */
12.2.2.3 *TreeSet
public class TreeSet_ {
    public static void main(String[] args) {
​
        //1. 当我们使用无参构造器,创建TreeSet时,仍然是无序的
        //2. 希望添加的元素,按照字符串大小来排序
        //3. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
        //   并制定排序规则
 
        TreeSet treeSet = new TreeSet();
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //下面 调用String 的 compareTo() 方法
                //如果要求加入的元素,按照长度大小排序
                return ((String) o1).compareTo((String) o2);//默认按照首字母顺序比较
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        //添加数据.
        treeSet.add("jack");
        treeSet.add("python");
        treeSet.add("Andy&Wink");
        treeSet.add("eq");//2
        treeSet.add("ff");//2
​
        System.out.println(treeSet);
​
        /*
          源码
          1. 构造器底层把传入的比较器对象,赋给了TreeSet底层的TreeMap的属性this.comparator
           public TreeMap(Comparator<? super K> comparator) {
                this.comparator = comparator;
            }
          2. 在 调用 treeSet.add()方法时,在底层会执行到
              if (cpr != null) { //cpr 就是我们的匿名内部类(对象)
                    do {
                        parent = t;
                        //动态绑定到我们的匿名内部类(对象)compare
                        cmp = cpr.compare(key, t.key);
                        if (cmp < 0)
                            t = t.left;
                        else if (cmp > 0)
                            t = t.right;
                        else //如果相等,即返回0,这个Key就没有加入
                            return t.setValue(value);
                    } while (t != null);
              }
         */
    }
}

12.3 Map

Map 接口实现类的特点

  • Map 与 Collection 并列存在。用于保存具有映射关系的数据:Key--Value

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

  • Map 中的 key 不允许重复,原因和HashSet 一样,前面分析过源码

  • Map 中的value 可以重复

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

  • 常用 String 类作为 Map 的 key

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

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

  • Map map = new HashMap();
    map.put("no1", "Andy");//k-v
    map.put("no2", "Wink");//k-v
    map.put(new Car(), new Person());//k-v
    ​
    //1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null)
    //2. k-v 为了方便程序员的遍历,还会 创建 EntrySet 集合 , 该集合存放的元素的类型是 Entry, 而一个Entry
    //   对象就有k,v EntrySet<Entry<K,V>> 即, transient Set<Map.Entry<K,V>> entrySet;
    //3. entrySet 中, 定义的类型是 Map.Entry , 但是实际上存放的还是 HashMap$Node -----由于Node 实现了 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());// HashMap$EntrySet
    ​
    for (Object obj : set) {
        //System.out.println(entry.getClass());// HashMap$Node
        //为了从 HashMap$Node 取出 k-v
        //1. 先做一个向下转型
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "-" + entry.getValue());
    }
    ​
    Set set1 = map.keySet();
    System.out.println(set1.getClass());//HashMap$KeySet
    Collection values = map.values();
    System.out.println(values.getClass());//HashMap$Values

Map 接口和常用方法

  • put:添加

  • remove:根据键删除映射关系

  • get:根据键获取值

  • size:获取元素个数

  • isEmpty:判断个数是否为0

  • clear:清除

  • containsKey:查找键是否存在

Map 接口遍历方法

  • containsKey:查找键是否存在

  • keySet:获取所有的键

  • values:获取所有的值

  • entrySet:获取所有关系

  • //第一组:先取出所有的 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));
    }
    ​
    ​
    //第二组:把所有的 values 取出
    System.out.println("-----取出所有的value 增强for-----");
    Collection values = map.values();
    //(1)增强for
    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(第3种)-----");
    for (Object obj : entrySet) { //EntrySet
        //将 entry 转成 Map.Entry
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "-" + entry.getValue());
    }
    ​
    //(2)迭代器
    System.out.println("-----使用EntrySet 的 迭代器(第4种)-----");
    Iterator iterator2 = entrySet.iterator();
    while (iterator2.hasNext()) {
        Object entry = iterator2.next();//HashMap$Node
        Map.Entry m = (Map.Entry) entry;
        System.out.println(m.getKey() + "-" + m.getValue());
    }

12.3.1 *HashMap

12.3.1.1 HashMap 底层机制及源码分析
  • HashMap 底层维护了 Node 类型的数组 table ,默认为 null

  • 当创建对象时,将加载因子(loadfactor) 初始化为0.75

  • 当添加 key-val 时,通过 key 的哈希值得到在 table 的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key 是否 和准备加入的key 相等。如果相等,则直接替换 val,如果不相等需要判断是树结构还是链表结构,做出相应的处理。如果添加时发现容量不够,则需要扩容。

  • 第1次添加,则需要扩容table 容量为 16,临界值(threshold)为 12

  • 以后再扩容,则需要扩容table 容量为原来的2倍,临界值为原来的 2倍,依此类推

  • 在Java8 中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)

12.3.1.2 HashMap 扩容树化触发
public class HashMapSource02 {
    public static void main(String[] args) {
​
        HashMap hashMap = new HashMap();
        for (int i = 1; i <= 12; i++) {
            hashMap.put(new A(i), "Andy");
        }
        System.out.println(hashMap);
    }
}
12.3.1.3 HashMap 小结
  • Map 接口的常用实现类:HashMap、Hashtable 和 Properties

  • HashMap 是 Map 接口使用频率最高的实现类

  • HashMap 是以 key-val 对的方式来存储数据

  • key 不能重复,但是value 可以重复,允许使用 null 键和 null 值

  • 如果添加相同的 key,则会覆盖原来的 key-val,等同于修改。(key 不会替换,val 会替换)

  • 与 HashSet 一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的

  • HashMap 没有实现同步,因此是线程不安全的

12.3.2 *Hashtable

12.3.2.1 Hashtable 的基本介绍
  • 存放的元素是键值对:即 K-V

  • hashtable 的键和值都不能为 null

  • hashtable 使用方法基本上和 HashMap 一样

  • hashtable 是线程安全的,hashMap 是线程不安全的

  • 简单看下底层结构

    /*
       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 = (oldCapacity << 1) + 1; 的大小扩容
     */

12.3.3 *TreeMap

public class TreeMap_ {
    public static void main(String[] args) {
​
        //使用默认构造器,创建 TreeMap, 是无序的
        /*  
        源码同 TreeSet 一致
         */
        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
//                return ((String) o2).compareTo((String) o1);
                return (((String) o2).length()) - (((String) o1).length());
            }
        });
        treeMap.put("jack", "杰克");
        treeMap.put("tom", "汤姆");
        treeMap.put("smith", "史密斯");
        treeMap.put("Wink$Andy", "机器学习");
        treeMap.put("aaa", "大姆");//key 加入不了  value 替换
​
        System.out.println(treeMap);
    }
}

12.3.4 *Properties

12.3.4.1 基本介绍
  • Properties 类继承自 Hashtable 类并且实现了 Map 接口,也是使用一种键值对的形式来保存数据

  • 它的使用特点和 Hashtable 类似

  • Properties 还可以用于 从 xxx.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改

12.4 Collections

12.4.1 Collections 工具类介绍

  • Collections 是一个操作 Set、List 和 Map 等集合的工具类

  • Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作

    • 排序操作:(均为 static 方法)

      • reverse(List):反转 List 中元素的顺序

      • shuffle(List):对 List 集合元素进行随机排序

      • sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序

      • sort(List, Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序

      • swap(List, int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

    • 查找、替换

      • Object max (Collection):根据元素的自然顺序,返回给定集合中最大元素

      • Object max (Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素

      • Object min (Collection):根据元素的自然顺序,返回给定集合中最小元素

      • Object min (Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素

      • int frequency(Collection, Object):返回指定集合中指定元素的出现次数

      • void copy(List dest, List src):将 src 中的内容复制到 dest 中

      • boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换 List 对象的所有旧值

12.5 总结

12.5.1 开发中如何选择集合实现类

在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:

  • 先判断存储的类型(一组对象或一组键值对)

  • 一组对象:Collection 接口

    • 允许重复:List

      • 增删多:LinkedList 【底层维护了一个双向链表】

      • 改查多:ArrayList 【底层维护 Object 类型的可变数组】

    • 不允许重复:Set

      • 无序:HashSet 【底层是 HashMap ,维护了一个哈希表 即(数组+链表+红黑树)】

      • 排序:TreeSet

      • 插入和取出顺序一致:LinkedHashSet , 维护数组+双向链表

  • 一组键值对:Map

      • 键无序:HashMap 【底层是:哈希表 jdk7:数组+链表, jdk8:数组+链表+红黑树】

      • 键排序:TreeMap

      • 键插入和取出顺序一致:LinkHashMap

      • 读取文件:Properties

  • 18
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值