上帝视角学JAVA- 基础14-集合01【2021-08-20】

1、集合概述

为了方便的对对象进行操作和存储,Java 提供了集合这个工具。前面已经讲过了使用数组来进行数据存储,但是数组有很多弊端,比如不支持动态扩展,一旦声明了数组元素类型就不可再变。

  • 数组初始化之后,长度就固定了,不利于拖拽

  • 声明了类型之后,就不可再变

  • 数组提供的属性和方法少,不利于添加、删除、插入等操作,而且效率较低。同时无法直接获取存储元素的个数

  • 数组存储元素是有序的,可重复的。特点单一

为了解决这些缺点,出现了集合。

共同点:

  • 都是用来对数据的存储,这里指的是内存的存储,而不是持久化到硬盘的存储。

集合的优势:

  • 长度可变

  • 提供了添加、删除、插入、查找等便捷高效的方法

  • 存储元素可以无序、可以保证不重复

JAVA 的集合主要分为 Collection 和 Map 2种接口。

2、Collection 接口

2.1 Collection 接口 详解

单列数据接口、主要包括2个子接口 List、Set

上帝视角学JAVA- 基础14-集合01【2021-08-20】_第1张图片

 

实线为继承关系,虚线为实现关系。

public interface Collection extends Iterable {
​
    int size();
​
    boolean isEmpty();
​
    boolean contains(Object o);
​
    Iterator iterator();
​
    Object[] toArray();
​
     T[] toArray(T[] a);
​
    boolean add(E e);
​
    boolean remove(Object o);
​
    boolean containsAll(Collection c);
​
    boolean addAll(Collection c);
​
    boolean removeAll(Collection c);
​
​
    default boolean removeIf(Predicate filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }
​
    boolean retainAll(Collection c);
​
    void clear();
​
    // Comparison and hashing
​
    boolean equals(Object o);
​
    int hashCode();
​
    @Override
    default Spliterator spliterator() {
        return Spliterators.spliterator(this, 0);
    }
 
    default Stream stream() {
        return StreamSupport.stream(spliterator(), false);
    }
​
    default Stream parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

以上为 JDK 8.0 的Collection 源码。就是一个普通接口,继承了 Iterable 接口

public interface Iterable {
​
    Iterator iterator();
    
