12、集合框架


前言

      本篇文章内容是在原有笔记之上进行补充总结,相当于回炉重造,第一次学习Java是看的尚硅谷教程:尚硅谷_Java零基础教程-java入门必备-适合初学者的全套完整版教程(宋红康主讲),其中比较好的部分内容如部分图片会借鉴尚硅谷笔记(如有侵权,我会删除)。

      我的文章索引目录:博客目录索引(持续更新)



一、认识集合框架

引言

前言:为了方便对对象操作,我们会将多个对象集合到一起,再没学习使用集合之前我们使用的是数组来存储对象,而使用数组存储对象的话有着许多的弊端,例如我们想直接添加一个对象或是删除、修改一个对象使用数组只能通过下标来对数组进行操作,比较有局限性。数组只给我们提供了length()、clone()的方法。

数组存储方面的特点

  1. 数组初始化以后,长度就确定了。
  2. 数组声明的类型,就决定了进行元素初始化的类型。

弊端

  1. 初始化数组之后,长度确定就不便于扩展。
  2. 数组中提供的方法少不便于增删改查,效率不高,且无法直接获得数组中真实的元素个数。
  3. 数组存储的数据是有序的、可以重复的。

介绍集合

Java中给我们提供了现成的各种存储结构的集合,其中可以存储数量不等的多个对象,并且可以保存具有映射关系的数组。

场景:在服务器端中我们可以将由对象构成的List集合转换为JSON对象或JSON数组来进行传递关键的信息内容。

Java的集合中分为CollectionMap两个体系,这两个都是最顶层的接口。

  • Collection接口:单列数据,定义了存取一组对象的方法集合。
    • List:元素有序、可重复的集合。
    • Set:元素无序、不可重复的集合。
  • Map接口:双列数据,保存具有映射关系“key-value对”的集合。

Collection接口继承树

image-20210214222231759

Map接口继承树

image-20210214222323301

神图:网上搜集

image-20210214222638856



二、 Iterator迭代器接口

介绍Iteractor接口

Iteractor对象:称为迭代器(设计模式中的一种),主要用于遍历Collection集合元素。仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。

  • GOF(设计模式作者)这样定义迭代器:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节,为容器而生。
  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
public interface Iterable<T> {
	...
}    


三个主要方法

image-20210214225518064

  • hasNext():判断是否还有下一个元素。
  • next():返回从第1个位置开始的元素。
  • remove():需要跟next()搭配,直接使用remove会报错。

遍历方法:三种

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("123");
    list.add("456");
    Iterator<String> iterator = list.iterator();
    //方式一:直接使用next()单个遍历
    //System.out.println(iterator.next());
    //System.out.println(iterator.next());
    //方式二:通过hasNext()与next()配合使用(确定Iterator中的个数并输出)
    //while(iterator.hasNext()){
    //    System.out.println(iterator.next());
    //}
    //方式三:通过原本集合的size()获取到指定个数,接着输出即可
    //        for(int i=0;i<list.size();i++){
    //            System.out.println(iterator.next());
    //        }
    //方式四:通过foreach遍历
    for (String s : list) {
        System.out.println(s);
    }
}
  • 这里使用到了hasNext()next()方法

删除元素

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("123");
    list.add("456");
    Iterator<String> iterator = list.iterator();
    //删除所有元素
    while(iterator.hasNext()){
        iterator.next();
        //需要在next()之后调用
        iterator.remove();
    }
    System.out.println(list);//[]
}


源码分析(ArrayList中的迭代器)

直接从上面三个方法例子中看源码:

首先我们是通过ArraryList实例中的Iterator()获得迭代器的:

public Iterator<E> iterator() {
    //实际上返回的是自己的内部类Itr
    return new Itr();
}
  • Iterator()是经过重写的,其ArraryList继承了AbstractList类,又该类继承了List类,List类继承了Collection类,Collection类实现了Iterable接口,就有Iteractor()方法了。

Itr这个内部类继承了Iterator接口,所以上面是多态返回。

