Java基础篇--集合

目录

前言

Java的容器体系

1.Java集合框架图

2.Collection类型

***:list和set的区别?

***:ArrayList和LinkedList对比?

***:ArrayList如何扩容?

***:Vector如何扩容?

***:ArrayList和Vector对比?

***:HashSet实现原理?

***:Stack实现原理?

***:CopyOnWriteArrayList是什么?

***:结果判断题1(for循环遍历)

***:结果判断题2(forEach遍历)

***:Java集合的遍历以及快速失败机制fail-fast

***:队列的元素存取方法区别?

3.Map类型

***:HashMap的数据结构和数据存储过程?

***:HashMap的扩容机制?

***:hashmap和hashtable的区别?

***:hashtable和concurrenthashmap对比?

4.Collections和Arrays工具类

***:Collections常用方法

***:Arrays常用方法

***:Collections和Arrays的排序算法


前言

带着问题学java系列博文之java基础篇。从问题出发,学习java知识。


Java的容器体系

在开始之前,我先讲一个争议点以及为什么我这里标题是“容器”体系。首先,关于map到底算不算集合,普遍存在争议点,有的人说map属于集合,有的人说map是键值对结构,不属于集合。咱们请出权威,《java编程思想》中第11章有写到list、set、queue和map都属于集合类;《java核心技术 卷一》第九版13.3节中有写到“集合有两个接口:Collection和Map”。我们之所以有争议,我觉得主要还是因为java对list、set和queue有一个公共的父接口Collection,而map是单独的Map接口。为了更好地理解,去除争议,我们不妨把它们都叫做容器,它们都是存储对象的容器。因此,我这里标题也是容器体系。以后遇到别人较真map的归属问题,可以淡定的说map属于容器--集合框架,不继承Collection接口。

1.Java集合框架图

这里写图片描述

2.Collection类型

这里写图片描述

  • set接口具体实现类:HashSet、LinkedHashSet、TreeSet、AbstractSet、CopyOnWriteArraySet、EnumSet、JobStateReasons
  • List接口具体实现类:ArrayList、LinkedList、Vector、AbstractList、CopyOnWriteArrayList、Stack、AttributeList、RoleList
  • Queue接口具体实现类:LinkedList、ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、PriorityQueue、ArrayDeque、ConcurrentLinkedQueue、ConcurrentLinkedDeque、LinkedBlockingDeque、AbstractQueue

***:list和set的区别?

list:有序、元素可以重复、可以使用简单的for循环进行遍历(因为每个元素有下标);

set:无序、元素不可重复、不可以使用简单的for循环进行遍历(元素没有下标);

***:ArrayList和LinkedList对比?

都实现list接口,具备list特征。

ArrayList:底层实现是数组,增删慢(对比LinkedList,如果仅仅是列表的头尾增删元素,二者效率相同)、查询快;

LinkedList:底层实现是链表,增删快、查询慢;

***:ArrayList如何扩容?

ArrayList底层依赖数组实现,初始化的时候如果不指定大小,则默认创建一个长度为0的数组,当调用add方法向其中添加元素时:如果数组长度为0,则创建一个长度为10的数组,并添加元素;如果数组满了,则触发扩容,新建一个长度是原来数组长度1.5倍的数组,将原来数组的元素完整复制到新数组,然后添加元素。

***:Vector如何扩容?

和ArrayList类似,Vector底层也是依赖数组实现,它的扩容和ArrayList也一样,只是当数组满时,新建的数组长度是原来的2倍。

***:ArrayList和Vector对比?

1.两个都是基于数组实现的;

2.ArrayList是非线程安全的,效率更高;Vector是线程安全的,允许多线程并发增删操作,效率较低;

3.ArrayList的扩容机制是每次扩大0.5倍;Vector的扩容机制是每次扩大1倍。

***:HashSet实现原理?

HashSet底层依赖hashmap实现,存储时hashset的值作为hashmap的key,而hashmap中key对应的value是一个默认的常量PRESENT(空对象),其他细节可以参考hashmap的实现原理。