    default void forEach(Consumer action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    
    default Spliterator spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Iterable 接口有一个 iterator 抽象方法,返回值是 Iterator 接口的实现类。

public interface Iterator {
​
    boolean hasNext();
​
    E next();
​
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
​
    default void forEachRemaining(Consumer action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

Collection 接口继承 Iterable 接口,重写了里面的默认方法 spliterator。 并没有重写 iterator 方法。

Iterator 接口的实现类对象称为迭代器、是设计模式中的一种。迭代器的定义:提供一种方法访问一个容器对象中的各个元素,而不暴露该对象的内部细节。迭代器模式就是为容器而生。

2.1.1 方法详解

Collection 里面有 很多的抽象方法,方便的进行增删改查。

使用 ArrayList 来测试 Collection 接口。因为接口是不能直接new对象的。必须是new实现类。

  • add方法: 添加元素; size方法: 获取元素个数

Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
coll.add(new Date());
​
System.out.println(coll.size()); // 4
  • addAll 方法: 添加另一个集合的所有元素; isEmpty 方法:判断集合是否没有元素(注意不是判断null)

Collection coll = new ArrayList();
Collection coll2 = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
coll.add(new Date());
​
coll2.add("456");
coll2.add("GGG");
// 将 coll2 集合的全部元素添加到 coll中
coll.addAll(coll2); 
System.out.println(coll.size()); // 6
// 判断coll2 集合是否没有元素
System.out.println(coll2.()); // false 即不为空,coll2里面有元素。
System.out.println(coll); // [AA, BB, 123, Tue Aug 17 19:40:48 CST 2021, 456, GGG]
  • clear方法:清空集合内所有元素

Collection coll = new ArrayList();
coll.add("AA");
coll.add("BB");
​
coll.clear();
System.out.println(coll.isEmpty()); // true
  • contains 方法: 判断集合内是否包含某个元素。 内部判断相等是调用equals方法。没有重写就是判断对象的地址。

因此,如果添加自定义类型对象Class类型的,需要重写equals方法。

Collection coll = new ArrayList();
coll.add("123");
coll.add("555");
coll.add(new String("tom"));
coll.add(false);
​
boolean tom = coll.contains("tom");
System.out.println(tom); // true
boolean tom1 = coll.contains("Tom");
System.out.println(tom1); // false
System.out.println(coll); // [123, 555, tom, false]
  • containsAll 方法:判断集合内的元素是否包含另一个集合的所有元素。同样是调用equals方法进行判断相等的。

Collection coll = new ArrayList();
Collection coll2 = new ArrayList();
Collection coll3 = new ArrayList();
coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱
​
coll2.add("456");
coll2.add("GGG");
coll2.add("ddd");
​
coll3.add("456");
coll3.add("GGG");
boolean b = coll.containsAll(coll2);
System.out.println(b); // false
boolean b1 = coll2.containsAll(coll3);
System.out.println(b1); // true
  • remove(Object o) 方法: 移除元素。 内部还是会调用equals方法,只有先判断相等才能移除。

Collection coll = new ArrayList();
Collection coll2 = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

boolean remove = coll.remove(123);
System.out.println(remove); // true
boolean remove1 = coll.remove(456);
System.out.println(remove1); // false
System.out.println(coll);  // [AA, BB]
  • removeAll(Collection c) 方法:移除输入集合内的所有元素。只能移除输入集合与被移除集合的公共元素,只要移除了1个元素就会返回true。一个都没有移除返回false。通用涉及到判断相等,调用equals方法。自定义类需要重写equals方法。

    类似于求2个集合的差集,被移除集合只剩下除公共元素外的元素。

Collection coll = new ArrayList();
Collection coll2 = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

coll2.add(123);
coll2.add(456);

boolean b = coll.removeAll(coll2); 
System.out.println(b); // true
System.out.println(coll); // [AA, BB]
  • retain (Collection c)方法: 求 输入集合与 原集合的交集。如果有交集返回true,否则返回false 。当返回值为true时,原计划元素为交集元素; 当返回值为false时,由于没有交集,原计划变成空集。 这个操作还是会调用equals方法。

    注意这个操作也是对原集合直接修改的。

Collection coll = new ArrayList();
Collection coll2 = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

coll2.add(123);
coll2.add(456);

boolean b = coll.retainAll(coll2);
System.out.println(b); // true
System.out.println(coll); // [123]
  • equals(Object o) 方法: 判断输入的对象与原集合是否相等。 参数是object对象,说明可以传任意值。

    但是只有传与原集合元素一致的集合时,才会返回true。

    注意:这个一致还得看集合是否是有序的,如果是List,集合元素要求顺序和值都相同才会返回true;对于无序的Set,只要求元素相同就会返回true。

Collection coll = new ArrayList();
Collection coll2 = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

coll2.add(12);
coll2.add(456);

boolean equals = coll.equals(coll2);
System.out.println(equals); // false
  • hashCode 方法 返回当前对象的哈希值。这是是object的方法。所有的对象都有这个方法。

Collection coll = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

int i = coll.hashCode();
System.out.println(i); // 2094266
  • toArray () 方法 : 集合转换为数组

Collection coll = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

Object[] arr = coll.toArray();
for (Object o : arr) {
	System.out.print(o + "\t");
}
//AA	BB	123

数组也可以转换为集合,使用 Arrays.asList() 方法即可

List strings = Arrays.asList(new String[]{"aa", "bb"});
System.out.println(strings);
// [aa, bb]

// 这样写会将 int[] 当成集合的一个元素。
List ints = Arrays.asList(new int[]{123, 456});
System.out.println(ints);
// [[I@5c8da962]

List integers = Arrays.asList(new Integer[]{123, 456});
System.out.println(integers);
// [123, 456]

List list = Arrays.asList(123, 456);
System.out.println(list);
// [123, 456]

2.1.2 迭代与遍历

  • iterator() 方法:用于集合的遍历

这个方法就是继承的是 Iterable 接口的方法 iterator。iterator方法返回值是一个 Iterator 实现类对象。即迭代器对象。

Collection coll = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

// 得到一个迭代器对象,使用Iterator接口接收Iterator实现类的对象。
Iterator iterator = coll.iterator();
while (iterator.hasNext()){
	System.out.println(iterator.next());
}

hasNext 和 next 都是 Iterator 接口中定义的方法。

hasNext 方法 就判断一下还有没有元素。

next 方法就是获取下一个元素。

那么这个 Iterator 实现类对象,是如何实现的 这2个方法hasNext 、next 呢?

Iterator iterator = coll.iterator();
System.out.println(iterator.getClass()); // class java.util.ArrayList$Itr

先看一下这个实现类对象究竟是什么:class java.util.ArrayList$Itr 是ArrayList里面的Itr这个类的对象。

下面是 ArrayList类里面的 Itr 内部类的源码:

private class Itr implements Iterator {
        int cursor;       // 要返回的下一个元素的索引
        int lastRet = -1; // 返回的最后一个元素的索引,如果没有,则返回 -1
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

原理就是 内部维护了一个 cursor ,这个cursor 叫做指针、游标、索引 都可以。就是指向下一个元素的索引。

hasNext方法 是判断 cursor 索引是否和容器个数相等。相等就没有下一个,不相等就有。cursor是使用默认初始化为0的

next 方法 会先判断 cursor 与 容器个数 size 的关系。通过验证之后,cursor就会 移动一位 + 1,然后取出cursor+1之前位置的元素返回。

里面还有remove方法。移除元素。这个方法没有参数,内部实现是删除 lastRet 这个索引对应的值。删除这个值还是调用的ArrayList的remove(int index)方法。

Collection coll = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

Iterator iterator = coll.iterator();
while (iterator.hasNext()){
    Object next = iterator.next();
    if ( "BB".equals(next)){
        // 调用迭代器的remove方法
        iterator.remove();
    }
}
// 增强for循环遍历。后面会讲到
for (Object o : coll) {
    System.out.println(o);
}
// 输出
//AA
//123

这个remove内部实现是依赖于 lastRet,lastRet初始值是-1,所有一开始不能直接调用remove方法。得先调用next方法改变lastRet值。才能调用remove方法。即调用remove方法之前需要调用next方法。

2.1.3 foreach 循环

这个 foreach 是JDK5.0 新增的特性。用于遍历集合、数组

Collection coll = new ArrayList();

coll.add("AA");
coll.add("BB");
coll.add(123); // 自动装箱

// 增强for循环
for (Object o : coll) {
    System.out.print(o + "\t");
}
// AA	BB	123

就是上一步写的增强for循环。与普通for循环的区别在于,可以直接的遍历每一个元素,但是也无法直接获取元素的下标了。

格式:

for(集合元素类型 变量名: 集合对象){}

变量名自己取。增强for还可以遍历数组。 那么就是格式就是: for(数组元素类型变量名:数组对象){}

这个增强for循环内部实现原理还是 调用的迭代器。只是形式上更简洁而已。

2.2 List 接口

存储有序、可重复的数据。习惯上叫 List 为动态数组。

public interface List extends Collection {

    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator iterator();

    Object[] toArray();

     T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection c);

    boolean addAll(Collection c);

    boolean addAll(int index, Collection c);

    boolean removeAll(Collection c);

    boolean retainAll(Collection c);

    default void replaceAll(UnaryOperator operator) {
        Objects.requireNonNull(operator);
        final ListIterator li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
    void clear();

    boolean equals(Object o);

    int hashCode();

    E get(int index);

    E set(int index, E element);

    void add(int index, E element);

    E remove(int index);

    int indexOf(Object o);

    int lastIndexOf(Object o);

    ListIterator listIterator();

    ListIterator listIterator(int index);

    List subList(int fromIndex, int toIndex);

    @Override
    default Spliterator spliterator() {
        return Spliterators.spliterator(this, Spliterator.ORDERED);
    }
}

List 接口继承了 Collection 接口,Collection中能被继承的抽象方法,非静态方法,默认方法在List接口里面都有。

List 接口有3个实现类。Vector 、ArrayList、LinkList

List的常用方法:以ArrayList为例

  • add 方法: 添加一个元素

ArrayList arr = new ArrayList(10);
arr.add("AA"); // 添加到末尾
arr.add(1, "BB"); // 插入到指定索引
  • addAll 方法:和Collection中的 addAll方法作用一样。

ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add(1, "BB");
List nums = Arrays.asList(1, 2, 3);
arr.addAll(nums); // 添加nums 中每一个元素到末尾
arr.addAll(2, nums); // 将 nums 中每一个元素插入到 索引为2的位置
for (Object o : arr) {
    System.out.println(o);
}
  • indexOf () : 返回元素首次出现的位置,没有就返回-1 ;作用与String类中的一样

ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add(1, "BB");
List nums = Arrays.asList(1, 2, 3);
arr.addAll(nums);
int i = arr.indexOf(2);
System.out.println(i); // 3
int i1 = arr.indexOf(5);
System.out.println(i1); // -1
  • lastIndexOf 返回元素最后一次出现的位置,没有就返回-1;作用与String类中的一样

 ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add(1, "BB");
List nums = Arrays.asList(1, 2, 3);
arr.addAll(nums);
arr.addAll(1, nums);
System.out.println(arr); // [AA, 1, 2, 3, BB, 1, 2, 3]
int i = arr.lastIndexOf(2);
System.out.println(i); // 6
  • remove 移除元素

remove(int index)、remove(Object o) 有2个重载的方法

ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add(1, "BB");
List nums = Arrays.asList(1, 2, 3);
arr.addAll(nums);
arr.addAll(1, nums);
System.out.println(arr); [AA, 1, 2, 3, BB, 1, 2, 3]
// 输入是数字时,默认调用参数为int index这个方法,返回值是删除的 元素
// 传入索引时,不能超过List的最大下标,否则报错
Object remove = arr.remove(2); 
// 是对象是时,调用这个方法,返回值是是否删除成功
boolean remove1 = arr.remove(nums);
System.out.println(remove1); // false
System.out.println(arr); // [AA, 1, 3, BB, 1, 2, 3]

这里面 Object remove = arr.remove(2); 删除的是 索引为2的元素。如果就是要删除 元素为2,要怎么做呢?

我们知道,List里面存储的都是对象,另一个remove重载的方法参数值就是对象。因此传一个包装类对象即可。

boolean b= arr.remove(new Integer(2)); 注意,这个方法返回值是 boolean 类型。

  • set(int index, E element) 方法: 将指定索引元素修改为输入值

ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add("CC");
arr.add(1, "BB");
arr.set(1,"dd");
System.out.println(arr);
  • subList(int fromIndex, int toIndex)方法: 返回一个子list, 也是左闭右开区间。 会返回新的List对象,不会修改原来的List

ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add("CC");
arr.add(1, "BB");
List list = arr.subList(0, 1);
System.out.println(arr); //[AA, BB, CC]
System.out.println(list); //[AA]
  • List 的遍历

ArrayList arr = new ArrayList(16);
arr.add("AA");
arr.add("CC");
arr.add(1, "BB");

1、普通for 循环遍历

for (int i = 0; i < arr.size(); i++) {
   System.out.println(arr.get(i));
}

2、List继承了Collection接口,使用 iterator() 方法 获取迭代器。使用迭代器进行遍历

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

3、使用增强for循环进行遍历

for (Object o : arr) {
   System.out.println(o);
}

2.2.1 Vector 是List的古老实现类

说它是古老实现类是因为它是JDK1.0出现的,而实现的接口Collection是JDK1.2出现的。出现的很早,现在基本不用。

Vector 是线程安全的,但是执行效率低。底层使用 Object[]

源码过长,不适合全部在这里写出。

public Vector() {
    this(10);
}

使用无参构造器会直接默认初始化容量为 10

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 默认扩容为原来的 2倍
    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);
}

其他的使用与ArrayList差不多。

它是线程安全的,但是效率低下。基本不用

即使需要使用线程安全的List,也可以使用Collections类中的 synchronizedList(List list) ,将不安全的ArrayList对象传入,得到线程安全的List对象。

2.2.2 ArrayList 是List的主要实现类

ArrayList 是线程不安全的,效率高。底层使用 Object[] ,源码过长,不适合全部在这里写出。以下为JDK8.0的源码分析

transient Object[] elementData;  // ArrayList 底层的数组

无参构造器:

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;
}

ensureCapacityInternal会确保容量够,如果不够会进行扩容。通常扩容为原来的1.5倍大小。

ArrayList 里面有一个默认容量 private static final int DEFAULT_CAPACITY = 10; 使用无参构造器第一次添加的数据如果小于10,数组的容量会扩容到默认容量10

// 扩容代码
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
    	// 默认扩容为 原来容量+原来容量右移1位 右移1位等于除以2 即1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 将原来的 数组的数据拷贝到 新的 数组里面
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

由于内部添加数据会进行容量确保操作,为了尽量避免扩容,推荐使用 带参数的构造器。预估可能需要的大小。

ArrayList arr = new ArrayList(16);

在JDK7.0中,使用无参构造器创建ArrayList对象时,会直接初始化为一个长度为10的数组。而JDK8使用无参构造创建对象则是一开始是一个空的,第一次添加时直接进行容量确保操作。如果需要的容量小于10,才会变成长度为10的数组。

2.2.3 LinkList 是List的另一种分工的实现类,底层使用双向链表实现。

底层使用 双向链表存储。适合频繁的插入、删除操作,源码过长,不适合全部在这里写出。

transient int size = 0;
// 第一个Node 默认初始化为 null
transient Node first;
// 最后一个Node 默认初始化为 null
transient Node last;

LinkList 有3个典型的属性,size是元素个数。以及 Node类型的first ,Node类型的last

Node 是LinkList的内部类,是linkList的基本存储单位

private static class Node {
    E item;
    Node next;
    Node prev;

    Node(Node prev, E element, Node next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

Node类里面有 3个属性,1个是item 元素,1个Node类型的next,1个Node类型的prev

next是记录当前节点的下一个节点,prev是记录当前节点的上一个节点

Node就是一个类,linkList的基本存储单位就是一个类。因为每个类实例化的位置都是随机的,所以链表是不连续的。需要用next、prev来记录下一个元素,上一个元素是什么。item存储的就是真正的内容。next、prev也说明这是双向链表,可以往下找,也可以往上找。

无参构造器:啥也不干。

public LinkedList() {
}
  • add方法

public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
    final Node l = last;
    final Node newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

总结:

Vector、ArrayList、LinkList的使用在List接口的方法介绍中以ArrayList为例。其他的2个一样的使用。

2.3 Set 接口

存储无序、不可重复的数据 类似于高中数学中的集合。

public interface Set extends Collection {

    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator iterator();

    Object[] toArray();

     T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o)

    boolean containsAll(Collection c);

    boolean addAll(Collection c);

    boolean retainAll(Collection c);

    boolean removeAll(Collection c);

    void clear();

    boolean equals(Object o);

    int hashCode();

    @Override
    default Spliterator spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT);
    }
}

以上为Set接口的源码,并没有额外定义新的方法,基本都是继承于Collection 接口的方法

以HashSet实现类为例。

