Java集合框架

集合

在这里插入图片描述

Iterable接口

  • Iterable对象称为迭代器,主要用于遍历Collection集合中的元素。
  • 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterable接口的对象,即可以返回一个迭代器。
  • Iterator仅用于遍历集合,Iterator本身不存放对象。

实现此接口允许对象成为for-each循环的目标,也就是增强for循环,它是Java中的一种语法糖

增强for循环---->简化版本的迭代器遍历

List<Object> list = new ArrayList();//ArrayList是该接口的实现类
for(Object obj : list){
    System.out.print(obj);
}

除了实现此接口的对象外,数组也可以用for-each循环遍历,如下:

Object[] list = new Object[10];
for(Object obj : list){}

迭代器遍历方式

jdk1.8之前Iterable就只有iterator一个方法,就是:

Iterator<T> iterator();

实现次接口的方法能够创建一个轻量级的迭代器,用于安全的遍历元素,移除元素,添加元素。这里面涉及到一个fail-fast机制。

总之一点就是能创建迭代器进行元素的添加和删除remove()方法的话,就尽量使用迭代器进行添加和删除。

也可以使用迭代器的方式进行遍历。

for(Iterator it = coll.iterator();it.hasNext();){
    System.out.println(it.next());
}
/*Iterator it = coll.iterator();这是得到一个集合的迭代器
*it.hasNext();判断是否还有下一个元素
while(it.hasNext()){
next():①指针下移②将下移以后集合位置上的元素返回
}
//快捷键itit -->快速生成while循环
//ctrl+j 显示所有的快捷键
*System.out.println(it.next());输出集合
*/
//遍历完一次后如果还要再次遍历,需要重置迭代器:即重新it = coll.iterator();写一遍!

顶层接口

  • Collection是一个顶层接口,它主要用来定义集合的约定。
  • List接口也是一个顶层接口,它继承了Collection接口,同时也是ArrayList、LinkedList等集合元素的父类。
  • Set接口位于与List接口同级的层次上,它也继承了Collection接口。Set接口提供额外的规定。它对add、equals、hashCode方法提供了额外的标准。
  • Queue是和List、Set接口并列的继承Collection的三大接口之一。Queue的设计用来在处理之前保持元素的访问次序。除了Collection基础的操作之外,队列提供了额外的插入、读取、检查操作。
  • SortedSet接口直接继承于Set接口,使用Comparable对元素进行自然排序或者使用Comparable在创建时对元素提供定制的排序规则。set的迭代器将按升序元素顺序遍历集合。
  • Map是一个支持key-value存储的对象,Map不能包含重复的key,每个键最多映射一个值。这个接口代替了Dictionary类,Dictionary是一个抽象类而不是接口。
Collection的常用方法
  1. add:添加单个元素
  2. remove:删除指定元素
  3. contains:查找元素是否存在
  4. size:获取元素个数
  5. isEmpty判断是否为空
  6. clear:清空
  7. addAll:添加多个元素
  8. containsAll:查找多个元素是否都存在
  9. removeAll:删除多个元素
//          创建一个ArrayList集合
            List list = new ArrayList();
//            1. add:添加单个元素
            list.add("jack");
            list.add(10);//list.add(new Integer(10))
            list.add(true);
            System.out.println("add添加单个元素后list=" + list);
//            2. remove:删除指定元素
            list.remove(0);//删除第一个元素
            list.remove("true");//删除指定元素
            System.out.println("删除元素后list="+ list);
//            3. contains:查找元素是否存在
            System.out.println("查找jack是否存在"+list.contains("jack"));//返回
//            4. size:获取元素个数
            System.out.println("获取集合的元素个数:"+list.size());
//            5. isEmpty判断是否为空
            System.out.println("集合是否为空:"+list.isEmpty());
//            6. clear:清空
            list.clear();
            System.out.println("清空后list="+ list);
//            7. addAll:添加多个元素
            ArrayList arrayList = new ArrayList();
            arrayList.add("三国演义");
            arrayList.add("红楼梦");
            list.addAll(arrayList);
            System.out.println("添加多个元素后list=" + list);
//            8. containsAll:查找多个元素是否都存在
            System.out.println("查找多个元素是否都存在:"+list.containsAll(arrayList));
//            9. removeAll:删除多个元素
            list.removeAll(arrayList);
            System.out.println("删除多个元素后list=" + list);

在这里插入图片描述

List接口

  • List集合类中的元素有序(即添加顺序和取出顺序一致)、且可重复
  • List集合中的每个元素都有其对应的顺序索引,即支持索引
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
  • 可以存放null值