***:Stack实现原理?

从集合框架图可以看到,stack是继承了vector,而vector又继承了abstractlist、list、collection接口。所以stack底层是一个collection集合、只能从某一端插入和删除的线性表,从而具备了栈特有的属性“先进后出”.

***:CopyOnWriteArrayList是什么?

CopyOnWriteArrayList(免锁容器),底层依赖ArrayList实现,区别于ArrayList是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。CopyOnWriteArrayList 使用乐观锁机制(乐观锁、悲观锁详见《Java基础篇--多线程》),写入操作将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。适合读多写少的场景,且实时性不佳,写操作会消耗大量内存,性能不佳。

***:结果判断题1(for循环遍历)

public class Test {
    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(i+"");
        }
        for (int i = 0; i < 5; i++) {
            if (list.get(i).equals("3")){
                System.out.println(list.remove(i));
            }
        }
        System.out.println(list.size());
    }
}

上面代码执行结果是?

先输出一个3,然后抛出IndexOutOfBoundsException异常。

public class Test {
    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(i+"");
        }
        for (int i = 0; i < 5; i++) {
            if (list.get(i).equals("3")){
                System.out.println(list.remove(i));
                break;
            }
        }
        System.out.println(list.size());
    }
}

上面代码执行结果是?

先输出一个3,然后再输出一个4。

为什么第一个会抛出数组越界异常呢?

通过对比两个代码的区别,我们可以看到,第一段代码for循环要执行5遍,当执行到第5遍的时候,抛出一个数组越界异常。这是因为我们代码在第4遍遍历的时候做了一个操作“list.remove()”。我们看下源码:

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

从源码可以看到,执行一次remove将会导致size自减(“--size”)。再看下get()方法的源码:

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

从源码可以看到,第一段代码之所以抛出异常,就是因为当遍历第5遍的时候,index是4,而size已经从5自减成了4(remove操作导致的)。所以rangeCheck方法检查到数组越界,抛出IndexOutOfBoundsException异常。

 

***:结果判断题2(forEach遍历)

public class Test {
    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(i+"");
        }
        for (String s : list) {
            if (s.equals("2")) System.out.println(list.remove(s));
        }
    }
}

上面代码执行结果是?

先输出一个true,然后抛出ConcurrentModificationException异常。

public class Test {
    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(i+"");
        }
        for (String s : list) {
            if (s.equals("3")) System.out.println(list.remove(s));
        }
    }
}

上面代码执行结果是?

输出一个true。

两次代码完全相同,仅仅是更换了一个值,为什么一个抛出异常,一个不抛出异常呢?

首先我们要清楚,foreach循环其实就是iterator遍历,等价于这段代码:

      Iterator<String> iterator = list.iterator();
      while (iterator.hasNext()){
          String s = iterator.next();
          ……

第一段代码foreach循环执行到第三遍的时候,list执行了remove操作。我们看下源码:

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

可以看到,又是size自减了,因此第一段代码继续执行第四遍循环,iterator.hasnext返回的是true(集合中有4个元素),进入到循环体,执行iterator的next方法,此时触发了java集合的快速失败机制fail-fast,抛出ConcurrentModificationException异常;而第二段代码是在第四遍循环时执行了remove操作,当第五遍循环时,iterator.hasnext返回false(集合中有4个元素,此时的cursor也到了下标4),退出while循环,所以第二段代码可以正常执行完毕。

***:Java集合的遍历以及快速失败机制fail-fast

set集合由于存储时无序、无下标,因此对set集合的遍历只能是foreach或者iterator;

list集合存储时有序、有下标,因此可以使用简单的for循环进行遍历,也可以使用foreach或者iterator遍历。

从上面的结果判断题引出了一个“快速失败机制fail-fast”,那它是什么呢?

它是java集合的一种错误检测机制,当线程对集合进行结构上的改变时,就有可能产生fail-fast。迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。还是看下源码最清晰:

//ArrayList的modCount变量
protected transient int modCount = 0;

//ArrayList的fastremove方法
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,numMoved);
    elementData[--size] = null; // clear to let GC do its work
}