  • add 方法 添加元素

HashSet set = new HashSet();
set.add(123);
set.add(123);
set.add("abc");
set.add(null);
for (Object o : set) {
    System.out.print(o + "\t");
}
// null	abc	123	

可以看到,可以添加null值。遍历输出时不是按照我们添加的顺序。这就是无序,但是无序不代表随机。

而且,虽然我们调用了2次添加 123,但是输出只有1个。说明Set添加的元素不可重复。

1、什么是无序呢? 看看源码

// HashSet 的无参构造实际上是 得到了一个 HashMap 对象
public HashSet() {
    map = new HashMap<>();
}
// HashSet 的 add 方法
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
// HashMap 的 add 方法
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

可以看到,源码里面HashSet的add方法是调用 map对象的put方法。map是 HashMap 类的一个对象。

所以,HashSet 实际上是和HashMap有关的。这里还需要研究HashMap。先直接给出答案,就是添加时的顺序不是按照索引顺序,而是按照一定的算法计算得到。但是一旦添加完成,顺序也就确定了。变量输出也是确定的。

2、什么是不可重复

// HashMap 的 putVal 方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode)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;
    }

上面讲到添加2次123,输出只有1个。 看源码可以知道,add方法是有返回值的,返回值是boolean类型。表示此次添加是否成功。

成功返回true,失败返回false。

那么,是如果判断不可重复的呢?即添加的数据是否与已有数据相等呢? 答案是 调用Object对象的equals方法。任意类有继承Object对象,都有equals方法。equals 默认是调用 == 比较地址。因此,要实现对象之间的判断相等,必须要重写这个equals方法。Set添加的元素才能确保不重复。

问题来了:如果添加 到1000个数据,10000个数据,每添加一个数据都要进行一一比较,效率就太慢了。

HashSet 内部还是采用 数组进行存储数据的。初始大小为16

HashSet采用的是 计算Hash值【调用hashCode方法】 + equals 混合的方法【最终以equals为准】,每一个hash值都对应数组一个位置【注意不同的hash值可能对应同一个位置】。只需要比较位置上是否有元素。没有元素就可以添加。

如果位置上已经有元素了,就需要调用equals方法比较 已有元素与 需要添加的元素是否相同。当hash不同,而且元素也不同时,说明这个元素需要被添加进去,但是这个位置已经有元素了啊。怎么办? 这个位置链上一个链表来存储,JDK7时,新元素位置在上面,JDK8新元素在老元素的下面。即 HashSet 实际上是 数组加上链表的 混合结构。

在第一步 计算hash值时,如果hash值一样,是否说明这2个元素一样呢?不一定。可能计算hash值的算法出现了重复值。因此还需要调用equals方法进行最终判断。如果equals不同,还是要添加的。如果equals也相同,就说明真的是同一个元素。

总结:

1、HashSet 内部采用 数组+链表混合结构存储数据

2、HashSet 为了加快速度,采用 hash 值的计算比较来辅助比较相等。最终确定是否相等的是equals方法。

3、调用equals的时机1:2个不同的 hash 值指向同一个数组位置时,调用equals判断是否真的相同

4、调用equals的时机2: 2个相同的hash值,还需要调用 equals 判断是否真的相同。

上帝视角学JAVA- 基础14-集合01【2021-08-20】_第2张图片

 

3、hashCode 与 equals 方法的重写

Object中的hashCode方法是一个native方法。即调用的是C语言写的。大致的逻辑是产生一个随机数。

先看看idea 给我们生成的重写的hashCode方法

public class Cat extends Animal{