接着我们开始代码简略分析一下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    
    //数据都存储在这个数组之中
    transient Object[] elementData;
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    //初始构造
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    //返回迭代器
    public Iterator<E> iterator() {
        return new Itr();
    }
    
    //内部类继承了Iterator接口
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        
        Itr() {}

        //调用这个方法实际上就是判断当前下标是否已经到达最后一个位置了,size指当前存放对象数组中的数量
        public boolean hasNext() {
            return cursor != size;
        }

        //主要就是返回在elementData数组的lastRet位置的数据
        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;//首次为0
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;//接着加+1,表示下一次要读取的位置
            return (E) elementData[lastRet = i];//这里实际上就是之前cursor的位置数
        }

        //移除lastRet位置的对象
        public void remove() {
            //因为初始lastRet或者执行一次remove()之后为-1,所以之前说单独调用remove()就会报错
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                //实际上就是调用了ArrayList本身的remove()方法
                ArrayList.this.remove(lastRet);
                cursor = lastRet;//倒退1
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
       ...
    }
}
    

  • 简单总结一下:hasNext()实际上就是判断一下当前位置是否不等于总元素个数(有点像之前遍历方法三中的方法)、next()实际上就是返回ArrayList中实际保存数据数组里指定位置的元素,remove()实际也就是调用ArrayList本身的Remove()方法,前提就是需要先next()才能执行该方法。


三、顶级接口及实现类

认识各个接口

Collection接口:顶层接口,是List、Set 和 Queue 接口的父接口。JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List) 实现。(单列集合,用来存储一个个对象)

需要记住:在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容 器中对象的数据类型。

image-20210214223354845

List接口:存储有序,可重复的数据,顶层接口,继承了Collection接口,同时也是ArrayList、LinkedList等集合元素的父类。

Set接口:无序,无重复,与List接口同级,也继承了Collection接口,对于add、equals、hashCode方法提供额外标准。

Queue接口:是List、Set接口并列的Collection的三大接口之一。用来保持元素访问次序,除了Collection接口提供方法,还有额外的插入、读取、检查操作。

SortSet接口:直接继承于Set接口,能够使用Comparable对元素进行自然排序或使用Comparator在创建时元素提供定制排序规则,Set迭代器按升序元素遍历集合。

Map接口:支持key-value存储的对象,Map不包含重复的key,每个键最多映射一个值。该接口代替Dictionary类,Dictionary是一个抽象类而不是接口。



List接口及常用实现类

List接口

List接口:存储有序、可重复的数据,"动态"的数组替换原有的数组。

  • ArrayList:作为List接口的有效实现类,线程不安全,效率高,底层使用Object[] elementData存储。
  • LinkedList:底层使用双向链表存储,对于频繁插入、删除操作效率比ArraryList高。
  • Vector:List接口古老实现类,线程安全,效率低,底层使用Object[] elementData存储。

除Collection集合继承的方法外,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 位置的子集合

Arrays的asList()方法

//Arrays工具类中的方法
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

通过该方法得到的List集合既不是 ArrayList 实例,也不是 Vector 实例,见下:

//该类是Arrays工具类的内部类,同样也是继承了AbstractList,所以也算是List的子类
private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }
    ...
}
  • 该类简单实现了一个List集合,其中并没有重新实现add()方法、remove()方法(假设调用会跑异常,因为调用的是AbstractList的重写方法,就只是抛出异常,可见源码)。
  • 它的目的就是简单的将一个数组转为一个list集合并对现有集合中的内容进行简单查询操作。

相关面试

面试题:ArraryList、LinkedList、Vector三者异同?

  • 相同点:三者都实现了List接口,都是存储有序、可重复的数据。
  • 不同点:如上。


ArrayList

image-20210217092415755

