• 22-集合(上)


    1. 概述

    • 一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用 Array 存储对象方面具有一些弊端,而 Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中
      • 数组在内存存储方面的特点
        • 数组初始化以后,长度就确定了
        • 数组声明的类型,就决定了进行元素初始化时的类型
      • 数组在存储数据方面的弊端
        • 数组初始化以后,长度就不可变了,不便于扩展
        • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。 同时无法直接获取存储元素的个数
        • 数组存储的数据是有序的、可以重复的 → 对于无序、不可重复的需求,不能满足
    • Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。Java 集合可分为 CollectionMap 两种体系
      • Collection 接口:单列数据,定义了存取一组对象的方法的集合
      • Map 接口:双列数据,保存具有映射关系 "key-value对" 的集合
    • 集合的使用场景

    2. Collection 接口

    • Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法 既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合
    • JDK 不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List) 实现
    • 在 JDK 5.0 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了 [泛型] 以后,Java 集合可以记住容器中对象的数据类型
    • 接口方法
      • boolean add(Object obj) / addAll(Collection<?> coll):添加元素/某集合的所有元素
      • int size():获取有效元素个数
      • void clear():清空集合
      • boolean isEmpty():是否是空集合
      • boolean contains(Object obj):是通过元素的 equals() 来判断是否是同一个对象
      • boolean containsAll(Collection<? extends E> c):也是调用元素的 equals() 来比较的。拿两个集合的元素挨个比较
      • boolean remove(Object obj):通过元素的 equals() 判断是否是要删除的那个元素。只会删除找到的第 1 个元素
      • boolean removeAll(Collection<?> coll):取当前集合的差集
      • boolean retainAll(Collection<?> c):把交集的结果存在当前集合(this) 中,不影响形参集合 c
      • boolean equals(Object obj):集合是否相等
      • Object[] toArray():转成对象数组
        // ↑→ public static <T> List<T> asList(T... a)
        List list = Arrays.asList(new Integer[]{1, 2, 3});
        System.out.println(list); // [1, 2, 3]
        
        List list2 = Arrays.asList(new int[]{1, 2, 3}); // 基本类型数组被当作一个元素
        System.out.println(list2); // [[I@4554617c]
        
        List list3 = Arrays.asList(1,2, 3);
        System.out.println(list3); // [1, 2, 3]
        
      • int hashCode():获取集合对象的哈希值
      • Iterator<E> iterator():返回迭代器对象,用于集合遍历

    3. Iterator迭代器接口

    3.1 概述

    • public interface Collection<E> extends Iterable<E>
    • Iterator 对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素
    • GOF 给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生
    • Collection<I> 继承了 java.lang.Iterable<I>,该接口有一个 iterator(),那么所有实现了Collection<I> 的集合类都有一个 iterator(),用以返回一个实现了 Iterator<I> 的对象
    • Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合
    • 集合对象每次调用 iterator() 都得到一个全新的迭代器对象,游标(cursor) 默认都在集合的第 1 个元素之前

    3.2 三个方法

    3.1 遍历集合元素

    • boolean hasNext():判断 iterator 内是否存在下1个元素,如果存在,返回true,否则返回false(注意,这时上面的那个指针位置不变)
    • E next():返回 iterator 内下1个元素,同时上面的指针向后移动一位。如果不断地循环执行next()方法,就可以遍历容器内所有的元素了
    • void remove():删除 iterator 内指针的前1个元素,前提是至少执行过1次 next()

    迭代的错误写法 // 在调用 it.next() 之前必须要调用 it.hasNext() 进行检测。若不调用,且下一条记录无效,直接调用 it.next() 会抛出 NoSuchElementException

    Iterator it = c.iterator();
    Object obj;
    while ((obj = it.next()) != null) // 第 c.size() + 1 次,会抛异常
        System.out.println(obj);
    

    3.2 删除集合元素

    Iterator iter = coll.iterator();
    while(iter.hasNext()) {
        Object obj = iter.next();
        if(obj.equals("Tom")) {
            iter.remove();
        }
    }
    
    • Iterator 可以删除集合的元素,但是是遍历过程中是通过迭代器对象的 remove(),不是集合对象的 remove(obj)
    • 如果还未调用 next() 或在上一次调用 next() 之后已经调用了 remove(), 再调用 remove() 都会报 IllegalStateException

    3.4 foreach

    JDK5.0 起,提供了 for each 循环迭代访问 Collection 和 数组。编译器简单地将 for each 循环翻译为带有迭代器的循环。 for each 循环可以与任何实现了 Iterator<I> 的对象一起工作,这个接口直播暗含了一个方法:Iterator<E> iterator()

    Collection<I> 扩展了 Iterator<I>。因此,对于标准类库中的任何集合都可以使用 for each 循环。

    4. List 接口

    • 鉴于 Java 中数组用来存储数据的局限性,我们通常使用 List 替代数组
    • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引
    • List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
    • JDK API中 List<I> 的实现类常用的有:ArrayListLinkedListVector

    4.1 ArrayList 源码

    JDK 7

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
    
        private transient Object[] elementData;
        
        private int size;
        
        public ArrayList() {
            this(10);
        }
        
        public ArrayList(int initialCapacity) {
            super();
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            this.elementData = new Object[initialCapacity];
        }
    
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
        private void ensureCapacityInternal(int minCapacity) {
            modCount++;
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
        
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            // 新长度是加上原来的一半
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    
        private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
        }
    }
    

    JDK 8

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
    
        private static final int DEFAULT_CAPACITY = 10;
    
        transient Object[] elementData;
    
        private int size;
    
        public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
        
        // 初始长度 0
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
        
        private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            ensureExplicitCapacity(minCapacity);
        }
    
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
        
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }
    
    • JDK 7
      • ArrayList list = new ArrayList(); // 底层创建了长度是 10 的 Object[] elementData
      • list.add(1); // elementData[0] = new Integer(1);
      • list.add(11); // 如果此次的添加导致底层 elementData[] 容量不够,则扩容
      • 默认情况下,扩容为原来的容量的 1.5 倍,同时需要将原有数组中的数据复制到新的数组中
      • [结论] 建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
    • JDK 8
      • ArrayList list = new ArrayList(); // 底层 Object[] elementData 初始化为{}
      • list.add(123); // 第一次调用 add() 时,底层才创建了长度 10 的数组,并将数据123 添加到 elementData[0]
      • 后续的添加和扩容操作与 JDK 7 无异
    • 小结
      • JDK 7 中的 ArrayList 的对象的创建类似于单例的饿汉式
      • JDK 8 中的 ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存

    4.2 LinkedList 源码

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    {
        // 实现序列化接口后,不想被序列化的成员变量前加 transient
        transient int size = 0;
        
        transient Node<E> first;
    
        transient Node<E> last;
    
        public LinkedList() {}
        
        // 双向链表
        private static class Node<E> {
            E item;
            Node<E> next;
            Node<E> prev;
    
            Node(Node<E> prev, E element, Node<E> next) {
                this.item = element;
                this.next = next;
                this.prev = prev;
            }
        }
    
        public boolean add(E e) {
            linkLast(e);
            return true;
        }
    
        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++;
        }
    
        public void addFirst(E e) {
            linkFirst(e);
        }
    
        private void linkFirst(E e) {
            final Node<E> f = first;
            final Node<E> newNode = new Node<>(null, e, f);
            first = newNode;
            if (f == null)
                last = newNode;
            else
                f.prev = newNode;
            size++;
            modCount++;
        }
    }
    

    4.3 Vector 源码

    public class Vector<E>
        extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
    
        protected Object[] elementData;
    
        protected int elementCount;
    
        protected int capacityIncrement;
    
        public Vector() {
            this(10);
        }
    
        public Vector(int initialCapacity) {
            this(initialCapacity, 0);
        }
    
        public Vector(int initialCapacity, int capacityIncrement) {
            super();
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            this.elementData = new Object[initialCapacity];
            this.capacityIncrement = capacityIncrement;
        }
    
        public synchronized boolean add(E e) {
            modCount++;
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
        }
    
        private void ensureCapacityHelper(int minCapacity) {
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
    
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                             capacityIncrement : oldCapacity);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }
    

    4.4 List 接口常用方法

    • void add(int index, E ele):在 index 位置插入 ele
    • boolean addAll(int index, Collection<? extends E> c):从 index 位置开始将 c 中的所有元素添加进来
    • E get(int index):获取指定 index 位置的元素
    • int indexOf(E obj):返回 obj 在集合中首次出现的位置
    • int lastIndexOf(E obj):返回 obj 在集合中末次出现的位置
    • E remove(int index):移除指定 index 位置的元素,并返回此元素
    • E set(int index, E ele):设置指定 index 位置的元素为 ele
    • List<E> subList(int fromIndex, int toIndex):返回 [fromIndex, toIndex) 位置的子集合

    5. Set

    • Set<I>Collection 的子接口,Set<I> 没有提供额外的方法
    • Set<I> 不允许包含相同(根据 equals())的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败
    • Set<I> 存储无序、不可重复的数据
      • 无序性:不等于随机性。存入底层数组的数据中并非按照数组索引顺序存放,而是根据数据的哈希值决定
      • 不可重复性:保证添加的元素按照 equals() 判断时,不能返回 true,即相同的元素不能重复添加

    5.1 散列


    5.2 HashSet

    • HashSet 是 Set<I> 的典型实现,大多数时候使用 Set 集合时都使用这个实现类
    • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能
    • HashSet 具有以下特点
      • 不能保证元素的排列顺序
      • HashSet 不是线程安全的
      • 集合元素可以是 null
    • HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 比较相等,并且两个对象的 equals() 返回值也相等
    • 对于存放在 Set 容器中的对象,对应的类一定要重写 equals()hashCode(),以实现对象相等规则。即:"相等的对象必须具有相等的散列码"
      • 重写 hashCode() 的基本原则
        • 在程序运行时,同一个对象多次调用 hashCode() 应该返回相同的值
        • 当两个对象的 equals() 比较返回 true 时,这两个对象的 hashCode() 的返回值也应相等
        • 对象中用作 equals() 比较的 Field,都应该用来计算 hashCode 值
      • 重写 equals() 的基本原则
        • 当一个类有自己特有的“逻辑相等”概念,当改写 equals() 的时候,总是要改写 hashCode(),根据一个类的 equals()(改写后),两个截然不同的实例有可能在逻辑上是相等的。但是,根据 Object.hashCode(),它们仅仅是两个对象, 因此,违反了 "相等的对象必须具有相等的散列码"
        • 【结论】复写 equals() 的时候一般都需要同时复写 hashCode()。通常参与计算 hashCode() 的对象的属性也应该参与到 equals() 中进行计算
    • 为什么用 Eclipse/IDEA 自动复写 hashCode(),有 31 这个数字?

    5.2.2 add

    HashSet 底层:数组 + 链表的结构

    当向 HashSet 集合中存入一个元素 a,HashSet 首先调用元素 a 所在类的 hashCode(),计算元素 a 的 hashCode 值,此 hashCode 值接着通过某种散列函数计算出在 HashSet 底层数组中的存放位置(这个散列函数会与底层数组的长度相计算得到在 数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布, 该散列函数设计的越好) ,判断数组此位置上是否已经有元素:

    • 如果此位置上没有其他元素,则元素 a 添加成功 ---> [情况1]
    • 如果此位置上有其他元素 b(或以链表形式存在的多个元素),则比较元素 a 与元素 b 的 hash 值
      • 如果 hash 值不相同,则元素 a 添加成功 ---> [情况2]
      • 如果 hash 值相同,进而需要调用元素 a 所在类的 equals()
        • equals() 返回 true,则元素 a 添加失败
        • equals() 返回 false,则元素 a 添加成功 ---> [情况3]

    对于添加成功的 [情况2] 和 [情况3] 而言,元素 a 与已经存在指定索引位置上数据以链表的方式存储:

    • JDK 7:元素 a 放到数组中,指向原来的数组元素
    • JDK 8:链表尾元素指向元素 a // 总结:七上八下

    5.2.3 例题

    public void test() {
        HashSet set = new HashSet();
        Person p1 = new Person("AA",21);
        Person p2 = new Person("BB",22);
        set.add(p1);
        set.add(p2);
        p1.name = "CC";
        set.remove(p1);
        System.out.println(set);
        set.add(new Person("CC",21));
        System.out.println(set);
        set.add(new Person("AA",22));
        System.out.println(set);
    }
    -------------------------------------
    [Person{name='CC', age=21}, Person{name='BB', age=22}]
    [Person{name='CC', age=21}, Person{name='BB', age=22}, Person{name='CC', age=21}]
    [Person{name='CC', age=21}, Person{name='BB', age=22}, Person{name='CC', age=21}, Person{name='AA', age=22}]
    

    5.2.4 LinkedHashSet

    • LinkedHashSet 是 HashSet 的子类
    • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的
    • LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能
    • LinkedHashSet 不允许集合元素重复

    5.3 TreeSet

    • TreeSetSortedSet<I> 的实现类,TreeSet 可以确保集合元素处于排序状态
    • TreeSet 底层使用 [红黑树] 结构存储数据
    • 常用方法
      • Comparator<? super E> comparator()
      • E first()
      • E last()
      • E lower(Object e)
      • E higher(Object e)
      • SortedSet<E> subSet(fromElement, toElement)
      • SortedSet<E> headSet(toElement)
      • SortedSet<E> tailSet(fromElement)
    • TreeSet 两种排序方法:[自然排序] 和 [定制排序]。默认情况下,TreeSet 采用自然排序

    5.3.1 自然排序

    TreeSet 会调用集合元素的 compareTo(T obj) 来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。所以,如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable<I>

    Comparable 的典型实现:

    • 向 TreeSet 中添加元素时,只有第一个元素无须比较 compareTo(),后面添加的所有元素都会调用 compareTo() 进行比较
    • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同 一个类的对象
    • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(T obj) 比较返回值
    • 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 时,应保证该方法与 compareTo(T obj) 有一致的结果:如果两个对象通过 equals() 比较返回 true,则通过 compareTo(T obj) 比较应返回 0。 否则,让人难以理解。

    5.3.2 定制排序

    • TreeSet 的自然排序要求元素所属的类实现 Comparable<I>,如果元素所属的类没有实现 Comparable<I>,或不希望按照升序(默认情况)的 方式排列元素或希望按照其它属性大小进行排序,则考虑使用 [定制排序]。定制排序,通过 Comparator<I> 来实现,需要重写 compare(T o1, T o2)
    • 利用 int compare(T o1, T o2),比较 o1 和 o2 的大小:如果方法返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负整数,表示 o1 小于 o2。
    • 使用 [定制排序] 判断两个元素相等的标准是:通过 Comparator 比较两个元素返回了 0。
    • 要实现定制排序,需要将实现 Comparator<I> 的实例作为形参传递给 TreeSet 的构造器:TreeSet(Comparator<? super E> comparator)。此时,仍然只能向 TreeSet 中添加类型相同的对象。否则发生 ClassCastException
  • 相关阅读:
    深度学习(机器学习)tensorflow环境配置及安装
    深度学习(机器学习)tensorflow学习第一课——tensor数据载体中的基本数据类型
    学生选课小项目——麻雀虽小,五脏俱全(Templates)
    素数定理简史
    关于全栈岗位及其成员轮岗问题的思考
    Spring WebFlux 简单业务代码及其Swagger文档
    Spring 5 中函数式webmvc开发中的swagger文档
    创建基于kotlin开发环境的spring项目入门
    折射定律的来历简介
    朴素贝叶斯方法入门
  • 原文地址:https://www.cnblogs.com/liujiaqi1101/p/13289266.html
Copyright © 2020-2023  润新知