    public int age = 14;
    public String type = "cat";
	
    // idea 默认模板生成的
    @Override
    public int hashCode() {
        int result = age;
        result = 31 * result + (type != null ? type.hashCode() : 0);
        return result;
    }

重写的原则是 同一个类的对象的属性值相同时,我们希望计算的hash值是相同的,而属性值不同时,计算的hash值是不同的

为什么会出现31这个数字?

加上系数是为了避免一些情况。String类已经重写了hashCode方法,能够实现String相同是,计算的hash值相同。假设A对象的属性 type计算的hash值是 40,B对象计算的hash值是 30,而 A的age = 20,B的age=30,不加系数直接用 下面的式子

 @Override
    public int hashCode() {
        int result = age;
        return result + (type != null ? type.hashCode() : 0);
    }

就会出现 hash值相同的情况。但是A、B对象属性其实是不同的。当乘以系数31时,放大了age之间的误差,就能尽量避免这种情况发生。

那为什么系数是31? 31=32-1=2^5 -1 因为31可以用 i * 31 == (i << 5)- 1 来表示,很多虚拟机对这里有优化。

31 只占用5个bit,相乘造成数据移除的概率较小。(安全性)

31是一个素数,素数的作用是如果我用一个数字A乘以这个素数,那么最终出来的结果只能被素数本身A以及1来整除。(减少冲突)

equals 方法的重写要尽可能的与hashCode值保持一致性。即如果只需要满足某一个属性相同,就判断对象相同,那么hashCode也应该是当这个属性相同时,计算的hash值是一样的。

保持一致性就是保持 判断对象相同的逻辑涉及到的属性,在计算hash值时,当这些属性一样时,计算出的hash值也要相同。

2.3.1 HashSet 主要实现类

线程不安全的,可以存储null值。

应用:去除List中重复的数字(对象)。

List list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(4);
list.add(4);
list.add(5);
// 使用 set 数据结构的特点,不可重复
Set set = new HashSet<>();
// addAll方法将传入的集合的全部元素添加到set中
set.addAll(list);
// 将 set转换为ArrayList
ArrayList newList = new ArrayList<>(set);
for (Integer i : newList) {
    System.out.println(i);
}

这里是去除重复的数字,Integer 类已经重写了equals 和hashCode方法。如果是去除我们自定义的类,这个类需要重写equals和hashCode方法。

练习: 有一个类Cat 已经重写了equals和hashCode方法。分析下面的过程

public class Cat{