添加元素使用add()方法,访问元素使用get()方法,修改元素使用set()方法,删除元素使用remove()方法,计算大小使用size()方法。

        List list = new ArrayList();
//        void add(int index,Object ele):在index位置插入ele元素
        list.add("ywh");
        list.add(0,"java");
//        boolean addAll(int inex,Collection eles):从index位置开始将else中的所有元素添加进来
        List list2 = new ArrayList();
        list2.add("c++");
        list2.add("java");
        list.addAll(1,list2);
        System.out.println(list);
//        Object get(int index):获取指定index位置的元素
        System.out.println(list.get(1));
//        int indexOf(Object obj):返回obj在集合中首次出现的位置
        System.out.println(list.indexOf("c++"));
//        int lastIndexOf(Object obj):返回obj在集合中最后一次出现的位置
        System.out.println(list.lastIndexOf("java"));
//        Object remove(int index):移除指定index位置的元素,并返回此元素
        list.remove(0);
        System.out.println(list);
//        Object set(int index,Object ele):设置指定index位置的元素为ele,相当于是替换
        list.set(0,"python");
        System.out.println(list);
//        List sublist(int fromIndex,int toIndex):返回从fromIndex到toIndex位置的子集合
        //返回的集合是from= ------to-1之间的
        System.out.println(list.subList(0,2));
ArrayList!

ArrayList是实现了List接口的可扩容数组(动态数组),它的内部是基于数组实现的。它的具体定义如下:

public class ArrayList<E> extends AbstractList<E> implements List<E>,RandomAccess,Cloneable,java.io.Serializable{···}
  • ArrayList可以实现所有可选择的列表操作,允许所有的元素,包括null值。ArrayList还提供了内部存储list的方法,它能够完全替代Vector,只有一点例外,ArrayList不是线程安全的容器。
  • ArrayList有一个容量的概念,这个数组的容量就是List用来存储元素的容量。
  • 改查效率较高
  • ArrayList不是线程安全的容器,如果多个线程中至少有两个线程修改了ArrayList的结构的话就会导致线程安全问题,作为替代条件可以使用线程安全的List,应使用==Collections.synchronizedList==。
List list = Collections.synchronizedList(new ArrayList(...))
  • ArrayList具有fail-fast快速失败机制,能够对ArrayList作出失败检测。当迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常。
ArrayList的扩容机制
  • ArrayList维护了一个Object类型的数组elementData。

transient Object[] elementData;//transient 表示瞬间,短暂的,表示该属性不会被序列化

  • 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
  • 如果使用的是指定大小的构造器,则初始elementData为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
  • 每次添加元素都会调用方法判断是否需要扩容,将size+1作为最小扩容量传入,如果最小扩容量大于原数组的长度,则调用grow方法开始扩容;
  • grow方法是将旧容量右移,新容量=旧容量+旧容量右移一位(即1.5倍),之后新容量与最小扩容量比较:
    • 如果新容量小于最小扩容容量,则将最小扩容量赋值给新容量来扩容
    • 如果新容量大于最大数组长度,调用方法判断最小扩容量与最大数组长度的大小,如果最小扩容量大于最大数组长度,则新容量为最大int长度;反之新容量为最大数组长度;
  • 最后调用copyOf方法扩容,创建一个新的指定为新容量大小的空数组,调用arrayCopy方法复制原有数组插入到新数组中,等到一个包含原有元素的新扩容数组。

分析一下扩容机制:

首先先看一下ArrayList的构造函数:

    /**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    //用于空实例的共享空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /*
     用于默认大小的空实例的共享空数组。我们将这与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时要膨胀多少
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     *存储数组列表元素的数组缓冲区
     *数组列表的容量是此数组缓冲区的长度。添加第一个元素时,任何带有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的  	  空ArrayList 都将扩展到DEFAULT_CAPACITY=10
     *这是调用无参构造函数后,第一次添加元素时的扩容
     */
    transient Object[] elementData; // 非私有,以简化嵌套类访问

    /**
     * 数组列表的大小(它包含的元素数)
     */
    private int size;

    /**
     * 默认构造函数,构造初始容量为 10 的空列表(无参数构造)
     * 但此时还是空数组
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * @param  initialCapacity-列表的初始容量
     * @throws IllegalArgumentException如果指定的初始容量为负数
     * 带初始容量的构造函数(用户自己指定容量)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//指定的初始容量大于0
            //创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始容量等于0
            //创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//初始容量小于0,抛出IllegalArgumentException异常
            throw new IllegalArgumentException("IllegalCapacity: "+initialCapacity);
        }
    }

    /**
     * 按照集合的迭代器返回的顺序,构造包含指定Collection元素的列表.
     * @param c-要将其元素放入此列表中的集合
     * @throws NullPointerException 如果指定的集合为 null
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

这里以无参构造函数创建的ArrayList为例子分析

第一步:先看一下add添加元素的方法:

    /**
     * 将指定的元素追加到此列表的末尾.
     * @param e-要附加到此列表的元素
     * @return true(as specified by Collection.add)
     */
    public boolean add(E e) {
        //添加元素之前,先调用ensureCapacityInternal方法,确定是否要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        //这里指的是将e存入到数组已有元素数的位置的下一位
        elementData[size++] = e;
        return true;
    }