//Arraylist的内部类Itr
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() {}

    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<? super E> 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();
    }
}

从源码可以看到,ArrayList定义了一个int型成员变量modCount,每次执行迭代器的next方法时,都会先执行checkForComodification方法,判断一下modCount和expectedModCount是否一致,不一致则抛出ConcurrentModificationException异常。而ArrayList自身的remove(Obejct o)方法的执行会改变modCount(“modCount++”),但是此时内部类Itr的成员变量expectedModCount并没有跟着一起改变,所以上面的结果判断题2才会抛出异常。因此,当我们需要在遍历集合的同时对集合进行结构上的改变,就要用iterator迭代器来实现,通过iterator的hasnext和next进行遍历,iterator的remove()来实现结构上的改变;而不能简单的使用for循环或者foreach循环,调用集合自身的remove方法。

 

 

***:队列的元素存取方法区别?

首先要区分非阻塞队列和阻塞队列:

非阻塞队列(Queue):add、remove、offer、poll、element(特有)、peek(特有)

阻塞队列(BlockingQueue):add、remove、offer(E)、offer(E,long,TimeUnit)(特有)、poll(long,TimeUnit)(特有)、put(特有)、take(特有)

上面标有“特有”的,表示仅该类队列特有方法。

方法解释:

element():非阻塞队列特有方法,返回队列头部元素,如果队列为空,则抛出NoSuchElementException异常;

peek():非阻塞队列特有方法,返回队列头部元素,如果队列为空,则返回null;

存取元素方法对比
 抛出异常特殊值阻塞超时
插入元素add(e)offer(e)

put(e)

offer(e,time,timeUnit)
移除元素remove()poll()take()poll(time,timeUnit)

 

add(e)和remove()是集合基本方法,当队列满时,add(e)方法向队列中添加元素失败,抛出IIIegaISlabEepeplian异常;当队列为空时,remove()方法从队列头部移除并返回元素失败,抛出NoSuchElementException异常;

offer(e)和poll()是队列基本方法,当队列满时,offer(e)方法向队列中添加元素失败,返回false;当队列为空时,poll()方法从队列头部移除并返回元素失败,返回null;

put(e)和take()是阻塞队列特有方法,当队列满时,put(e)方法向队列中添加元素被阻塞,一直等待队列中有空间,则存入;当队列为空时,take()方法从头部移除并返回元素被阻塞,一直等待队列中有元素,则移除。

offer(e,time,timeUnit)和poll(time,timeUnit)是阻塞队列特有方法,当队列满时,offer方法向队列中添加元素被阻塞,等待参数time和timeUnit设定的时长,超时不再等待;当队列为空时,poll方法从队列头部移除并返回元素被阻塞,等待参数time和timeUnit设定的时长,超时直接返回null。

 

3.Map类型

map接口继承数:

map类型区别于Collection类型,map是key-value键值对形式,key作为元素在容器中的定位,容器中完整存储key-value的Map.Entry。

***:HashMap的数据结构和数据存储过程?

Java8更新后的HashMap数据结构是:数组+链表,当链表中存储的元素超过一定长度(默认8)时,不再使用链表,扩容为红黑树。

hashmap数据存储过程:(hashmap数据结构是数组+链表,我们可以形象的理解为数组的每一个节点是一个桶,桶中可以存放key-value的Map.Entry)

首先根据key的hash值进行运算(hash & (数组length -1)),找到该key在数组中的索引index;然后分为三种情况:

  1. 如果此时该索引对应的桶没有任何数据,则生成一个节点的链表(相当于初始化桶),节点中保存Map.Entry<K,V>,包含key和value。
  2. 如果该索引对应的桶是一个链表,并且已存有数据,则根据key的hash值遍历链表,查找是否有相同key的节点。如果找到,则覆盖该节点,并返回节点中存储的值;没找到,则在链表尾部插入一个新节点,存储数据(涉及扩容)。
  3. 如果该索引对应的桶是一个红黑树(当一个桶中链表的节点数超过8(jdk默认)时,则进行扩容,改为红黑树存储),此时遍历方式有所变化,不过也是和链表一样,根据hash值找重复节点,有则覆盖节点,并返回覆盖前的value值,没有重复节点,则生成叶子节点存储数据;

