# 技术栈知识点巩固——Java集合
Posted MarlonBrando1998
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了# 技术栈知识点巩固——Java集合相关的知识,希望对你有一定的参考价值。
技术栈知识点巩固——Java集合
Arraylist与LinkedList区别
-
ArrayList
是实现了基于动态数组的数据结构,而LinkedList
是基于链表的数据结构; -
对于随机访问
get
和set
,ArrayList
要优于LinkedList
,因为LinkedList
要移动指针;
Collections.sort和Arrays.sort的实现原理
Collections.sort
:底层调用的还是Arrays.sort
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
Arrays.sort
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
- 底层实现都是
TimSort
实现的,这是jdk1.7新增的,以前是归并排序。TimSort算法就是找到已经排好序数据的子序列,然后对剩余部分排序,然后合并起来
HashMap原理,java8做了什么改变
-
key value
键值对存储方式,数组、链表、红黑树。 -
HashMap
使用哈希表进行存储,哈希表为解决冲突,采用开放地址法和链地址法等来解决问题。 -
使用高位运算、取模运算计算
key
的hashcode
值。 -
先通过
hash
方法找到数组中的位置,然后再次hash
得到在链表中的位置。 -
当链表长度超过阈值8时,会将链表转换为红黑树,使
HashMap
的性能得到进一步提升。 -
图片来自:https://www.lagou.com/lgeduarticle/18098.html
List 和 Set,Map 的区别
List
-
List
:元素可重复,元素有序 -
ArrayList()
: 长度可变,查询块,插入删除慢。 -
LinkedList()
: 在实现中采用链表数据结构,插入和删除速度快,访问速度慢。
Set
-
Set
:元素无重复,元素无序。 -
HashSet
:为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。 -
TreeSet
: 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。 -
LinkedHashSet
:具有HashSet
的查询速度,且内部使用链表维护元素的顺序在使用迭代器遍历Set时,结果会按元素插入的次序显示。
Map
Map
:键值对存储。HashMap
:Map
基于散列表的实现,插入和查询“键值对”的开销是固定的。可以通过构造器设置容量capacity
和负载因子load factor
,以调整容器的性能。LinkedHashMap
: 类似于HashMap
,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU
)的次序。只比HashMap
慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序。TreeMap
: 基于红黑树数据结构的实现。WeakHashMap
:弱键Map
,Map
中使用的对象也被允许释放: 这是为解决垃圾回收。
poll()方法和 remove()方法的区别?
poll
从队列头部拿出一个元素。poll()
在获取元素失败的时候会返回空,但是remove()
失败的时候会抛出异常。
HashMap,HashTable,ConcurrentHashMap的共同点和区别
HashMap
- 数组+链表+红黑树,存储
null
键和null
值,线程不安全。 - 初始
size
为16
,扩容:newsize = oldsize*2
。 - 每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入。
Map
中元素总数超过Entry
数组的75%,触发扩容操作。
HashTable
- 键与值成对存在,键是唯一的,不能重复。线程安全。
- 存储结构中使用了数组、单链表,其中单链表是用来处理哈希冲突时用的。
- HashTable处理冲突时采用链表,HashMap处理冲突采用链表+红黑树。
ConcurrentHashMap
- 分段的数组+链表实现,线程安全。
- 把整个Map分为
N
个Segment
,可以提供相同的线程安全。读操作不加锁,HashEntry
的value
变量是volatile
的,也能保证读取到最新的值。 - Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占。
- 段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容。
写一段代码在遍历 ArrayList 时移除一个元素
- 使用迭代器
@Test
public void test(){
List<String> list = new ArrayList<>();
list.add("张三");
list.add("张三");
list.add("李四");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
Object next = iterator.next();
if("李四".equals(next)){
iterator.remove();
}
}
logger.info(JSON.toJSONString(list));
}
- 使用
removeIf
@Test
public void testOne(){
List<String> list = new ArrayList<>();
list.add("张三");
list.add("张三");
list.add("李四");
list.removeIf("李四"::equals);
logger.info(JSON.toJSONString(list));
}
TreeMap底层
- TreeMap 的实现就是红黑树数据结构。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
//自定义的比较器:
private final Comparator<? super K> comparator;
//红黑树的根节点:
private transient Entry<K,V> root;
//集合元素数量:
private transient int size = 0;
//对TreeMap操作的数量:
private transient int modCount = 0;
//无参构造方法:comparator属性置为null
//代表使用key的自然顺序来维持TreeMap的顺序,这里要求key必须实现Comparable接口
public TreeMap() {
comparator = null;
}
//带有比较器的构造方法:初始化comparator属性;
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
//带有map的构造方法:
//同样比较器comparator为空,使用key的自然顺序排序
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
//带有SortedMap的构造方法:
//根据SortedMap的比较器来来维持TreeMap的顺序
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
}
HashMap 的扩容过程
HashMap
默认容量为16,加载因子,默认是0.75,阈值=容量*加载因子。默认12,当元素数量超过阈值时便会触发扩容。- 一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。
- 空参数的构造函数:实例化的
HashMap
默认内部数组是null
,没有实例化。第一次调用put
方法时,则会开始第一次初始化扩容,长度为16。 - 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数设置赋值给阈值。第一次调用
put
方法时,会将阈值赋值给容量。
HashSet是如何保证不重复的
- 使用
HashMap
的put
方法,根据键的hash
值判断,如果hash
只重复则不存,否则放入Map
。
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
LinkedHashMap的应用,底层,原理
Map
中的每个Entry
是有序的。- 通过双向链表维护节点的顺序。
- 例如下图所示的
key value
,图片来自:https://zhuanlan.zhihu.com/p/34490361
LinkedHashMap
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{}
哪些集合类是线程安全的?哪些不安全?
线程安全 | 线程不安全 |
---|---|
Vector :方法前面都加了synchronized 关键字 | HashMap |
HashTable :使用了synchronized 关键字 | Arraylist |
ConcurrentHashMap :使用锁分段技术确保线性安全 | LinkedList |
Stack :继承于Vector | HashSet |
TreeSet | |
TreeMap |
ArrayList 和 Vector 的区别是什么?
Vector
是线程安全的,ArrayList
不是线程安全的。ArrayList
在底层数组不够用时在原来的基础上扩展0.5倍,Vector
是扩展1倍。
Collection与Collections的区别是什么?
Collection
是一个接口,里面有各种集合的操作接口
interface Collection<E> extends Iterable<E> {
// ...
}
Collections
是一个工具类
如何决定使用 HashMap 还是TreeMap
HashMap
不支持排序,TreeMap
默认是按照key
值升序排序的可以指定排序的比较器。- HashMap
大多数情况下有更好的性能
,尤其是读数据。 HashMap
基于数组+链表实现,TreeMap
基于红黑树实现。
如何实现数组和 List之间的转换?
@Test
public void test3() {
int[] num = {1, 2, 3, 54, 4};
List<int[]> ints = Arrays.asList(num);
logger.info(JSON.toJSONString(ints));
List<String> list = new ArrayList<>();
list.add("张三");
list.add("张三");
list.add("李四");
String[] objects = list.toArray(new String[0]);
logger.info(JSON.toJSONString(objects));
}
迭代器 Iterator 是什么?怎么用,有什么特点?
-
Iterator
是遍历集合的工具,即我们通常通过Iterator迭代器来遍历集合 -
Collection
接口中有接口Iterator<E> iterator();
实现Collection
接口的类通过iterator
遍历集合中的元素。 -
Iterator
只能单向移动。 -
Iterator.remove()
是唯一安全的方式来在迭代过程中修改集合;
Iterator 和 ListIterator 有什么区别?
ListIterator
是Collection
框架中的一个接口;是用于扩展Iterator
接口的。使用ListIterator
,可以向前和向后遍历集合的元素。还可以添加、删除或修改集合中的任何元素。
ListIterator<String> iterator = list.listIterator();
怎么确保一个集合不能被修改?
- 使用
Collections
中的API
List<String> list = new ArrayList<>();
List<String> unmodifiableList = Collections.unmodifiableList(list);
- 使用Arrays.asList创建的集合
String[] str = {"one", "two"};
List<String> stringList = Arrays.asList(str);
logger.info(JSON.toJSONString(stringList));
快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
-
当多个线程对
Collection
进行操作时,若其中某一个线程通过iterator
去遍历集合时,该集合的内容被其他线程所改变;则会抛出ConcurrentModificationException
异常。 -
Iterator
的安全失败是基于对底层集合做拷贝,它不受源集合上修改的影响。 -
java.util
包下面的所有的集合类都是快速失败的,而java.util.concurrent
包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException
异常,而安全失败的迭代器永远不会抛出这样的异常。 -
可以使用``JUC`下面相关的类。
Java 中的 LinkedList是单向链表还是双向链表?
LinkedList
是基于双向链表而实现的。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
/**
* Constructs an empty list.
*/
public LinkedList() {
}
}
说一说ArrayList 的扩容机制吧
ArrayList
扩容发生在add()
方法调用的时候, 调用ensureCapacityInternal()
来扩容的,通过方法calculateCapacity(elementData, minCapacity)
获取需要扩容的长度:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
calculateCapacity
获取扩容长度
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
}
ensureExplicitCapacity
方法可以判断是否需要扩容:ArrayList
扩容的关键方法grow()
获取到ArrayList
中elementData
数组的内存空间长度 扩容至原来的1.5倍。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
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);
}
- 调用
Arrays.copyOf
方法将elementData
数组指向新的内存空间时newCapacity
的连续空间从此方法中我们可以清晰的看出其实ArrayList
扩容的本质就是计算出新的扩容数组的size
后实例化,并将原有数组内容复制到新数组中去。
ConcurrenHashMap 原理
ArrayList的默认大小
- 默认大小是10;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
}
我们如何对一组对象进行排序
Arrays.sort()
:数组排序。Collections.sort(list)
:集合排序。new TreeMap()
:定义比较器对Map
中的元素排序。- 对象实现
Comparable
接口,重写compareTo
方法;
public class Shop implements Comparable<Shop> {
private String shopName;
private String shopCode;
@Override
public int compareTo(Shop shop) {
int length = shop.getShopName().length();
return this.shopName.length() - length;
}
}
当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?
Collections.unmodifiableCollection(list)
创建的集合不可修改,修改时后会抛出异常。- 使用
Arrays.asList
创建的集合。
HashSet 的实现原理
- 使用
map
的put
操作,当key
的hash
值和equals
相等的时候就往里放了。 HashSet
中的元素都存放在HashMap
的key
上面,而value
中的值都是统一的一个private static final Object PRESENT = new Object()
,HashSet
跟HashMap
一样,都是一个存放链表的数组。
Array 和 ArrayList 有何区别
Array
的空间大小是固定的。ArrayList
的空间是动态增长的。Array
数组可以包含基本类型和对象类型。ArrayList
却只能包含对象类型。
红黑树的特点
- 每个节点或者是黑色,或者是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
HashSet和TreeSet区别
- 最大的区别就是里面元素
TreeSet
可以实现排序。
@Test
public void treeSetTest() {
TreeSet<String> set = new TreeSet<String>((Comparator) (o1, o2) -> {
int length1 = String.valueOf(o1).length();
int length2 = String.valueOf(o2).length();
if (length1 == length2) return 1;
return Integer.compare(length1, length2);
});
set.add("zhangsan");
set.add("test");
set.add("lisi");
set.add("1234");
set.add("wangwu");
logger.info(String.valueOf(set));
}
Set里的元素去重原理
-
add
里面是map
的put
操作,通过计算hash
值和内容是否相等进行add
操作。 -
Set
里的元素是不能重复的,元素重复与否是使用equals()
方法进行判断的。
equals()
和==
方法决定引用值是否指向同一对象 。hash
相等不一定内容相等,所以使用Equals
进行判断。