Java集合类浅谈

Collection接口 

  1. add(E e):将指定的元素添加到此集合(可选操作)。
  2. addAll(Collection<? extends E> c):将指定集合中的所有元素添加到此集合(可选操作)。
  3. remove(Object o):从该集合中删除指定元素的单个实例(如果存在)(可选操作)。
  4. removeAll(Collection<?> c):删除指定集合中包含的所有此集合的元素(可选操作)。
  5. removeIf(Predicate<? super E> filter):删除满足给定谓词的此集合的所有元素。
  6. contains(Object o):如果此集合包含指定的元素,则返回 true 。
  7. containsAll(Collection<?> c):如果此集合包含指定 集合中的所有元素,则返回true。
  8. retainAll(Collection<?> c):仅保留此集合中包含在指定集合中的元素(可选操作)。
  9. isEmpty():如果此集合不包含元素,则返回 true 。
  10. size():返回此集合中的元素数。
  11. clear():从此集合中删除所有元素(可选操作)。
  12. equals(Object o):将指定的对象与此集合进行比较以获得相等性。
  13. hashCode():返回此集合的哈希码值。
  14. iterator():返回此集合中的元素的迭代器。
  15. stream():返回以此集合作为源的顺序 Stream 。
  16. toArray():返回一个包含此集合中所有元素的数组。

List接口

  1. add(int index, E element):将指定的元素插入此列表中的指定位置(可选操作)。
  2. addAll(int index, Collection<? extends E> c):将指定集合中的所有元素插入到此列表中的指定位置(可选操作)。
  3. get(int index):返回此列表中指定位置的元素。
  4. indexOf(Object o):返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。
  5. lastIndexOf(Object o):返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。
  6. remove(int index):删除该列表中指定位置的元素(可选操作)。
  7. set(int index, E element):用指定的元素(可选操作)替换此列表中指定位置的元素。
  8. subList(int fromIndex, int toIndex):返回此列表中指定的 fromIndex (含)和 toIndex之间的视图。
  9. replaceAll(UnaryOperator<E> operator):将该列表的每个元素替换为将该运算符应用于该元素的结果。
  10. sort(Comparator<? super E> c):使用随附的 Comparator排序此列表来比较元素。

Set接口

Queue接口

  1. add(E e):将指定的元素插入到此队列中,如果可以立即执行此操作,而不会违反容量限制, true在成功后返回 IllegalStateException如果当前没有可用空间,则抛出IllegalStateException。
  2. element():检索,但不删除,这个队列的头。
  3. offer(E e):如果在不违反容量限制的情况下立即执行,则将指定的元素插入到此队列中。
  4. peek():检索但不删除此队列的头,如果此队列为空,则返回 null 。
  5. poll():检索并删除此队列的头,如果此队列为空,则返回 null 。
  6. remove():检索并删除此队列的头。

Map接口

  1. put(K key, V value):将指定的值与该映射中的指定键相关联(可选操作)。
  2. putAll(Map<? extends K,? extends V> m):将指定地图的所有映射复制到此映射(可选操作)。
  3. putIfAbsent(K key, V value):如果指定的键尚未与某个值相关联(或映射到 null )将其与给定值相关联并返回 null ,否则返回当前值。
  4. get(Object key):返回到指定键所映射的值,或 null如果此映射包含该键的映射。
  5. remove(Object key):如果存在(从可选的操作),从该地图中删除一个键的映射。
  6. remove(Object key, Object value):仅当指定的密钥当前映射到指定的值时删除该条目。
  7. replace(K key, V value):只有当目标映射到某个值时,才能替换指定键的条目。
  8. replace(K key, V oldValue, V newValue):仅当当前映射到指定的值时,才能替换指定键的条目。
  9. replaceAll(BiFunction<? super K,? super V,? extends V> function):将每个条目的值替换为对该条目调用给定函数的结果,直到所有条目都被处理或该函数抛出异常。
  10. containsKey(Object key):如果此映射包含指定键的映射,则返回 true 。
  11. containsValue(Object value):如果此地图将一个或多个键映射到指定的值,则返回 true 。
  12. isEmpty():如果此地图不包含键值映射,则返回 true 。
  13. size():返回此地图中键值映射的数量。
  14. clear():从该地图中删除所有的映射(可选操作)。
  15. entrySet():返回此地图中包含的映射的Set视图。
  16. keySet():返回此地图中包含的键的Set视图。
  17. equals(Object o):将指定的对象与此映射进行比较以获得相等性。
  18. values():返回此地图中包含的值的Collection视图。
  19. forEach(BiConsumer<? super K,? super V> action):对此映射中的每个条目执行给定的操作,直到所有条目都被处理或操作引发异常。
  20. hashCode():返回此地图的哈希码值。