重点:ArrayList不是线程安全的容器,有线程安全的问题,我们可以使用Collections.synchronizedList(...)来获得线程安全的List集合。

  • 例如:List list = Collections.synchronizedList(new ArrayList(...))。获取一个单独的线程安全的类,该类创建在Collections类中,实际上就是给每个方法包了一个同步代码块。
  • ArrayList具有fail-fast机制,对ArrayList做出失败检测,即可抛出ConcurrentModificationException异常。
    • fail-fast:多个线程对同一个集合的内容进行操作,会产生fail-fast事件,例如:当线程A通过iteractor去遍历某个集合的过程中,该集合内容被其他线程改变,那么线程A访问集合时,就会出现异常,从而产生fail-fast事件。

ArrayList不同JDK版本描述

JDK7.0:

  • ArrayList list = new ArrayList():底层创建了长度为10的Object[]数组elementData。
  • list.add(666);:相当于elementData[0]=new Integer(666); 若是add()方法操作导致容量不够会进行自动扩容,默认情况下,扩容为原来的1.5倍,同时将原有数组内容复制到新数组中。

JDK8.0:

  • ArrayList list = new ArrayList():底层只是初始化Object[] elementData={}。
  • list.add(666);:第一次调用add()方法时,才创建长度为10 的数组,并将666添加到数组中,后序若是容量不够也是有扩容操作为1.5倍。

注意

①开发中最好使用带参的构造器ArrayList(int initialCapacity),指定容量大小。

②jdk7中ArraryList里对象创建类似于单例的饿汉式,直接创建好指定10容量的数组;而在jdk8中如单例懒汉式,只有add()方法调用时才创建,延迟了数组创建,节省内存。


源码分析

JDK7源码:

image-20210215234423441

  • 无参构造器:直接创建容量为10的数组。
  • add()操作:简单添加操作,一旦容量不够,重新开辟并复制到一个1.5倍容量数组,接着进行添加操作。
  • 有参构造器:直接创建容量为initialCapacity的数组。

JDK8源码:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
        
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    transient Object[] elementData;
    
    //无参构造器:并没有开辟指定容量大小,直接elementData = {};
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    //首次add()方法会开辟10个容量大小的数组,接着进行添加操作;超过容量大小重新扩容创建数组
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    //这里给出一个关键的方法(在add()中调用),其中就可以看到扩容为1.5倍
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//>>1是位运算相当于除以2,所以扩容为1+0.5=1.5倍
        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);
    }
}


LinkedList

image-20210217092507844

对于频繁的插入与删除元素使用LinkedList集合,效率高。

LinkedList:是一个双向链表,允许存储任何元素(包含null)。

  • 内部并没有声明数组,而是通过定义内部类Node来构成链表
  • 该集合所有的操作都可以表示为双向性的,索引链表的操作可以从前到后,或者往前遍历。
  • 非线程安全的,当多个线程并发访问该集合时,一旦有线程更改链表的结构,就需要在外部上锁,我们可以通过使用Collections.synchronizedList方法获取。

源码:内部链表类Node

private static class Node<E> {
    E item;
    Node<E> next;//记录当前Node节点的前一个元素
    Node<E> prev;//记录当前Node节点的后一个元素

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

接着看下LinkedList集合中的构造器以及add()添加方法:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
    
    //无参构造器:没有做任何事
    public LinkedList() {
    }
    
    //add添加方法:直接调用linkLast()方法
    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;//若是第一次创建链表,该Node节点为第一个节点
        else
            l.next = newNode;//不是第一个节点,那么将刚创建的节点放置到最后一个Node节点的Next()接收
        size++;
        modCount++;
    }
    ...
}
  • 通过链表方式来存储数据。

对于链表其频繁插入以及删除元素效率是很高的,但是想要查看指定元素效率就没有ArrayList中使用数组来得快。



Vector

image-20210217092543976

Vector:JDK1.0的时候就出现了,底层使用的是Object[] elementData数组来存储数据,是线程安全的。

  • 无参构造器会直接创建长度为10的数组。
  • 使用add()方法扩容时,会扩容原来数组长度的2倍。
  • 内部的方法都是简单粗暴的上锁,使用时开销较大。