    public int age;
    public String type = "cat";

    @Override
    public int hashCode() {
        int result = age;
        result = 31 * result + (type != null ? type.hashCode() : 0);
        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Cat)) {
            return false;
        }

        Cat cat = (Cat) o;

        if (age != cat.age) {
            return false;
        }
        return Objects.equals(type, cat.type);
    }

    public Cat(int age, String type) {
        this.age = age;
        this.type = type;
    }

    public Cat() {
    }

    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                ", type='" + type + '\'' +
                '}';
    }
}
HashSet set = new HashSet();
Cat c1 = new Cat(15, "AA");
Cat c2 = new Cat(16, "BB");
set.add(c1);
set.add(c2);
System.out.println(set); // [Cat{age=16, type='BB'}, Cat{age=15, type='AA'}]

// 修改c1对象的 type属性
c1.type = "CC";
// set 移除 c1 对象:set寻找元素会先计算hash值,再找位置。会先计算传入的c1对象的hash值,由于type属性改变,hash值也被改变。因此位置与原来不同。remove 的时候发现新位置 没有元素,就认为成功删除。即使有元素,再调用equals方法,发现不一样,也认为删除成功。
set.remove(c1);
System.out.println(set); // [Cat{age=16, type='BB'}, Cat{age=15, type='CC'}]