Iterator接口

  1. forEachRemaining(Consumer<? super E> action):对每个剩余元素执行操作,直到所有元素都被处理或动作引发异常。
  2. hasNext():如果迭代具有更多元素,则返回 true 。
  3. next():返回迭代中的下一个元素。
  4. remove():从底层集合中删除此迭代器返回的最后一个元素(可选操作)。

Collections工具类

  1. addAll(Collection<? super T> c, T... elements)将所有指定的元素添加到指定的集合。
  2. binarySearch(List<? extends Comparable<? super T>> list, T key):使用二叉搜索算法搜索指定对象的指定列表。
  3. copy(List<? super T> dest, List<? extends T> src):将所有元素从一个列表复制到另一个列表中。
  4. disjoint(Collection<?> c1, Collection<?> c2):如果两个指定的集合没有共同的元素,则返回 true 。
  5. fill(List<? super T> list, T obj):用指定的元素代替指定列表的所有元素。
  6. frequency(Collection<?> c, Object o):返回指定集合中与指定对象相等的元素数。
  7. replaceAll(List<T> list, T oldVal, T newVal):将列表中一个指定值的所有出现替换为另一个。
  8. reverse(List<?> list):反转指定列表中元素的顺序。
  9. sort(List<T> list):根据其元素的natural ordering对指定的列表进行排序。
  10. swap(List<?> list, int i, int j):交换指定列表中指定位置的元素。

Arrays工具类

  1. binarySearch():使用二进制搜索算法搜索指定数组的指定值。
  2. copyOf():复制指定的数组。
  3. copyOfRange():将指定数组的指定范围复制到新数组中。
  4. equals():判断两个数组是否相等。
  5. fill():把a数组所有值都赋给b。
  6. sort():对数组a排序。
  7. toString():数组转化成字符串。

ArrayList

定义

ArrayList是动态数组,与传统数组相比,它的容量能动态增长。

它继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable这些接口。

原理

本质是一个封装的数组,扩容时将原始数组数据拷贝到新数组,然后将新数组作为扩容之后的数组。数组扩容的操作代价很高,我们应该尽量减少这种操作。

扩容机制

默认长度为0(无参先扩充到10)

指定容量则扩充到旧容量1.5倍

线程安全:否

优点

  1. ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快。
  2. ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已。
  3. 根据下标遍历元素,效率高。
  4. 根据下标访问元素,效率高。
  5. 可以自动扩容,默认为每次扩容为原来的1.5倍。

缺点

  1. 插入和删除元素的效率不高。
  2. 根据元素下标查找元素需要遍历整个元素数组,效率不高。
  3. 线程不安全。

实现方式

ArrayList<String> arrayList = new ArrayList<>();

ArrayList<Integer> arrayList = new ArrayList<>();

常见操作

add():添加一个元素到当前列表的末尾

Insert():用于添加一个元素到指定位置,列表后面的元素依次往后移动

InsertRange用于从指定位置开始添加一批元素,列表后面的元素依次往后移动

remove():删除一个元素,通过元素本身的引用来删除

removeAt:删除一个元素,通过索引值来删除

RemoveRange():用于删除一批元素,通过指定开始的索引和删除的数量来删除

Clear():清除现有所有的元素

set(int index, E element):修改指定索引处的元素,返回被修改的元素

get(int index):返回此列表中指定位置上的元素

遍历

for循环的遍历方式

for (int i = 0; i < lists.size(); i++) {

     System.out.print(lists.get(i));

}

foreach的遍历方式

for (Integer list : lists) {

    System.out.print(list);

}

Iterator的遍历方式

for (Iterator<Integer> list = lists.iterator(); list.hasNext();) {

     System.out.print(list.next());

}