第二步:再来看看ensureCapacityInternal()方法

//得到最小扩容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //如果elementData数组是默认空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            /**
             *获取默认的容量和传入参数的较大值
             *当要add进第一个元素时,minCapacity(最小扩容量)为1,在Math.max()方法比较后,minCapacity为10
             */
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

//判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        /**overflow-conscious code
         *最小扩容量减数组长度大于0,即数组不足以再添加元素,需要扩容
         *第一次添加元素minapacity为10,数组长度为0,则开始扩容
         *之后每次添加元素minCapacity则为传入的size+1,直到当size+1=11时,再次调用grow方法进行扩容
         */
        if (minCapacity - elementData.length > 0)
            //调用grow方法开始扩容
            grow(minCapacity);
    }

第三步:看看grow()方法

    /**
     * 要分配的最大数组大小.
     * Some VMs reserve some header words in an array.
     * 尝试分配更大的数组可能会导致OutOfMemoryError: 请求的内存大小超出 VM 限制
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 增加容量以确保它至少可以容纳最小容量参数指定的元素数.
     * @param minCapacity-所需的最小容量
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        //oldCapacity为旧容量,等于数组原先的长度
        int oldCapacity = elementData.length;
        /**
         *oldCapacity >> 1,这个操作时将oldCapacity右移一位,其效果相当于oldCapacity/2.
         *位运算的速度远远快于整除运算
         *newCapacity为新容量,整句运算式的结果就是将新容量更新为旧容量的1.5倍.
         */
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //比较新容量与最小扩容量的大小,如果新容量小于最小扩容量,那么就把最小扩容量赋值给新容量,让最小扩容量当作新容量来扩容
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新容量大于最大数组大小,则调用hugeCapacity方法来比较minCapacity和MAX_ARRAY_SIZE的大小.
        //如果新容量大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`.
        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();
        //对minCapacity和MAX_ARRAY_SIZE进行比较
        //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
        //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
        //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

Arrays.copyOf()方法:

调用这个方法给原有数组扩容!!!

public static int[] copyOf(int[] original, int newLength) {
    	// 申请一个新的数组
        int[] copy = new int[newLength];
	// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
    //Math.min(original.length, newLength)比较出新容量跟原有数组容量大小的较小值
    //复制原数组0下标开始的Math.min(original.length, newLength)个元素,并插入到copy数组0下标开始的位置上
        return copy;
    }

System.arraycopy()方法:

// 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义
    /**
    *   复制数组
    * @param src 源数组
    * @param srcPos 源数组中的起始位置
    * @param dest 目标数组
    * @param destPos 目标数组中的起始位置
    * @param length 要复制的数组元素的数量
    */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