// 添加之前先计算 hash值。传入的对象的hash值对应的位置没有元素,添加成功。注意最开始添加的是按照属性是15,“AA” 计算的。
set.add(new Cat(15, "CC"));
// 添加的时候计算 hash值,发现已经有一个元素,属性是15,“CC”了,然后调用equals方法比较,发现不同,以链表的形式指向老元素。添加成功。
set.add(new Cat(15, "AA"));
System.out.println(set); // [Cat{age=16, type='BB'}, Cat{age=15, type='CC'}, Cat{age=15, type='CC'}, Cat{age=15, type='AA'}]

2.3.1.1 LinkedHashSet 是 HashSet 的 一个子类

public class LinkedHashSet
    extends HashSet
    implements Set, Cloneable, java.io.Serializable {

    private static final long serialVersionUID = -2851667679971038690L;

    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

  
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
    
    public LinkedHashSet() {
        super(16, .75f, true);
    }

    public LinkedHashSet(Collection c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }

    @Override
    public Spliterator spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
    }
}

遍历其内部数据时,可以按照添加的顺序进行遍历。不代表它是有序的。

我们知道无序是指添加元素存储的时候是无序。那么LinkedHashSet 是如何做到遍历时,按照我们添加的顺序呢?就是增加了双向链表

对于频繁的遍历操作,使用 LinkedHashSet 比 HashSet 效率更高。