判断是否为空:isEmpty()

返回元素个数:size()

返回索引位置元素:index()

查找某个对象在不在列表之中:Contains()

将ArrayList固定到实际元素的大小,当动态数组元素确定不在添加的时候,可以调用这个方法来释放空余的内存:TrimSize()

把ArrayList的元素Copy到一个新的数组中:ToArray()

Vector

Stack

定义

栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。 栈的特点是:先进后出,或者说是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈。

优点

提供后进先出方式的存取

缺点

存取其他项很慢

使用场景

栈常应用于实现递归功能方面的场景,例如斐波那契数列。

API实现

Stack<Integer> s = new Stack<>();

常用操作

遍历:for each

压栈:push(E item)

出栈:pop()

查看栈顶对象:peek()

判断此堆栈是否为空:empty()

Queue

定义

队列与栈一样,也是一种线性表,不同的是,队列可以在一端添加元素,在另一端取出元素,也就是:先进先出。从一端放入元素的操作称为入队,取出元素为出队,示例图如下:

优点

提供先进先出方式的存取

缺点

存取其他项很慢

使用场景

因为队列先进先出的特点,在多线程阻塞队列管理中非常适用。

API实现

Queue<String> q = new LinkedList<>();

常用操作

检索但不删除此队列头:peek()

检索并删除此队列的头:poll()

将指定的元素插入到此队列中(会先判断容量):add(E e)

检索并删除此队列的头:remove()

LinkedList

定义

链表是物理存储单元上非连续的、具有逻辑顺序的存储结构。

数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个结点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域。根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表等。

优点

链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;

添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;

缺点

因为含有大量的指针域,占用空间较大;

查找元素需要遍历链表来查找,非常耗时。

适用场景

数据量较小,需要频繁增加,删除操作的场景

API实现

LinkedList<Integer> nums = new LinkedList<>();

常见操作

add(E e):将指定的元素追加到此列表的末尾。

add(int index, E element):在此列表中的指定位置插入指定的元素。

remove(int index):删除该列表中指定位置的元素。

clear():从列表中删除所有元素。

set(int index, E element):用指定的元素替换此列表中指定位置的元素。

get(int index):返回此列表中指定位置的元素。

遍历

  1. for方式
  2. foreach方式
  3. Iterator方式遍历

返回此列表中的元素数:size()

以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组:toArray()

如果此列表包含指定的元素,则返回 true :contains(Object o)

HashSet

Hashtable

定义

哈希表(Hash table,也叫散列表),具有像数组那样根据随机访问的特性,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

几种常见的哈希函数(散列函数)构造方法

直接定址法

取关键字或关键字的某个线性函数值为散列地址。

即 H(key) = key 或 H(key) = a*key + b,其中a和b为常数。

除留余数法 

取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。

即 H(key) = key % p, p < m。 

数字分析法 

当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。

仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。

平方取中法 

先计算出关键字值的平方,然后取平方值中间几位作为散列地址。

随机分布的关键字,得到的散列地址也是随机分布的。

折叠法(叠加法) 

将关键字分为位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为散列地址。

用于关键字位数较多,并且关键字中每一位上数字分布大致均匀。 

随机数法

选择一个随机函数,把关键字的随机函数值作为它的哈希值。

通常当关键字的长度不等时用这种方法。

hash冲突的减少办法

开放寻址法

所有的元素都在散列表中,每一个表项或包含动态集合的一个元素,或包含NIL。这种方法中散列表可能被填满,以致于不能插入任何新的元素。在开放寻址法中,当要插入一个元素时,可以连续地检查或探测散列表的各项,直到有一个空槽来放置待插入的关键字为止。有三种技术用于开放寻址法:线性探测、二次探测以及双重探测。

线性探测

给定一个普通的散列函数h':U —>{0,1,.....,m-1},线性探测方法采用的散列函数为:h(k,i) = (h'(k)+i)mod m,i=0,1,....,m-1

探测时从i=0开始,首先探查T[h'(k)],然后依次探测T[h'(k)+1],…,直到T[h'(k)+m-1],此后又循环到T[0],T[1],…,直到探测到T[h'(k)-1]为止。探测过程终止于三种情况: 

若当前探测的单元为空,则表示查找失败(若是插入则将key写入其中); 

