Loading

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
posted @ 2020-07-12 18:12  tree6x7  阅读(111)  评论(0编辑  收藏  举报