LinkedHashSet set = new LinkedHashSet();
set.add(123);
set.add(456);
set.add("abc");
set.add(456);
for (Object o : set) {
    System.out.print(o + "\t");
}
// 123	456	abc

调用无参构造器时:调用的是父类 HashSet中的构造器

 public LinkedHashSet() {
        super(16, .75f, true);
    }

实际是:得到一个 LinkedHashMap 对象

    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

调用 add 方法。看上面的源码可知,LinkedHashSet 类只是定义了4个构造方法,以及重写了Spliterator方法。

调用add方法,实际上是父类 HashSet 中的add方法,最终还是使用map进行添加数据

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

2.3.2 TreeSet

public class TreeSet extends AbstractSet
    implements NavigableSet, Cloneable, java.io.Serializable
{
    private transient NavigableMap m;
    private static final Object PRESENT = new Object();

    TreeSet(NavigableMap m) {
        this.m = m;
    }
    public TreeSet() {
        this(new TreeMap());
    }
    public TreeSet(Comparator comparator) {
        this(new TreeMap<>(comparator));
    }
    public TreeSet(Collection c) {
        this();
        addAll(c);
    }
    public TreeSet(SortedSet s) {
        this(s.comparator());
        addAll(s);
    }
    public Iterator iterator() {
        return m.navigableKeySet().iterator();
    }
    public Iterator descendingIterator() {
        return m.descendingKeySet().iterator();
    }
    public NavigableSet descendingSet() {
        return new TreeSet<>(m.descendingMap());
    }
    public int size() {
        return m.size();
    }
    public boolean isEmpty() {
        return m.isEmpty();
    }
    public boolean contains(Object o) {
        return m.containsKey(o);
    }
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }
    public void clear() {
        m.clear();
    }
    public  boolean addAll(Collection c) {
        // Use linear-time version if applicable
        if (m.size()==0 && c.size() > 0 &&
            c instanceof SortedSet &&
            m instanceof TreeMap) {
            SortedSet set = (SortedSet) c;
            TreeMap map = (TreeMap) m;
            Comparator cc = set.comparator();
            Comparator mc = map.comparator();
            if (cc==mc || (cc != null && cc.equals(mc))) {
                map.addAllForTreeSet(set, PRESENT);
                return true;
            }
        }
        return super.addAll(c);
    }
    public NavigableSet subSet(E fromElement, boolean fromInclusive,
                                  E toElement,   boolean toInclusive) {
        return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                                       toElement,   toInclusive));
    }
    public NavigableSet headSet(E toElement, boolean inclusive) {
        return new TreeSet<>(m.headMap(toElement, inclusive));
    }
    public NavigableSet tailSet(E fromElement, boolean inclusive) {
        return new TreeSet<>(m.tailMap(fromElement, inclusive));
    }
    public SortedSet subSet(E fromElement, E toElement) {
        return subSet(fromElement, true, toElement, false);
    }
    public SortedSet headSet(E toElement) {
        return headSet(toElement, false);
    }
    public SortedSet tailSet(E fromElement) {
        return tailSet(fromElement, true);
    }
    public Comparator comparator() {
        return m.comparator();
    }
    public E first() {
        return m.firstKey();
    }
    public E last() {
        return m.lastKey();
    }
    public E lower(E e) {
        return m.lowerKey(e);
    }
    public E floor(E e) {
        return m.floorKey(e);
    }
    public E ceiling(E e) {
        return m.ceilingKey(e);
    }
    public E higher(E e) {
        return m.higherKey(e);
    }
    public E pollFirst() {
        Map.Entry e = m.pollFirstEntry();
        return (e == null) ? null : e.getKey();
    }
    public E pollLast() {
        Map.Entry e = m.pollLastEntry();
        return (e == null) ? null : e.getKey();
    }
    @SuppressWarnings("unchecked")
    public Object clone() {
        TreeSet clone;
        try {
            clone = (TreeSet) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError(e);
        }
        clone.m = new TreeMap<>(m);
        return clone;
    }

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        s.defaultWriteObject();
        s.writeObject(m.comparator());
        s.writeInt(m.size());
        for (E e : m.keySet())
            s.writeObject(e);
    }
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        @SuppressWarnings("unchecked")
            Comparator c = (Comparator) s.readObject();

        TreeMap tm = new TreeMap<>(c);
        m = tm;
        int size = s.readInt();

        tm.readTreeSet(size, s, PRESENT);
    }
    public Spliterator spliterator() {
        return TreeMap.keySpliteratorFor(m);
    }
    private static final long serialVersionUID = -2479143000061671589L;
}