源码分析

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    
    protected Object[] elementData;
    
    //无参构造器默认创建大小为10的数组
    public Vector() {
        this(10);
    }
    
    //add()方法:扩容为2倍,可以看到其中的方法都是上锁了的
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
    
    //grow是add()中的一个关键方法,用于创建一个新的数组,一旦添加时超过现有容量时,会扩容为2倍
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);//这里1+1=2
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }


Stack

image-20210217092613793

Stack:栈类,我们的程序方法调用执行也是使用的栈。该类继承了Vector类,提供了push()、pop()(入栈、出栈)的方法,peek()获取栈顶数据、empty()判断栈是否为空,search(Object o)查询指定数据与栈顶的距离。

      单独一个Stack类其中的方法比较少,一个更完善、可靠性更强的LIFO(后进先出)栈操作由Deque接口及其实现类,应该优先使用ArrayDeque类,如:Deque<Integer> deque = new ArrayDeque<>();

  • Deque是双端队列,可以作为队列,也可以使用于栈操作。

测试一下

public class Main {
    public static void main(String[] args) {
        Deque<Integer> deque = new ArrayDeque<>();
        deque.push(123);
        deque.push(456);
        System.out.println(deque.pop());//456
        System.out.println(deque.pop());//123
    }
}


总结及相关面试题

总结

一般情况下我们都是使用ArrayList来操作数据;对于频繁插入、删除操作的话使用LinkedListVector是线程安全的,相关操作与ArrayList无异,效率低,尽量避免使用。若是对于多线程的话我们一般使用Collections.synchronizedList(List<T> list)获取线程的集合,实际上就是包裹了一层synchronized。


面试题

:请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?VectorArrayList的最大区别?

①ArrayList和LinkedList的异同

  • 二者都线程不安全,相对线程安全的Vector,执行效率高。
  • 此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于 随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增 和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。

②ArrayList和Vector的区别

  • VectorArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类,线程安全的。因此开销就比ArrayList要大,访问要慢。
  • 正常情况下,大多数的Java程序员使用 ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack


Set接口及常用实现类

Set接口

Set接口:是Collection的子接口,set接口没有提供额外的方法,都是Collection中的抽象方法。

  • Set集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
  • Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法。

接口及实现类描述

Set接口:存储无序的、不可重复的数据。

  • HashSet:作为Set接口的主要实现类,线程不安全,可存储null值。
    • LinkedHashSet:为HashSet子类,可按照添加顺序遍历。
  • TreeSet:可以按照添加对象的指定属性进行排序。

都是线程不安全的,除排序类可通过Collections.synchronizedSet()获取到Set集合进行外部加锁。

排序类如TreeSet可通过Collections.synchronizedSortedSet(new TreeSet(...))来获取SortedSet集合进行外部加锁。



HashSet

image-20210217092321459

初步认识HashSet

HashSet:是Set接口的典型实现,大多使用Set集合都使用该实现类,按Hash算法来存储集合中的元素,对存取、查找、删除有着很好的性能。

特点:无序、不是线程安全的,集合元素可为null。

  • 无序性:不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序依次添加,而是根据数据的哈希值来作为索引位置。
  • 不重复性:保证添加的元素按照equals()判断时,不返回true,及相同的元素只能添加一个。

HashSet集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的 equals() 方法返回值也相等。

  • 对于存放在Set容器中的对象,该类需要重写equals()hashCode(Object obj)方法,以实现对象相等规则,即相等的对象必须具有散列码。

实际遍历一下

public class Main {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("changlu");
        set.add("love");
        set.add("love");
        set.add("liner");
        //遍历Set集合,使用Iterator
        Iterator<String> iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
        //love
		//changlu
		//liner
    }
}
  • 通过迭代器进行遍历

HashSet添加过程介绍

