Java集合基础知识总结

139 阅读9分钟

Java中有那些集合以及其数据结构?

首先有两大类集合接口:Collection接口和Map接口,在其下面Collection下面又有List,常用的有Linklist,ArrayList,queue队列比如说priorityqueue和ArrayDeque,还有set,常见用的有hashset,TreeSet等。在Map接口下有,最常用的HashMap,TreeMap,Hashtable。

  • List:

    • ArrayList底层是数组;
    • Linklist底层数据结构是双向链表。
  • set:
    • Treeset的底层是红黑树;
    • Hashset底层是基于hashmap实现;
  • queue
    • priorityqueue底层是数组实现的小顶堆;
    • ArrayDeque底层是可扩容的双向数组;
  • Map
    • HashMap底层是jdk1.8之前是数组+链表,jdk1.8之后是数组+链表+红黑树;
    • LinkedHashMap底层是jdk1.8之前是数组+链表+双向链表,jdk1.8之后是数组+链表+红黑树+双向链表;
    • TreeMap底层是红黑树;
    • Hashtable是数组+链表。

如何选择使用什么集合?为什么要使用集合?

这个从不同集合的特点考虑:List集合有序可重复,set集合无序不可重复,queue集合队列,特定的排队规则,可重复,Map集合存储键值对。

当然如果说还要考虑线程安全问题:就可以使用ConcurrentHashMap,CopyOnWriteArrayList之类的集合。

集合说白点就是容器,便于存储和处理数据。

无序性和不可重复性

无序性是:存储数据不是按照数组索引的顺序添加,而是通过计算hashcode插入

不可重复性,我的理解是经过equals判断,为false,通过重写equals和hashcode实现。

vector和stack

底层都是数组,vecter是线程安全的数组,方法用synchronized关键字进行了同步处理,stack继承了vecter实现了先进后出的栈,现在基本都不用了。

LinkedList

底层是通过双向链表实现,线程不安全。

数据增加:如果是在链表首或者链表尾时间复杂度为O(1),如果插入指定位置,时间复杂度为O(n);

数据删除:如果是在链表首或者链表尾时间复杂度为O(1),如果指定位置,时间复杂度为O(n)。

不可以快速访问,没有实现RandomAccess接口,因为底层是链表。

空间占用比ArrayList大,因为节点还要存放上下节点的信息,一般不怎么使用。

ArrayList

特点

底层是通过数组实现,线程不安全;

数据增加:如果是在首或者尾时间复杂度为O(1),如果插入指定位置,时间复杂度为O(n);

数据删除:如果是在首或者尾时间复杂度为O(1),如果指定位置,时间复杂度为O(n)。

可以快速访问,实现了RandomAccess接口,因为底层是数组有下标。

ArrayList和Array对比

存储类型不同:ArrayList只可以存储引用类型,基本数据类型会被自动装箱,Array还可以存储基本数据类型;

泛型支持:ArrayList支持泛型,Array不支持;

长度不同:ArrayList长度可以动态扩容,Array在定义后就不可以再更改;

API不同:ArrayList提供添加,删除等API,Array没有;

扩容机制

在最开始创建ArrayList的时候,调用无参构造方法的时候,生成的是空的数组,直到进行第一步add操作后,真正分配容量为十,然后先判断在扩容,也就是说如果在添加第十一个元素的时候,ArrayList判断大于当前长度,然后就进行扩容,扩容的长度为原来的1.5倍,int newCapacity = oldCapacity + (oldCapacity >> 1)。

Comparable和Comparator的区别?

类需要实现Comparable接口,然后重写其中的comparaTo方法,定义类的默认比较方法,实现比较;

而Comparator是比较器,他不需要再类实现接口,而是根据需求直接定义类的比较方法,就像使用

// 定制排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
});

Treeset,Hashset,LinkedHashset的区别

三个集合均实现了set接口,能够实现去重,他们最大的不同就是底层数据结构不同,然后导致使用的场景不同;

Hashset数据结构采用的是:hashmap,他的很多内部方法都是直接用调用的hashmap,比如说他的add()方法调用的是put方法,用于存储键值对,对添加数据、取出数据没有顺序要求的场景;

LinkedHashset:双向链表和哈希链表,数据添加和取出满足FIFO;

Treeset:底层数据是红黑树,适用于需要去重且排序的场景。

补充:HashSet如何检查重复

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

Queue和Deque的区别

Queue是一个队列,数据的取出和插入,支持FIFO,提供了查看队头元素,取出队头元素,插入队尾元素等操作,比如说PrioityQueue和BlockQueue。

Deque是一个双端队列,FIFO,也可以进行栈相关的操作,先进后出,比如ArrayDeque。

简单说一下ArrayDeque,PrioityQueue和BlockingQueue

ArrayDeque:底层数据结构是可扩容双向数组;

PrioityQueue:优先级队列,是一个优先级队列,根据优先级出队,通常,元素需要实现 Comparable 接口或提供自定义的 Comparator 来定义优先级;

BlockingQueue:阻塞队列,支持在队伍为空或者队伍已满的时候阻塞。

HashMap和Hashtable的区别