TreeSet 使用二叉树存储的,具体一点是红黑二叉树存储的。可以按照添加的对象的指定属性进行排序。

TreeSet 只能存储同类型的对象。这是为了保证 可以按照添加的对象的指定属性进行排序。要是不同类的对象,就无法保证都有某个属性了。

TreeSet set = new TreeSet();
set.add("abc");
set.add("3333");
set.add("abc");
for (Object o : set) {
    System.out.print(o + "\t");
}
// 3333	abc

输出是按照从小到大。

当我们传入自己定义的类时,要求必须 实现 Comparable接口。重写compareTo 方法

public class Cat implements Comparable{

    public int age;
    public String type = "cat";

    @Override
    public int hashCode() {
        int result = age;
        result = 31 * result + (type != null ? type.hashCode() : 0);
        return result;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Cat)) {
            return false;
        }

        Cat cat = (Cat) o;

        if (age != cat.age) {
            return false;
        }
        return Objects.equals(type, cat.type);
    }


    @Override
    public int compareTo(Object o) {
        if ( !(o instanceof Cat)){
            throw new RuntimeException("必须传入相同类型的对象");
        }
        Cat cat = (Cat) o;
        if (this == cat){
            return 0;
        }
        return Integer.compare(this.age, cat.age);
    }

    public Cat(int age, String type) {
        this.age = age;
        this.type = type;
    }

    public Cat() {
    }

    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                ", type='" + type + '\'' +
                '}';
    }
}

注意,TreeSet 判断相等不再使用equals方法,而是使用Compare 接口中的 compareTo方法。

TreeSet set = new TreeSet<>();
set.add(new Cat(15, "ddd"));
set.add(new Cat(10, "ddd"));
set.add(new Cat(12, "ddd"));
set.add(new Cat(50, "ddd"));
for (Cat o : set) {
    System.out.println(o);
}
// 输出
Cat{age=10, type='ddd'}
Cat{age=12, type='ddd'}
Cat{age=15, type='ddd'}
Cat{age=50, type='ddd'}

如果不想实现 Comparable 接口,也想用 TreeSet存储,可以使用 Comparator 的匿名实现类对象,初始化TreeSet

Comparator com = new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        if ( !(o1 instanceof Cat)){
            throw new RuntimeException("必须传入Cat类型的对象");
        }
        if ( !(o2 instanceof Cat)){
            throw new RuntimeException("必须传入Cat类型的对象");
        }
        Cat cat1 = (Cat) o1;
        Cat cat2 = (Cat) o2;
        if (cat1 == cat2){
            return 0;
        }
        return Integer.compare(cat1.age, cat2.age);
    }
};
TreeSet set = new TreeSet<>(com);

set.add(new Cat(15, "ddd"));
set.add(new Cat(10, "ddd"));
set.add(new Cat(12, "ddd"));
set.add(new Cat(50, "ddd"));

其实,就是在 实例化 TreeSet 对象时,告诉它 比较的方法。这样添加的对象就不要求一定实现 Comparable 接口了。

当使用这个方式实例化 TreeSet 对象,即使 添加的对象实现了 Comparable 接口,比较的方法还是 Comparator匿名实现类中定义的方法。同理,比较相等,也是这个 compare方法。而不是 equals方法了。

你可能感兴趣的:(JAVA基础,java,集合,arraylist,hash)