通过调用add()方法,过程如下:

  • 向HashSet中添加元素,首先会调用元素a所在类的hashCode()方法,计算元素a的哈希值(通过某种算法计算出在HashSet底层数组中存放的位置,即索引位置),判断数组在此位置上是否有元素。
  • 若该位置上没有其他元素,直接将元素a添加进来。
  • 若是该位置上有其他元素b(是以链表形式存在的多个元素),则会比较元素a和元素b的hash值
    • 若是hash值不相同,则元素a添加成功!
    • 若hash值相同,接着会调用元素a所在类的equals()方法
      • 若equals()返回true,则元素a添加失败!
      • 若equals()返回false,则元素a添加成功!

大致过程如上,对于元素a实际上是在已经存在指定索引位置上以数组+链表形式存储的。

image-20210216231652719

  • 底层也是数组,初始容量为16,当如果使用率超过0.75(即若是使用了16*0.75=12)时就会扩大容量为原来2倍。(之后依次推即可,16扩容为32,32扩容为64)

JDK不同版本说明:总结为7上8下

  • JDK7:将元素a放到数组中,指向原来的元素
  • JDK8:将元素a放在链表最后位置。

看下源码

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{
    //内部实际上使用的是HashMap
    private transient HashMap<E,Object> map;
    
    //添加元素
    public boolean add(E e) {
        //直接使用了HashMap中的put()操作。
        return map.put(e, PRESENT)==null;
    }
}
  • 这里暂时不展示HashMap相关源码,在HashMap中是使用一个Node<K,V>[] table;数组,其中存放了一个个Node节点(即链表),配合存储,其中还相关hash算法。


LinkedHashSet

image-20210217092705755

LinkedHashSet:是HashSet的子类,无相关自己实现的类。其中实际上使用的是LinkedHashMap(同样是HashMap子类),能够顺序访问插入进来的数。

  • 底层使用的是数组+双向链表形式,其中访问数据都是有序的。如下图清晰看出为什么能够顺序访问存储的数据:

    image-20210216235022817

要求:向Set中添加的数据,其所在类一定要重写hashCode()equals()方法。极可能保持两个方法一致性。

说明LinkedHashSet插入性能略低于HashSet,但在迭代访问Set里的全部元素有很好的性能。



TreeSet

image-20210217092813746

TreeSet:是 SortedSet 接口的实现类,可以确保元素处于排序状态,底层使用红黑树结构存储数据。

  • 两种排序方式:自然排序与定制排序。默认情况下采用自然排序(map根据key自然排序)。
  • 特点:有序,查询速度比List快。

两种排序实现

默认是使用的自然排序(很多类中本身就实现了Comparable,add()添加中调用public int compareTo(T o);即可进行比对);另一种则是定制排序,需要实现Comparable接口传入到TreeSet构造器中,见如下:

自然排序

import java.util.*;