场景:

    /**
     * 在此列表中的指定位置插入指定的元素。
     *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
     *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //arraycopy()方法实现数组自己复制自己
        //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
    }

两者联系和区别

联系:

看两者源代码可以发现 copyOf()内部实际调用了 System.arraycopy() 方法

区别:

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 ;

copyOf() 是系统自动在内部新建一个数组,并返回该数组。

Vector!

Vector同ArrayList一样,都是基于数组实现的,只不过Vector是一个线程安全的容器,它对内部的每个方法都简单粗暴的上锁,避免多线程引起的安全性问题,但是通常这种同步方式需要的开销比较大,因此,它在访问效率上要远远低于ArrayList。

还有一点在扩容上,ArrayList扩容后的数组长度会增加50%,而Vector的扩容长度后数组会增加一倍。

  • 如果无参,默认10,满后,就按2倍扩容
  • 如果指定大小,则每次直接按2倍扩容
LinkedList类!

LinkedList是一个双向链表和双端队列,允许存储任何元素(包括null)。它的主要特性如下:

  • LinkedList所有的操作都可以表现为双向性的,索引到链表的操作将遍历从头到尾,视哪个距离近为遍历顺序。
  • 添加和删除对象,不通过数组完成,效率高
  • 注意这个实现也不是线程安全的,如果多个线程并发访问 链表,并且至少其中的一个线程修改了链表的结构,那么这个链表必须进行外部加锁。或者使用
List list = Collections.synchronizedList(new LinkedList(...))
Stack

堆栈是我们常说的后入先出的容器。它继承了Vector类,提供了通常的push和pop操作,以及在栈顶的peek方法,测试stack是否为空的empty方法,和一个寻找与栈顶距离的search方法。

第一次创建栈,不包含任何元素。一个更完善,可靠性更强的LIFO栈操作由Deque接口和他的实现提供,应该优先使用这个类

Deque<Integer>stack = new ArrayDeque<Integer>()

Set接口

  • 无序性:无序性不等于随机性,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值hashcode决定的。
  • 不可重复性:是指添加的元素按照equals()判断时,返回false,需要同时重写equals()方法和hashCode()方法。
  • Set遍历方式:增强for循环、迭代器遍历;但不能使用普通for循环遍历,因为for循环是根据数组的索引查找元素
HashSet!

HashSet是Set接口的的实现类,由哈希表支持(实际上HashSet是HashMap的一个实例)。它不能保证集合的迭代顺序。

  • HashSet即是HsahMap里面没有value,只有key。
  • 这个类允许null元素。
  • 注意这个实现不是线程安全的。如果多线程并发访问HashSet,并且至少一个线程修改了set,必须进行外部加锁。或者使用Collections.synchronizedList()方法进行重写。
  • 这个类是支持fail-fast机制

HashSet的存储原理:

  1. 首先,根据存储元素,调用其元素的hashCode方法,获取其哈希值

  2. 根据哈希值,在数组的下标为哈希值的位置上,查看是否存在元素。

    (1) 如果没有元素就直接存储进去

    (2)如果对应位置有元素,就调用其equals方法比较它们的值,看是否相等,相等就不添加,不相等就添加。

Hashcode:

返回该对象的哈希码,将对象的地址号通过计算得到哈希码,将哈希码分组为不同的区域,所以在集合中,插入元素时,通过计算其哈希码可以快速的知道该元素应该存储在哪个区域,减少使用equals方法比较次数。

HashSet如何检查重复

当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcodeHashSet会假设对象没有重复出现。但是如果发现有相同的hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。

在JOK1.8中,HashSetadd()方法只是简单的调用了HashMapput()方法,并且判断了一下返回值以确保是否有重复元素。直接看一下源码:

//Results:true if this set did not already contain the specified element
//返回值:当set中没有包含add的元素时返回true
public boolean add(E e){
    return map.put(e,PRESENT)==null;
}
TreeSet!

TreeSet是一个基于TreeMap的NavigableSet实现。这些元素使用它们的自然排序或者在创建时提供的Comparator进行排序具体取决于使用的构造函数。

  • 此实现为基本操作add、remove和contains提供了log(n)的时间成本。
  • 注意这个实现不是线程安全的。如果多线程并发访问TreeSet,并且至少一个线程修改了set,必须进行外部加锁,或者使用:
SortedSet s = Collections.synchronizedList(new TreeSet(...))
  • 这个实现类持有fail-fast机制
LinkedHashSet

LinkedHashSet继承于Set,先来看一下它的继承体系:
在这里插入图片描述

LinkedHashSet是Set接口的Hash表和LinkedList的实现。这个实现不同于HashSet的是它维护着一个贯穿所有条目的双向链表。此链表定义了元素插入集合的顺序。注意:如果元素重新插入,则插入顺序不会受到影响。

  • LinkedHashSet有两个影响其构成的参数:初始容量和加载因子。它们的定义于HashSet完全相同。但请注意:对于LinkedHashSet,选择过高的初始容量值的开销要比HashSet小,因为LinkedHashSet的迭代次数不受容量影响。
  • 注意LinkedHashSet也不是线程安全的,如果多线程同时访问LinkedHashSet,必须加锁,或者通过使用Collections.synchronized
  • 该类也支持fail-fast机制。

Map接口

Map接口六种遍历方式

首先以HashMap为例子:

Map map = new HashMap();
map.put("邓超","孙俪");
map.put("王宝强","马蓉");
map.put("宋喆","马蓉");
map.put("六零波","null");
map.put("null","刘亦菲");
map.put("鹿晗","关晓彤");
  • containsKey:查找键是否存在
  • keySet:查找所有键
//先取出所有的key,通过key去除对应的value
Set keyset = map.keySet();
//(1)增强for
System.out.println("---使用keySet方式---");
for(Object key:keyset){
    System.out.println(key+"-"+map.get(key));
}
//(2)迭代器--通过迭代器获取key
System.out.println("---使用keySet方式---");
Iterator iterator = keyset.iterator();
while(iterator.hasNext()){
    Object key = iterator.next();
    System.out.println(key+"-"+map.get(key));
}
  • entrySet:获取所有关系k-v
Set entrySet =map.entrySet();//EntrySet<Map,Entry<K,V>>
//(1)增强for
for(Object entry : entrySet){
    //将entry转成Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey()+"-"+m.getValue());
}
//(2)迭代器
Interator iterator3 = entry.iterator();
while(iterator3.hashNest()){
    Object entry = iterator3.next();
    //向下转型Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey()+"-"+m.getValue());
}
  • values:获取所有的值
//把所有的vals取出
Collection values = map.values();
//这里可以使用所有的Collections使用的遍历方法
//(1)增强for
System.out.println("---取出所有的value 增强for---");
for(Object value:values){
    System.out.println(value);
}
//(2)迭代器
System.out.println("---取出所有的value 迭代器---");
Iterator iterator2 = values.iterator();
while(iterator2.hasNext()){
    Object value = iterator2.next();
    System.out.println(value);
}
//不能使用普通for循环
HashMap!
  • HashMap是一个利用哈希表原理来存储元素的集合,并且允许空的key-value键值对。HashMap是非线程安全的,也就是说在多线程的环境下,可能会存在问题,而Hashtable是线程安全的容器。HashMap也支持fail-fast机制。HashMap的实例有两个参数影响其性能:初始容量和加载因子。可以使用==Collections.synchronizedMap(new HashMap(…))==来构造一个线程安全的HashMap。
  • HashMap可以存储null的key和balue,但null作为键只能有一个,null作为值可以有多个。
  • HashMap默认的初始化大小为16,之后每次扩容,容量变为原来的2倍。并且,HashMap总是使用2的幂作为哈希表的大小。

HashMap和Hashtable的区别

  • 线程是否安全HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部的方法基本都经过synchronized修饰。(如果要保证线程安全的话就使用ConcurrentHashMap);
  • 效率:因为线程安全问题,HashMap要比Hashtable效率高一点。另外,Hashtable基本被淘汰,不要在代码中使用;
  • 对Null key和Null value的支持HashMap可以存储null的key和value,但null作为key只能有一个,作为value可以有多个;Hashtable不允许有null的键值对,否则会抛出NullPointrException
  • 初始容量大小和每次扩容大小的不同:①创建时如果不指定容量初始值,Hashtable默认的护士大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16,之后没扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小(HashMap中的tableSizeFor()方法保证)。也就是说HashMap总是使用2的幂作为哈希表的大小。
  • 底层数据结构:JDK1.8之后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable没有这样的机制。

HashMap和HashSet的区别
如果你看过HashSet的源码的话,就应该知道:HashSet底层就是基于HashMap实现的。(HashSet的源码非常少,因为除了clone()、writeObject()、readObject()时HashSet自己不得不实现之外,其他方法都是直接调用HashMap中的方法。

HashMapHashSet
实现了Map接口实现Set接口
存储键值对仅存储对象
调用put()向map添加元素调用add()向Set添加元素
HashMap使用键(key)计算hashcodeHashSet使用成员对象来计算hahscode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性
HashMap底层机制和源码分析

在这里插入图片描述

<–到达临界值就要再次扩容–>

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

JDK1.8 HashMap的hash方法源码:

static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8 之后

相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

HashMap的扩容机制

  1. put()方法,先调用一个hash()方法,得到当前key的一个hash值,用于确定当前key应该存放在数组的哪个下标位置
  2. 调用putVal方法
    1. 首先判断table是否为空或者长度是0,如果是则开始第一次扩容,扩容为16,临界值为12
    2. 根据hash计算数组的下标
      1. 如果定位到的数组位置没有元素,就直接插入,判断是否要扩容
      2. 如果定位到的数组位置有元素就和要插入的key比较
        1. 如果key相同就直接覆盖,return,不会有后续操作
        2. 如果key不相同,就判断p是否是一个树节点
          1. 如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入
          2. 如果不是就遍历链表插入(插入的是链表尾部),插入时会判断链表长度是否大于阈值(8)
            1. 如果链表长度大于8,此时判断数组长度是否大于64,如果大于64则转化为红黑树插入,如果小于64则优先进行数组扩容
    3. 插入元素,要再判断数组元素个数是否大于临界值,大于临界值时要进行扩容,每次扩容为原先的2倍
  3. resize()扩容,进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有元素。
    1. 数组长度超过最大值,则不再扩容
    2. 数组长度没有超过最大值,则扩充为原来的2倍
TreeMap类!

一个基于NavigableMap实现的红黑树。这个map根据key自然排序存储,或者通过Comparator进行定制排序。

  • TreeMap为containsKey,get,put和remove方法提供了log(n)的时间开销。
  • 注意这个实现不是线程安全的。如果多线程并发访问TreeMap,并且至少一个线程修改了map,必须进行外部加锁。这通常是通过在自然封装集合的某个对象上进行同步来实现,或者使用
SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...))
  • 这个实现持有fail-fast机制。
LinkedHashMap类!

HashMap类的一个子类,同时实现Map接口。

public class LinkedHahMap<K,V> extends HashMap<K,V> implements Map<K,V>{......}

LinkedHashMap是Map接口的哈希表和链表的实现。这个实现与HashMap不同之处在于它维护了一个贯穿其所有条目的双向链表。这个链表定义了遍历顺序,通常是插入map中的顺序。

  • 它提供了一个特殊的LinkedHashMap(int,float,boolean)构造器来创建LinkedHashMap,其遍历顺序是其最后一次访问的顺序。
  • 可以重写removeEldestEntry(Map.Entry)方法,以便在将新映射添加到map时强制删除过期映射的策略。
  • 这个类提供了所有可选择的map操作,并且允许null元素。由于维护链表的额外开销,性能可能会低于HashMap,有一条除外:遍历LinkedHashMap中的collection-views需要与map.size成正比,无论其容量如何。HashMap的迭代看起来开销更大,因为还要求时间与其容量成正比。
  • LinkedHashMap有两个因素影响了它的构成:初始容量和加载因子。
  • 注意这个实现不是线程安全的。如果多线程并发访问LinkedHashMap,并且至少一个线程修改了map,必须进行外部加锁。这通常通过在自然封装集合的某个对象上进行同步来实现Map m = Collections.synchronizedMap(new LinkedHashMap(...)).
  • 这个实现持有fail-fast机制。
Hashtable类!

Hashtable类实现了一个哈希表,能够将键映射到值。任何非空对象都可以用作键或值。

  • 此实现类支持fail-fast机制
  • 与新的集合实现不同,Hashtable是线程安全的。如果不需要线程安全的容器,推荐使用HashMap,如果需要多线程高并发,推荐使用ConcurrentHashMap
properties类!

Hashtable类的一个子类。

IdentityHashMap类

IdentityHashMap是比较小众的Map实现了。

  • 这个类不是一个通用的Map实现!虽然这个类实现了Map接口,但它故意违反了Map的约定,该约定要求在比较对象时使用equals方法,此类仅适用于需要引用相等语义的极少数情况。
  • 同HashMap,IdentityHashMap也是无序的,并且该类不是线程安全的,如果要使之线程安全,可以调用Collections.synchronizedMap(new IdentityHashMap(...))方法来实现。
  • 支持fail-fast机制

Queue

PriorityQueue

PriorityQueueAbstractQueue的实现类,优先级队列的元素根据自然排序或者通过在构造函数时期提供Comparator来排序,具体根据构造器判断。PriorityQueue不允许null元素。

  • 队列的头在某种意义上是指定顺序的最后一个元素。队列查找操作pollremovepeekelement访问队列头部元素。
  • 优先级队列是无限制的,但具有内部capacity,用于控制用于在队列中存储元素的数组大小。
  • 该类以及迭代器实现了Collection、Iterator接口的所有可选方法。这个迭代器提供了iterator()方法不能保证以任何特定顺序遍历优先级队列的元素。如果你需要有序遍历,考虑使用Arrays.sort(pq.toArray())
  • 注意这个实现不是线程安全的,多线程不应该并发访问PriorityQueue实例,如果有某个线程修改了队列的话,使用线程安全的类PriorityBlockingQueue

PriorityQueue 在面试中可能更多的会出现在手撕算法的时候,典型例题包括堆排序、求第K大的数、带权图的遍历等,所以需要会熟练使用才行。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值