若当前探测的单元中含有key,则查找成功,但对于插入意味着失败; 

若探测到T[h'(k)-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。

线性探测方法较容易实现,但是存在一次群集问题,即连续被占用的槽的序列变的越来越长。采用例子进行说明线性探测过程,已知一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:

散列过程如下图所示:

二次探测

二次探测法的探查序列是:h(k,i) =(h'(k)+i*i)%m ,0≤i≤m-1 。初次的探测位置为T[h'(k)],后序的探测位置在次基础上加一个偏移量,该偏移量以二次的方式依赖于i。该方法的缺陷是不易探查到整个散列空间。

双重散列

该方法是开放寻址的最好方法之一,因为其产生的排列具有随机选择的排列的许多特性。采用的散列函数为:h(k,i)=(h1(k)+ih2(k)) mod m。其中h1和h2为辅助散列函数。初始探测位置为T[h1(k)],后续的探测位置在此基础上加上偏移量h2(k)模m。

链接法

将所有关键字为同义词的结点链接在同一个链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。

举例说明链接法的执行过程,设有一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:

最终结果如下图所示:

 

常见操作

遍历

其他 

HashMap

数据结构

哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。当链表长度超过 8 时,链表转换为红黑树。

工作原理

HashMap 底层是 hash 数组和单向链表实现,数组中的每个元素都是链表,由 Node 内部类(实现 Map.Entry接口)实现,HashMap 通过 put & get 方法存储和获取。

存储对象时,将 K/V 键值传给 put() 方法:

调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;

调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容resize 为 2n);

hash值判断存入

  1. 如果 K 的 hash 值在 HashMap 中不存在,则执行插入,若存在,则发生碰撞;
  2. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 true,则更新键值对;
  3. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。

JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法)(注意:当碰撞导致链表大于 TREEIFY_THRESHOLD = 8 时,就把链表转换成红黑树)

获取对象时,将 K 传给 get() 方法:

  1. 调用 hash(K) 方法(计算 K 的 hash 值)从而获取该键值所在链表的数组下标;
  2. 顺序遍历链表,equals()方法查找相同 Node 链表中 K 值对应的 V 值。

hashCode 是定位的,存储位置;equals是定性的,比较两者是否相等。)

当两个对象的 hashCode 相同会发生什么

因为 hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。

你知道 hash 的实现吗?为什么要这样实现

JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16);

主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。

为什么要用异或运算符

保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。

HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题?

table 数组大小是由 capacity 这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30;

loadFactor 是装载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值是0.75,比如table 数组大小为 16,装载因子为 0.75 时,threshold 就是12,当 table 的实际大小超过 12 时,table就需要动态扩容;

扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold)

如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。

HashMap中put方法的过程

调用哈希函数获取Key对应的hash值,再计算其数组下标;

如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;

如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;

如果结点的key已经存在,则替换其value即可;

如果集合中的键值对大于12,调用resize方法进行数组扩容。

数组扩容的过程

创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小。

拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。

而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

说说你对红黑树的见解

每个节点非红即黑

根节点总是黑色的

如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

每个叶子节点都是黑色的空节点(NIL节点)

从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

jdk8中对HashMap做了哪些改变

在java 1.8中,如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容)

发生hash碰撞时,java 1.7 会在链表的头部插入,而java 1.8会在链表的尾部插入

在java 1.8中,Entry被Node替代(换了一个马甲。

HashMap,LinkedHashMap,TreeMap 有什么区别

LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;

TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)

HashMap & TreeMap & LinkedHashMap 使用场景

一般情况下,使用最多的是 HashMap。

HashMap:在 Map 中插入、删除和定位元素时;

TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;

LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。

HashMap 和 HashTable 有什么区别

HashMap 是线程不安全的,HashTable 是线程安全的;

由于线程安全,所以 HashTable 的效率比不上 HashMap;

HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;

HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;

HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同

ConcurrentHashMap 类(是 Java并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。

HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);

而针对 ConcurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized。

HashMap & ConcurrentHashMap 的区别

除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有null,但是ConCurrentHashMap 都不允许。

常用操作

遍历

其他

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱强拆的鲁班七号

码字不易,梦想路上与君共勉

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值