***:HashMap的扩容机制?

默认初始化时,hashmap的桶数组初始容量为2^4(16);当数据过多时,以2倍扩容桶数组;注意当桶扩容了,需要对存储数据的链表或者红黑树进行重排。

 

***:hashmap和hashtable的区别?

  • hashtable继承Dictionary类,是java早期的接口;hashmap继承AbstractMap类,是新版jdk提供的接口;二者都实现了map接口;
  • hashtable不允许null键和null值,hashmap允许null键和null值;
  • hashtable是线程安全的,存取数据效率较低;hashmap是非线程安全的,存取数据效率较高。

***:hashtable和concurrenthashmap对比?

hashtable底层依赖hashmap实现,它的线程同步依赖于synchronize关键字,内部对存取方法都加上了synchronize。因此它的存取方法会对整个容器进行加锁,导致效率低下;

concurrenthashmap采用了桶的概念,仅对同一个hash值下的链表或者红黑树进行同步,当多线程并发操作时,仅当涉及同一个桶中的数据操作,才会有加锁同步;不同hash值下的数据操作互不影响,也不需要锁等额外的同步消耗,所以它的效率较高,性能很好。当需要线程安全的map容器时,推荐使用concurrenthashmap,不建议使用hashtable。

 

4.Collections和Arrays工具类

Collections和Arrays是jdk给我们提供的两个静态工具类,里面有很多封装好的操作集合和数组的方法。

***:Collections常用方法

addAll(Collection<? super T> c, T... elements ):将所有元素添加到指定集合;

copy(List<? super T> dest, List<? extends T> src):将src列表中的所有元素复制到dest列表;

fill(List<? super T> list, T obj):用obj元素替换list中的所有元素;

list(Enumeration<T> e):将Enumeration中所有的元素按照枚举的顺序,添加到ArrayList,并返回该列表;

max(Collection<? extends T> coll):返回集合中最大的元素(不指定comparator,则调用对象的compare方法);

min(Collection<? extends T> coll):返回集合中最小的元素(不指定comparator,则调用对象的compare方法);

replaceAll(List<T> list, T oldVal, T newVal):将列表中所有的oldVal元素替换成newVal;

reverse(List<?> list):反转列表的元素顺序;

swap(List<?> list, int i, int j):交换列表指定下标的元素;

sort(List<T> list):对列表进行排序(不指定comparator,则调用对象的compare方法);

***:Arrays常用方法

asList(T... a):将a中的所有元素添加到ArrayList中,并返回该列表;

copyOf(T[] original, int newLength):复制数组original中的元素填充到新数组,复制newLength个,如果original数组中不存在这么多,则用null或者0填充,并返回新数组;

copyOfRange(T[] original, int from, int to):复制数组original中取值在from和to之间的元素填充到新数组,并返回新数组;

equals(Object[] a, Object[] a2):比较两个数组,仅当两个数组元素彼此完全相同时(顺序一致,值一致),返回true;

fill(Object[] a, Object val):用val替换数组a中的所有元素;

sort(Object[] a):对数组进行排序;

sort(Object[] a, int fromIndex, int toIndex):对数组指定下标范围的元素进行升序排序;

***:Collections和Arrays的排序算法

Collections.sort():LegacyMergeSort.userRequested 为true使用归并排序(将来有可能移除归并排序);不为true使用TimeSort算法排序(Timesort是结合归并和插入排序算法得到的排序算法);

Arrays.sort():数组长度length小于47,使用插入排序;47<=length<286,使用双轴快速排序;286<=length,且连续性较好,使用归并排序;连续性不好,使用双轴快速排序。

 

 

 


以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值