底层数据结构不同:Hashmap在Jdk8之前是数组+链表,后来是数组+链表+红黑树,Hashtable是数组+链表;

安全性不同:HashMap不安全,Hashtable线程安全;

是否能存null:HashMap可以存储一个key为null的值,Hashtable不能存储null值;

扩容不同:Hashmap初始值16,两倍扩容,hashtable初始值11,扩容为2n+1;

性能不同:HashTable性能更差。

HashMap和TreeMap

底层数据结构不同:Hashmap在Jdk8之前是数组+链表,后来是数组+链表+红黑树,TreeMap底层是红黑树;

安全性来说:都不安全

存储数据顺序来说:TreeMap存储的顺序有序,Hashmap是无顺序的;

能否存储null:HashMap的key可以是null,而TreeMap的key不能为null;

HashMap和Hashset

底层数据结构不同:Hashmap在Jdk8之前是数组+链表,后来是数组+链表+红黑树,Hashset是基于hashmap实现,底层是哈希表;

存储元素不同:Hashmap存储的键值对,hashset存储的是单个对象;hashMap的value可以相同,key不能相同,Hashset的存储的元素不能重复;

实现的父接口不同:HashMap继承于Map,Hashset继承自Collection。

HashMap

HashMap的数据结构?

这个得分jdk1.8之前和之后,jdk1.8之前使用的是数组+链表,jdk1.8之后采用的是数组+链表+红黑树。

红黑树:为什么使用红黑树?怎么保持平衡的?

红黑树是平衡二叉树,进行增删操作的时间复杂度都是O(logn),性能好。

红黑树的特点:

    • 根节点是黑色;
    • 叶子节点是黑色;
    • 红节点的子节点是黑色;
    • 黑高度相等:任一一节点到其子树节点中的黑节点的数量相同;
    • 保持平衡

保持平衡:通过旋转和染色保持平衡。

怎么进行put的?get怎么运行的?

hashmap执行put操作:

  • 通过扰动函数对key计算hash值,然后hash & (size - 1)计算出桶位置;
  • 如果没有发生哈希碰撞,则直接插入数据;
  • 如果发生了哈希碰撞以链表存在桶的位置,插入数据;
  • 如果链表长度大于8,并且数组长度大于64,则转变了红黑树;
  • 此时节点如果已经存在,则覆盖掉old值;
  • 如果出现了容量不够,进行hashmap扩容(这里是指jdk1.8,jdk8先插入,后扩容);

hashmap的get操作?

  • 通过扰动函数对key计算hash值,然后hash & (size - 1)计算出桶位置;
  • 然后去找这个节点,如果能直接找到,则返回;
  • 如果是红黑树或者链表就需要遍历寻找。
  • 找到匹配的值,返回对应的值;
  • 没找到返回null。

为什么HashMap的容量是2的倍数?故意初始化为17怎么初始化?

我认为有两个原因:一个是为了方便哈希寻址,hashmap的地址是通过对key值通过扰动函数得到哈希值,然后在和地址-1按位与,得到的结果,那么如果是2的倍数就可以直接进行位运算,比hash%2效率更高;

另一个就是,减少哈希碰撞,因为2的倍数的二进制的值,只有一位是1,进行计算后能充分散列函数。

故意初始化不是2的倍数,在HashMap中会自动向上取值到2的倍数,比如初始化为17,实际初始化的值为32.

什么时候扩容?为什么扩容因子为0.75?

当满足扩容因子为0.75时,也就是,如果现在长度为16,当有到12的时候,就是进行扩容。

之所以设计为0.75,是考虑到,时间和空间的平衡问题:

如果扩容因子太大:就会频繁的产生哈希碰撞,查找时间更多;

如果扩容因子太小:相对不容易发生哈希碰撞,但是可能会浪费太多的空间。

说一下扩容机制?(重点在JDK1.8之后)

在JDK1.7之前,达到扩容因子0.75后,就开始扩容,然后调用rehash,重新进行hash寻址放入新的位置。

在JDK1.8之后

  • 达到扩容因子,进行二倍扩容;
  • 然后优化了rehash,将数据分为两种情况,一种是位置不变,另一种就是数据的原本位置+数据的原来的容量。

hashmap在jdk1.8做了哪些优化?

数据结构优化:数据结构由数组+链表,到数组+链表+红黑树;

rehash算法优化:之前rehash会将所有的元素重排,优化之后重排部分算法;

链表的插入方式:之前是头插法,后来改为了尾插法;

扩容时间改变:之前是线扩容后插入,后来是插入后再判断是否需要扩容。

ConcurrentHashMap 线程安全的具体实现方式/底层具体实现?

JDK1.7之前:ConcurrentHashMap的底层数据结构是数组+链表,保持安全使用的是segment锁,这个继承于RTLock,有16个,每个锁锁一个HashEntry数组,HashEntry本身就是一个链表的结构,如果要处理信息必须得先拿到这个锁才可以。

jdk1.8之后,改成了用syn+CAS锁保持安全,每个锁锁住的是链表头,或者是根节点,锁粒度更细。

ConcurrentHashMap 为什么 key 和 value 不能为 null?

避免二义性的出现,要求更加严格。