public class Main {
    public static void main(String[] args) {
        TreeSet<Person> set = new TreeSet<>();
        set.add(new Person("changlu",12));
        set.add(new Person("liner",10));
        set.add(new Person("changlu",8));
        //遍历
        Iterator<Person> iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

class Person implements Comparable{
    private String name;
    private Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (name != null ? !name.equals(person.name) : person.name != null) return false;
        return age != null ? age.equals(person.age) : person.age == null;
    }

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


    @Override
    public int compareTo(Object o) {
        if(o instanceof Person){
            Person p = (Person) o;
            //先按照String排序
            int result = this.name.compareTo(p.name);
            if(result == 0){//0表示相等
                return this.age.compareTo(p.age);
            }
            return result;
        }else{
            throw new RuntimeException("类型不匹配");
        }
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • 自然排序就是会根据添加类中通过实现Comparable接口实现的compareTo方法来进行比对添加。对于一些JDK中提供的类已经实现了Compareable接口,而自定义的则需要手动实现该接口并实现其方法。
  • 重写equals()hashCode()法是为了让Set集合中不出现重复的元素,compareTo方法是去实现排序的效果。

image-20210217101059652

自定义排序

import java.util.*;

public class Main {
    public static void main(String[] args) {
        //实现一个匿名接口类,实现其中的方法,让字符串倒序排列
        Comparator<String> comparator = new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return -o1.compareTo(o2);
            }
        };
        TreeSet<String> set = new TreeSet<String>(comparator);
        set.add("changlu");
        set.add("liner");
        //遍历
        Iterator<String> iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
    
}
  • 通过实现Comparator的匿名接口类将其放入到TreeSet的构造器中,之后执行add()方法则里排序会根据Comparator中的compareTo方法进行比对。

image-20210217102053079


源码分析

下面只是我自己简单的分析:

//TreeSet源码
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable{
    
    private transient NavigableMap<E,Object> m;
    
    public TreeSet() {
        //实际上创建了TreeMap的实例
        this(new TreeMap<E,Object>());
    }
    
    //实际上调用了TreeMap的有参构造传入了comparator(即定制排序)
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    
    //执行添加操作实际上就是调TreeMap的put()方法
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    ...
}

//TreeMap源码
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
    
    //内部comparator实例
    private final Comparator<? super K> comparator;
    
    //该构造器将外部自定义comparator引用传给内部属性
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    
    //put()方法:其中就会根据情况使用自然排序或者定制排序
    public V put(K key, V value) {
        ...
        Comparator<? super K> cpr = comparator;//接收本类中的comparator(初始为null)
        if (cpr != null) {//这里就是判断是使用自然排序还是定制排序
            //不为null表示,说明我们之前传入了Comparator实例,使用定制排序
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);//执行实现了compare()方法
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            //使用自然排序,一般就是直接调集合元素中的compareTo()方法
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);//这里进行了调用
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }    
        ...
    }
}
  • 粗略的看了下源码,这里只针对使用自然排序还是定制排序进行分析


Set相关面试题

(1)、在list中取出重复数字值,要求尽量简单

import java.util.*;

public class Main {

    public static List<Integer> duplicateList(List<Integer> list){
        HashSet<Integer> set = new HashSet<>(list);//或者使用set.addAll(list)添加
        return new ArrayList<>(set);
    }

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(12);
        list.add(15);
        list.add(12);
        list.add(26);
        list.add(15);
        list = duplicateList(list);
        //遍历
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }

}

image-20210217113921689

(2)、hashset增加或删除值的判断

class Person{
    public String name;
    public int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (age != person.age) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }

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

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

public class exerTest {
    @Test
    public void test(){
        HashSet hashSet = new HashSet();
        Person p = new Person("Tom",100);
        Person p1 = new Person("Tom1",101);

        hashSet.add(p);
        hashSet.add(p1);
        System.out.println(hashSet);//[Person{name='Tom', age=100}, Person{name='Tom1', age=101}]
        p.age = 102;
        hashSet.remove(p);
        System.out.println(hashSet);//[Person{name='Tom', age=102}, Person{name='Tom1', age=101}]

        hashSet.add(new Person("Tom",102));
        System.out.println(hashSet);
        //[Person{name='Tom', age=102}, Person{name='Tom', age=102}, Person{name='Tom1', age=101}]

        hashSet.add(new Person("Tom",100));
        System.out.println(hashSet);
        //[Person{name='Tom', age=102}, Person{name='Tom', age=102}, Person{name='Tom1', age=101}, Person{name='Tom', age=100}]

    }
}
  • 50行修改原先p的age值,接着执行remove()方法删除无效,过程:首先比对hashcode值,找到了确定位置,接着使用equals()方法比较值,没有相同的,所以删除失败。
  • 第54行添加了一个对象,这里添加成功,过程:首先会推出hashcode值,没有与之相同的则会直接添加。注意这里添加的虽然和之前修改的一个对象属性相同,但是hashcode值不同,所以添加成功。
  • 第58行我们又添加了一个对象,这个对象中的属性与原来没有修改的p相同,所以hashcode值与p是同一个位置,接着使用equals()方法比对,发现没有值与其相同,最后添加成功。


hashCode()方法重写介绍

class Person{
    String name;
    Integer age;

    //这是通过IDEA生成的hashCode()方法
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (age != null ? age.hashCode() : 0);
        return result;
    }
}
  • result是本地方法得到的一个hash值,选择一个系数值31与result相乘。
    • 选择系数的时候要选择尽量大的稀疏,如果计算出来的hash地址越大,所谓的冲突也就越少,查找起来效率也会提高(减少冲突)。
    • 这里的31也可以使用i*31=(i<<5)-1,乘以2*5-1=31,很多虚拟机中都有相关优化。(提高算法效率)
    • 31是一个素数,数组作用就是如果我用一个数字来乘以这个素数,最终出来的结果只能被本身和被乘数还有1来整除!(减少冲突)


Map接口及常用实现类

Map接口

Map接口:就是一个单独的接口interface Map<K,V> ,用于保存具有映射关系的数据:key-value,key与value都可以是任何引用类型的数据。

  • key使用Set来存放,不允许重复,即同一个Map对象所对应的类必须重写equals()hashCode()方法。一般使用String类作为key。
  • 通过指定的key总能找到唯一、确认的value。

image-20210217121614614

  • key使用的是Set(无序、不可重复),Values使用的Collection存储(无序、可重复),每个键值对则为Entry(无序、不可重复)。

常用实现类

  • HashMap:作为Map的主要实现类:线程不安全,效率高;可以存储null的key与value。
    • LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
      • 原因:在原本的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,执行效率比HashMap高。
  • TreeMap:按照添加的key-value进行排序,实现排序遍历。会考虑key的自然排序或定制排序,底层使用了红黑树。
  • HashTable:线程安全,效率低,不能存储为null的key和value(若是value为空直接手动抛出异常,而key报异常是因为调用了key的hashcode()方法若为null则是空指针)。
    • Properties:常用来处理配置文件,key和value都是String类型。

针对于HashMapLinkedHashMap想要构造一个线程安全的则可用Collections.synchronizedMap(new HashMap(...))

针对于TreeMap想要构造线程安全的可用Collections.synchronizedSortedMap(new TreeMap(...))

常用方法

image-20210217121358737



HashMap

image-20210217121207911

HashMap:是利用哈希表原理来存储元素的集合,有两个参数影响性能:初始容量和加载因子。

不同版本变化

JDK7:底层是数组+链表(链地址法)。

JDK8:数组+链表(或红黑树)

image-20210217123402175

image-20210217123422214

不同版本底层实现原理

JDK7的实现原理如下:

image-20210217123956466

  • 简述:初始创建长度为16的数组,执行put()后,首先获取哈希值来得到存放位置,判断当前位置是否为空,为空插入成功;不为空来去比对其中的哈希值是否相同,若都不相同则添加成功;若相同,再比较equals()方法若是返回false添加成功,返回true为失败。
  • 扩容情况:扩容2倍。

JDK8的实现原理如下:

image-20210217124257386

  • 同样也是扩容2倍。

转换不同数据结构:当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后, 下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

若是我们已知HashMap中的元素个数,建议预设元素个数能够有效提升HashMap的性能。


重要常量

  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16。
  • MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30 。
  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子 。
  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树 。
  • UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表 。
  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的 数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行 resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4 倍。)
  • table:存储元素的数组,总是2的n次幂 。
  • entrySet:存储具体元素的集 。
  • size:HashMap中存储的键值对的数量 。
  • modCount:HashMap扩容和结构改变的次数。
  • threshold:扩容的临界值,=容量*填充因子 loadFactor:填充因子。


LinkedHashMap

image-20210217131239664

LinkedHashMap:是HashMap的子类,在HashMap存储结构基础上,使用了双向链表来记录添加元素的顺序。与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序(与插入顺序一致)。

看一下HashMap中的内部类及LinkedHashMap中的内部类:

//HashMap中的内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

//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);
    }
}
  • 与HashMap不同之处就是维护了这个Entry双向链表,通过该Entry来保存前后顺序


TreeMap

image-20210217133654291

TreeMap:底层是红黑树,存储 Key-Value 对时,需要根据 key-value 对进行排序。保证所有的 Key-Value 对处于有序状态。对于TreeMap可以使用自然排序与定制排序(可看前面TreeSet),注意是对key进行排序。

  • TreeMap的containsKey、get、put、remove方法提供了log(n)的时间开销。


HashTable

image-20210217134818115

HashTable:线程安全(方法都上了锁),实现了一个哈希表,能够将键映射到值,key与value都不能为空。

源码分析:看一下为什么key与value不为为空

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {//一旦value为空则抛出异常
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();//若为key为空,这里调用hashCode()则会出现访问空指针异常
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}


Properties

image-20210217135325571

Properties:是HashTable的子类,该对象用于处理属性文件,其中的key、value值都是String类型的。

常用于读取properties配置文件,读取方法一般使用:setProperty(String key,String value)方法和 getProperty(String key)

实际上这个类给我们封装了文件读取流的操作,可从配置文件中读取key、value键值对存储到Map集合中,接着我们使用方法来获取键值对,一般使用于读取数据库配置文件:

实例演示

public static void main(String[] args) {
    Properties properties = new Properties();
    try {
        properties.load(new FileInputStream("jdbc.properties"));
        String username = properties.getProperty("username");
        String password = properties.getProperty("password");
        System.out.println(username+":"+password);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

image-20210217140442773

  • 一行写一对key、value键值对,查看源码是每次读取一行

image-20210217140451054



Map遍历方法

public static void main(String[] args) {
    Map<String,Integer> map = new HashMap<>();
    map.put("changlu",18);
    map.put("liner",21);
    //遍历key
    Set<String> set = map.keySet();
    for (String s : set) {
        System.out.println(s);
    }

    //遍历value
    Collection<Integer> values = map.values();
    for (Integer value : values) {
        System.out.println(value);
    }

    //遍历Entry
    Set<Map.Entry<String, Integer>> entries = map.entrySet();
    for (Map.Entry<String, Integer> entry : entries) {
        System.out.println(entry.getKey()+":"+entry.getValue());
    }
}
  • keySet():获取由所以的key组成的set集合
  • values():获取由所有的value组成的collection集合。
  • entrySet():获取包含key、value的set集合。


相关面试题

例1:负载因子值的大小,对HashMap有什么影响

  • 负载因子的大小决定了HashMap的数据密度。
  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长, 造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的 几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性 能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建 议初始化预设大一点的空间。
  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。


四、Collections工具类

image-20210217153749755

  • 单独一个分支,是一个包装类。

Collections:是一个操作Set、List、Map等集合的工具类,提供了一系列静态方法来对集合元素进行排序、查询、修改等操作,提供了对集合对象设置不可变,对集合对象实现同步控制等方法(上锁)。

  • Arrays:是一个操作数组的工具类

相关方法

排序方法

  • 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 对象的所有旧值。

同步控制方法:将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。

  • public static Collection synchronizedCollection(Collection c);
  • public static Set synchronizedSet(Set s);
  • public static List synchronizedList(List list);
  • public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
  • public static SortedSet synchronizedSortedSet(SortedSet s);:例如TreeSet
  • public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);:例如TreeMap


参考资料

[1]. fail-fast

[2]. 真正搞懂hashCode和hash算法(面试官都馋哭了)

[3]. 面试常问:什么是红黑树?

[4]. 尚硅谷_Java零基础教程-java入门必备-适合初学者的全套完整版教程(宋红康主讲)


我是长路,感谢你的阅读,如有问题请指出,我会听取建议并进行修正。
欢迎关注我的公众号:长路Java,其中会包含软件安装等其他一些资料,包含一些视频教程以及学习路径分享。
学习讨论qq群:891507813 我们可以一起探讨学习
注明:转载可,需要附带上文章链接

posted @ 2021-02-20 20:04  长路  阅读(101)  评论(0编辑  收藏  举报