可以扫描下面二维码访问我的小程序来打开,随时随地通过微信访问。
1.Collection和Collections 的区别?
(1)Collection是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。List,Set,Queue接口都继承Collection。
(2)Collections是一个包装类。它包含有各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class TestCollections {
public static void main(String[] args) {
ArrayList nums = new ArrayList();
nums.add(100);
nums.add(-80);
nums.add(25);
nums.add(91);
nums.add(-1);
System.out.println("集合是否为空:" + nums.isEmpty());
System.out.println("默认顺序:" + nums);
Collections.reverse(nums);
System.out.println("反转后顺序:" + nums);
Collections.sort(nums);
System.out.println("排序后顺序:" + nums);
Collections.shuffle(nums);
System.out.println("混淆后顺序:" + nums);
// 下面只是为了演示定制排序的用法,将int类型转成string进行比较
Collections.sort(nums, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
String s1 = String.valueOf(o1);
String s2 = String.valueOf(o2);
return s1.compareTo(s2);
}
});
System.out.println("指定排序后顺序:" + nums);
System.out.println("最大的值是:" + Collections.max(nums));
System.out.println("最小的值是:" + Collections.min(nums));
}
}
//输出
集合是否为空:false
默认顺序:[100, -80, 25, 91, -1]
反转后顺序:[-1, 91, 25, -80, 100]
排序后顺序:[-80, -1, 25, 91, 100]
混淆后顺序:[91, 100, -80, 25, -1]
指定排序后顺序:[-1, -80, 100, 25, 91]
最大的值是:100
最小的值是:-80
2.修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法?
(1)这个在提问HashMap存储的原理,存储时先要调用hashCode方法生成新节点的hashCode,之后再次存储的时候,先通过hashCode得到新的存储内容hashCode,在链表中循环查找比较hashCode与其相同的,如果找到hashCode相同的节点,则再次比较key值,key值如果相同,替换为新的节点,如果key值不同,再次比较eqauls方法(这个时候需要通过重写的equals比较,如果相同就认为时同一个值),将新节点替换到该节点位置,如果没有相同的则在链尾端增加该节点。
(2)总结来说,HashMap存入时的比较就是hashCode相同的情况下,==相同或者equals相同就认为是相同的key值存入,==相同就不用通过equals比较了。
(3)所以上面的答案如果对象A是作为key存入,则先比较重写的hashCode方法返回的值,如果相同在通过==或者equals比较对象A是否相同,如果==相同,则就不在调用equals了。
具体来看看我写的测试
import lombok.Builder;
import java.util.HashMap;
import java.util.Objects;
public class EqualsTest {
public static void main(String[] args) {
HashMap map = new HashMap();
Demo a = Demo.builder().a("aaa").build();
Demo b = Demo.builder().a("bbb").build();
Demo c = Demo.builder().a("bbb").build();
map.put(a, "a");
System.out.println("注意两个b开始");
//这个时候存入了两个key为b的相同值,没有进入equals方法
map.put(b, "b");
map.put(b, "b");
System.out.println("注意两个b结束,没调用equals吧");
System.out.println("见证奇迹的c开始");
//这个时候存入了key为c的值,底层先比较key的hashCode相同
// 然后再次比较b和c是否是一个值,发现不一样
// 最后比较equals再次确认是否相同,这就进入了equals方法
map.put(c, "b");
System.out.println("见证奇迹的c,调用了equals");
System.out.println("见证奇迹的c结束");
map.put("d", b);
}
@Builder
static class Demo{
public String a;
public String b;
@Override
public boolean equals(Object o) {
System.out.println("Override equals");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Demo demo = (Demo) o;
return Objects.equals(a, demo.a);
}
@Override
public int hashCode() {
System.out.println("Override hashCode");
return Objects.hashCode(a);
}
}
}
//输出
Override hashCode
注意两个b开始
Override hashCode
Override hashCode
注意两个b结束,没调用equals吧
见证奇迹的c开始
Override hashCode
Override equals
见证奇迹的c,调用了equals
见证奇迹的c结束
3.List,Set,Map的区别
(1)List和Set都是Collection的子类,所以这两个都实现了iterator接口(Collection),都可以使用迭代器遍历集合中的内容,而Map未继承Collection类。
(2)List可以存入重复的实体
(3)Set存入重复的实体则不会存入,是否重复如果是HashSet会针对hashCode方法的重写的比较是否相同,如果是TreeSet则会比较compareTo方法的重写比较大小排序
(4)Map是键值对形式,key,value。
4.List和Map的实现方式以及存储方式
(1)List中常用实现方式有ArrayList和LinkedList,ArrayList的存储方式是数组,查询快;LinkedList的存储方式是链表,插入、删除快。
(2)Set常用实现方式有HashSet和TreeSet,HashSet的存储方式是hashCode算法存储的数组,加入的对象需要实现hashCode方法,快速查找元素;TreeSet的存储方式是按顺序序存储的二叉树,需要实现Comparable接口来比较大小来排序
(3)Map常用实现方式有HashMap和TreeMap,HashMap的存储方式是通过hashCode算法存储的数组,当存入的数据达到一定程度会转换为红黑树(目前不了解,等我后边研究的),快速查找键值;TreeMap存储方式是对key值按顺序存储的红黑树,类似于TreeSet同样要实现Comparable接口来比较大小来排。
5.LinkedList实现原理
(1)通过双向链表实现的,内部存储了链表头/链表尾的节点以及存储节点的个数
(2)get(int index)判断index是在前半部分还是后半部分,前半部分从链表头开始遍历直到index位置,后半部分则从链表尾向前开始遍历直到index。
(3)set(int index, E element)同样是先查找到index的节点,替换掉
(4)add(int index, E element)同样是先查找到index的节点,在这个节点后边添加一个节点,如果原位置后边有节点,则将原位置后边的节点添加到新添加的节点的后边。
(5)remove其实一样先index节点,然后将他的前一个节点的指向当前节点的位置设置为null。
6.HashMap的实现原理
(1)HashMap创建完毕默认是数组+链表的形式,具体如下图,根据key的不同的hash计算分配到数组不同的位置,如果已经有值则在链表的后边挂上一个新的节点,把value存储到该位置上。
(2)由于是通过hash计算,这样就会出现基本随机打散到数组的各个位置,一般来说应该平均,但是极端情况下可能都挂在数组的同一个位置下。
(3)内部节点实现是Node,分别存储了key,hash值,value,以及下一个Node,具体代码:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
(3)向HashMap中存储内容时,实际上先针对key值hash,得到具体的位置,然后到数据位置看是否已经存放了Node,通过key,value,hash值等创建一个Node,如果该位置还没有Node,则直接将新建的Node放到该数组位置;如果已经有了Node,也先通过key,value,hash值等创建一个Node,则向下查找,直到最后一个Node的next为null,将这个next为null的Node的next设置为新建的Node(说白了就是新建的Node要放在链表的尾部)。
(4)通过Key在hashMap中查找时,先把key值hash,找到具体位置,然后再链表上循环查找key值相同的(这个时候注意不是hash值,而是key值),找到后,返回Node的value。
(5)当存入的值过多时候,超过默认容量是,会再次重新调整位置,创建一个大小为原来数据两倍大小的数据,然后循环原来数组的每个链表,将每个节点分配到新数组的新链表中去。
(6)jdk1.8有个改动,为了查找效率更高,当数组中的某个链表中节点超过8个时,会转换为红黑树。红黑树我后边会详细讲,主要是现在我也没明白。
7.ConcurrentHashMap的实现原理
(1)由于HashMap不是线程安全,主要是由于如果并发访问会造成扩容过程中的链表出现循环链,所以提出了线程安全的ConcurrentHashMap。
(2)在JDK1.7中,使用的是分段锁Segment,每一个segment都可以想象成是一个独立HashMap,这样每个线程请求过来,只能在自己独立的分段操作,互不影响,实现简单,但是这种耗费空间比较大。
(3)在JDK1.8中,放弃了分段锁,采用的是使用Synchronized和CAS来操作,CAS就是compare and swap的缩写,中文翻译成比较并交换。CAS具体步骤:首先从内存位置A取出值B,然后机系列计算后,得到了值C,如果这个时候内存位置的值仍然是B,则修改为C,如果在准备修改前已经变味了D,则修改失败,重新从内存位置A取出新值D,重新上述步骤,直到成功。说完了CAS,再来具体说ConcurrentHashMap如何通过CAS来修改:ConcurrentHashMap仍然是顶层是数组的模式,只是每次来修改,通过hash计算出要修改的数组位置后,将这个数组位置加锁,然后到这个数据的链表上修改,仍然是循环查找到next为null的,把新增的节点设置为next,如果这个链表上的个数超过8个,注意了,超过8个只是扩容(妈蛋,网上帖子都是8个就扩容,骗人的玩意儿),不是转变为红黑树,只有当超过64个时候才转换为红黑树,修改完毕后,释放锁。
8.HashTable实现原理
(1)Hashtable的默认容量为11,默认负载因子为0.75。
(2)不允许值和键为空(null),若为空,会抛空指针。
(3)基本方法都有synchronized关键字,线程安全,其实从个人理解,这个就是HashMap的线程安全版本,但是效率并不高。
(4)每次扩容,新容量为旧容量的2倍加1,而HashMap为旧容量的2倍。Hashtable默认容量为11,HashMap默认容量为16。
(5)根据hashcode计算索引时将hashcode值先与上0x7FFFFFFF,这是为了保证hash值始终为正数
9.HashMap和HashTable的区别
(1)父类不同,HashMap继承自AbstractMap类,而HashTable是继承自Dictionary类。
(2)Hashtable不允许值和键为空。HashMap中key和value可以为null。
(3)Hashtable基本方法都有synchronized关键字,线程安全;而HashMap没有,线程不安全。
(4)Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
(5)hash方法不同,Hashtable直接使用hashCode,然后与上0x7FFFFFFF,保持hash返回的值为正数,最后与数组长度取余;HashMap的key不为空时取hashCode再与 (hashCode >>> 16)异或得到位置。hashCode >>> 16实际上是hashCode无符号右移16位,就是把hashCode的二进制数字的前16位都干掉,保留剩下的得到的值。比如说十进制数字78897121,转换为二进制数字为0000 0100 1011 0011 1101 1111 1110 0001,>>> 16计算后得到了剩下的高16位,0000 0100 1011 0011,转换为十进制为1203。
//HashTable计算位置
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//HashMap.hash(key)计算位置
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
10.HashMap与HashSet的区别
(1)HashSet的底层实现就是HashMap
(2)HashSet实现了Set接口,所以大家都懂,不允许重复的对象存储,只不过是通过HashMap作为存储实现的Set接口。HashSet是存储的对象(add(E e)),而HashMap是key,value的形式(put(K key, V value))。
(3)效率上肯定是HashMap更快,因为HashSet存储得在HashMap的基础上在再存储。
11.集合Set实现Hash怎么防止碰撞
(1)这个实际上问的,HashSet在存入值时候的操作。
(2)HashSet存入的时候,先获取存入值的hashCode,如果HashSet中没有这个hashCode,则认为是新值,直接存入。
(3)如果hashCode相同,这个时候先通过==或者euqals判断,存入的值是否相同,==或者equals有任何一个相同,则认为是相同的值,就返回false。
具体可以看下这一行源码:
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
12.数组和链表的区别
(1)数组在内存中连续;链表采用动态内存分配的方式,在内存中不连续,每个节点存储了下个节点的位置
(2)数组长度固定,不支持动态改变数组大小(只能通过复制到新的扩容后的数组中);链表支持动态增加或者删除元素,直接在链上增加或者删除即可
(3)数组在内存中顺序存储,可通过下标访问,访问效率高;链表访问效率低,如果想要访问某个元素,需要从头遍历,查找效率慢
(4)数组可能越界,链表是逐个向后查找,不存在越界
(5)数组适合查找次数多,增加修改次数少的情况;链表适合增加修改次数多,查找次数少的情况。
13.Array和ArrayList有何区别?什么时候更适合用Array
(1)Array是数组;ArrayList是列表
(2)Array声明时候定于长度,且长度不可变;ArrayList不必声明长度,后期达到一定程度,会自动扩容。
(3)Array声明时候定义类型,如下面的String[];ArrayList如果不适用泛型,则可以存入多种类型。
String[] b = new String[10];
b[0] = "0";
(4)知道初始确定的长度且长度不变的时候就可以用Array;不确定长度,而且经常变化可以用ArrayList
14.EnumSet是什么?
(1)EnumSet是一个针对Enum枚举类的集合,抽象类,不能通过new来创建
(2)底层有两个实现,集合中个数超过64个为JumboEnumSet,小于64个为RegularEnumSet
(3)RegularEnumSet通过long作为存储,实际是按照位去存储的,这样非常节省空间,比如我下面那个代码中的实际上就4个内容,存储时候通过二进制的1111(转换为十进制就是15存储到long中),就把全部四个内容存储进入了,如果只有三个内容,TEST_0,TEST_1,TEST_3,那么就存储为1101(转换为十进制就是13存储到long中),即可。
(4)JumboEnumSet超过64位按照long存储费劲了,因此采取了long[]数组来存储,分成64位一组,这样超过64的时候分成多组,比如110个,那就么就是long[0]是满的存储-1,long[1]就是110个Enum-64个Enum=46个Enum,通过位数实际写成46个1,高位补齐0,转换为十进制,就是long[1]=70368744177663。
(5)有一些方法,allOf,noneOf等的操作,具体如下:
import java.util.EnumSet;
public enum TestEnum {
TEST_0(0),
TEST_1(1),
TEST_2(2),
TEST_3(3);
private final int value;
private TestEnum(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
public static void main(String[] args) {
System.out.println("EnumSet.allOf");
EnumSet<TestEnum> set = EnumSet.allOf(TestEnum.class);
set.forEach(s-> System.out.println(s));
System.out.println("======");
System.out.println("EnumSet.noneOf");
EnumSet<TestEnum> enumSetNone = EnumSet.noneOf(TestEnum.class);
enumSetNone.forEach(s-> System.out.println(s));
System.out.println("======");
System.out.println("EnumSet.add");
enumSetNone.add(TestEnum.TEST_2);
enumSetNone.forEach(s-> System.out.println(s));
System.out.println("======");
System.out.println("EnumSet.range TEST_0 to TEST_2");
EnumSet<TestEnum> enumSetRange = EnumSet.range(TestEnum.TEST_0, TestEnum.TEST_2);
enumSetRange.forEach(s-> System.out.println(s));
System.out.println("======");
System.out.println("EnumSet.of");
EnumSet<TestEnum> enumSOf = EnumSet.of(TestEnum.TEST_0, TestEnum.TEST_2);
enumSOf.forEach(s-> System.out.println(s));
}
}
//输出
EnumSet.allOf
TEST_0
TEST_1
TEST_2
TEST_3
======
EnumSet.noneOf
======
EnumSet.add
TEST_2
======
EnumSet.range TEST_0 to TEST_2
TEST_0
TEST_1
TEST_2
======
EnumSet.of
TEST_0
TEST_2
15.Comparable和Comparator接口有何区别?
(1)Comparable接口通过实现类实现compareTo方法来实现实体的比较大小。这样就可以直接使用Collections.sort就可以对实体的List进行排序了,排序的结果是按照compareTo方法比较大小的降序。
(2)Comparator接口是在Collections.sort中直接使用的,可以针对那些没有实现Comparable的实体,或者不按照compareTo去比较排序的情况。
public static void main(String[] args) {
List<TestComparator> list = new ArrayList<TestComparator>();
list.add(TestComparator.builder().a("a").b(1).build());
list.add(TestComparator.builder().a("a1").b(2).build());
Collections.sort(list,new Comparator<TestComparator>(){
@Override
public int compare(TestComparator o1, TestComparator o2) {
return o2.getB() - o1.getB();
}
});
list.forEach(System.out::println);
}
@Data
@Builder
static class TestComparator{
String a;
int b;
}
//输出
TestEnum.TestComparator(a=a1, b=2)
TestEnum.TestComparator(a=a, b=1)
16.Java集合的快速失败机制 “fail-fast”?
(1)fail-fast是一种错误机制,实际上是针对集合类使用过程中,如果被修改、删除其中的内容后触发的一种错误机制。
(2)具体可以看下面这个例子,就是当操作了remove后,ArrayList中有一个modCount,这个值每次增加修改删除都会增加数值,而创建iterator时候,会将ArrayList的modCount赋值给iterator的expectedModCount,每次操作iterator(hasNext不会判断)都会判断这两个值是否相同,不同则直接抛出ConcurrentModificationException
import java.util.ArrayList;
import java.util.Iterator;
public class ArrayListTest {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(10);
arrayList.add(11);
arrayList.add(12);
Iterator<Integer> iterator = arrayList.iterator();
Integer last = 0;
while (iterator.hasNext()) {
Integer next = iterator.next();
System.out.println(next);
// 神奇的现象,改成11就不会报错,为什么呢?
// 因为remove后,上面的iterator.hasNext()就是false了
if (next == 12) {
arrayList.remove(next);
}
}
}
}
//输出
10
11
12
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at cn.homecredit.miniapp.bff.application.command.ArrayListTest.main(ArrayListTest.java:15)
(3)可以再看下抛出ConcurrentModificationException的代码,如果操作过程中被修改了就会抛出异常中断操作。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
17.fail-fast 与 fail-safe 之间的区别?
(1)fail-fast是上面20条讲的一种错误机制,当iterator循环集合过程中,被修改了就会抛出错误。
(2)fail-safe是任何对集合结构的修改都会在一个复制的集合上进行修改,不会抛出ConcurrentModificationException,concurrent包下的都是fail-safe机制的
(3)大家来看一下CopyOnWriteArrayList的测试情况,不会抛出异常
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class ArrayListTest {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> arrayList = new CopyOnWriteArrayList<>();
arrayList.add(10);
arrayList.add(11);
arrayList.add(12);
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
System.out.println(next);
if (next == 10) {
arrayList.remove(next);
}
}
}
}
//输出
10
11
12
(3)看一下CopyOnWriteArrayList.add的源码,加锁,先copy出来一个,再到copy的上面操作,解锁,有一个重点,setArray方法把旧的数组废弃掉
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
(4)CopyOnWriteArrayList.remove的源码,更有意思,我只看重点部分了,加锁,创建一个size-1的新数组,从需要remove的节点的前后分别copy到新数组上,解锁
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
18.Iterator类有什么作用
(1)对集合进行迭代的迭代器,将内容从集合中循环读取出来
(2)有人说iterator效率比较高,比foreach效率高咱们来试试,我的结论不是这么回事。。。
import java.util.ArrayList;
import java.util.Iterator;
public class ArrayListTest {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList();
for (int i = 0; i < 10000000; i++) {
arrayList.add(i);
}
Long start = System.currentTimeMillis();
testForEach(arrayList);
Long end = System.currentTimeMillis();
System.out.println((end-start) + " ms");
Long newstart = System.currentTimeMillis();
testIterator(arrayList);
Long newend = System.currentTimeMillis();
System.out.println((newend-newstart) + " ms");
}
public static void testIterator(ArrayList<Integer> arrayList){
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
}
}
public static void testForEach(ArrayList<Integer> arrayList){
for (Integer i:arrayList) {
}
}
}
//输出
38 ms
35 ms
//如果修改testForEach和testIterator的执行顺序再来看看输出结果,谁先执行谁慢3-5ms,我的结论是量效率差不多
40 ms
35 ms
19.Queue中的poll()方法和remove()方法区别?
(1)poll和remove都是取出头元素,poll如果没有取到值会返回null,但是remove这个时候会报异常
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class QueueTest {
public static void main(String[] args) {
BlockingQueue<String> bq = new ArrayBlockingQueue<String>(10);
bq.add("111");
bq.add("222");
//分别取出111,222
System.out.println(bq.remove());
System.out.println(bq.poll());
//元素取出来了,里面没有了,可以看看poll和remove的返回
System.out.println(bq.poll());
System.out.println(bq.remove());
}
}
//输出
111
222
null
Exception in thread "main" java.util.NoSuchElementException
at java.util.AbstractQueue.remove(AbstractQueue.java:117)
at cn.homecredit.miniapp.bff.application.command.ArrayListTest.main(ArrayListTest.java:16)
(2)其实还可以引申peek()和element()的区别,peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,调用element()方法会抛出NoSuchElementException异常。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class QueueTest {
public static void main(String[] args) {
BlockingQueue<String> bq = new ArrayBlockingQueue<String>(10);
bq.add("111");
bq.add("222");
//一直取出的都是111,因为没有移除队头元素
System.out.println(bq.element());
System.out.println(bq.peek());
//取出元素
System.out.println(bq.poll());
System.out.println(bq.poll());
//元素取出来了,里面没有了,可以看看peek和element的返回
System.out.println(bq.peek());
System.out.println(bq.element());
}
}
//输出
111
111
111
222
null
Exception in thread "main" java.util.NoSuchElementException
at java.util.AbstractQueue.element(AbstractQueue.java:136)
at cn.homecredit.miniapp.bff.application.command.QueueTest.main(QueueTest.java:19)
20.JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。
(1)浪费内存空间,每个线程都使用独立的分段的Map
(2)实际使用过程中,发生冲突抢占分段锁的情况不多,另外加了分段锁导致了存入值的操作效率变低
(3)让我来设计?那就按照1.8的cas来呗。。。
(4)每次来修改,通过hash计算出要修改的数组位置后,将这个数组位置加锁,然后到这个数据的链表上修改,仍然是循环查找到next为null的,把新增的节点设置为next,如果这个链表上的个数超过8个,注意了,超过8个只是扩容(妈蛋,网上帖子都是8个就扩容,骗人的玩意儿),不是转变为红黑树,只有当超过64个时候才转换为红黑树,修改完毕后,释放锁。
============2022.8.16新增=========
21. 常用的集合有哪些?
(1)List集合:ArrayList和LinkedList,他们的区别是。
ArrayList是基于索引的数据接口,底层是动态数组,查询快,增删改的速度较慢;
LinkedList是以元素列表的形式存储它的数据,底层是列表,查询慢增删快;
LinkedList比ArrayList更占用内存因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
(2)Set集合:HashSet、TreeSet、LinkedHashSet。
一般来说,如果我们需要保证集合元素是唯一的,就用set集合;
一般我在工作中常用的就是HashSet;
(3)Map集合:HashMap、LinkedHashMap、TreeMap、HashTable。
HashMap是最常用的Map,他根据键的HashCode值存储数据,根据键可以直接获取他的值,具有很快的访问速度;
遍历时,取到的数据顺序是随机的;
HashMap最多只允许一条记录的键为Null,而可以允许多个记录的值为null;
HashMap不支持线程同步,是非线程安全的;
HashTable与HashMap类似,他不允许记录的键或者值为空,支持线程同步,所以在写入时较慢;
如果考虑线程安全的问题时用ConcurrentHashMap,我们很多时候把ConcurrentHashMap用于本地缓存。
22. 常见的并发集合有哪些
(1)ConcurrentHashMap,线程安全的hashmap的实现
(2)CopyOnWriteArrayList,线程安全且在读操作时无锁的ArrayList
(3)CopyOnWriteArraySet,基于CopyOnWriteArrayList,不添加重复元素
(4)ArrayBlockingQueue,基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
(5)LinkedBlockingQueue,基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue
23. 集合使用泛型有什么优点?
(1)Java1.5引⼊了泛型,所有的集合接⼝和实现都⼤量地使⽤它。
(2)泛型允许我们为集合提供⼀个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。
(3)这避免了在运⾏时出现ClassCastException,因为你将会在编译时得到报错信息。
(4)泛型也使得代码整洁,我们不需要使⽤显式转换和instanceOf操作符。
(5)它也给运⾏时带来好处,因为不会产⽣类型检查的字节码指令。
24. 为何Map接口不继承Collection接口?
(1)尽管 Map 接口和Collection的实现也是集合框架的一部分,但 Map 不是集合。
(2)Map 包含 key-value 对,它提供抽取 key 或 value 列表集合的方法,但是它不适合 “ 一组对象 ” 规范,因此 Map 无法继承 Collection 接口。
25. HashMap是如何解决hash冲突的?
(1)Hash冲突是由于哈希算法,被计算的数据是无限的,而计算后的结果的范围是有限的,总会存在不同的数据,经过计算之后得到值是一样,那么这个情况下就会出现所谓的哈希冲突。
(2)解决Hash冲突有四种方案:开放定址法,链式寻址法,再Hash法,建立公共移除区
(3)开放定址法也称线性探测法,就是从发生冲突的那个位置开始,按照一定次序从Hash表找到一个空闲位置然后把发生冲突的元素存入到这个位置,而在java中,ThreadLocal就用到了线性探测法来解决Hash冲突。这种会存在问题,当冲突很多的时候,会导致后续的hash算法找到的位置均不准确,都需要向后查找。
(4)链式寻址法,这是一种常见的方法,简单理解就是把存在Hash冲突的key,以单向链表来进行存储,存在冲突就向该位置的链表向下增加一个节点,比如HashMap,具体如下图:
(5)再Hash法,就是通过某个Hash函数计算的key,存在冲突的时候,再用另外一个Hash函数对这个可以进行Hash,一直运算,直到不再产生冲突为止,这种方式会增加计算的一个时间,性能上呢会有一些影响
(6)建立公共溢出区,就是把Hash表分为基本表和溢出表两个部分,凡是存在冲突的元素,一律放到溢出表中
26. 什么是hash冲突?
Hash冲突是由于哈希算法,被计算的数据是无限的,而计算后的结果的范围是有限的,总会存在不同的数据,经过计算之后得到值是一样,那么这个情况下就会出现所谓的哈希冲突。
27. HashMap是如何扩容的?
(1)对数组大小进行扩容。那么HashMap在什么情况下会触发扩容呢?HashMap中定义了一个扩容的阈值,该阈值的计算公式为 : threshold = capacity loadFactor,其中capacity为数组的长度,默认是16。loadFactor为负载因子,默认为0.75。threshold为该阈值,默认是16 0.75 = 12。达到的效果就是HashMap中元素数量超过了阈值,会进行扩容操作,扩容后的数组大小为原来的2倍。这个步骤上,1.7和1.8没有区别。
(2)重新存放数据位置。因为数组大小有变化,所以数据的下标需要重新计算,将数组放到新计算的下标位置。在JDK1.7中,链表数据的添加采用的是头插法,这是一种类似于栈的先进后出特点的添加方式,所以在扩容时,有点类似于数据从一个栈转移到另一个栈,转移后的顺序会反转过来(这里用栈来举例是为了方便理解,链表和栈是不同的数据结构)。这里在多个线程同时扩容时,会出现环形链表问题。具体如下1.7的源码
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//因为数组大小有变化,所以需要重新计算存放下标位置
int i = indexFor(e.hash, newCapacity);
//这里就是顺序反转的核心算法啦,是不是有点像某个面试题,给数组中的两个元素调换位置?
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
在JDK1.8中,链表数据添加采用的是尾插法。所以并不存在顺序反转,相应的也不会出现环形链表问题。具体如下1.8的源码
final Node<K,V>[] resize() {
...
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
//没有hash冲突,即链表只有一个元素,直接计算下标位置存放即可。
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//数据的结构为红黑树,增加了链表转红黑树的操作。
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//数据结构为链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/* hash & 旧数组大小
* 这里以原数组大小为16举例,那么新数组的大小就是32
* 原数组下标计算用的是15(0000...1111)新数组下标计算用的是31(0000...11111)
* 不难发现,影响下标计算结果的其实就只有一个最高位的1
* 所以这里我们用hash先与16(0000...10000)进行与运算,目的就是提前区分开hash值在最高位的不同,根据在最高位是1还是0,将原链表拆分成两个不同的链表。
*/
if ((e.hash & oldCap) == 0) {
//尾插法
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//尾插法
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//在新数组下标中最高位的值如果为0,直接存放到原下标位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//在新数组下标中最高位的值如果为1,存放下标位置为:原下标 + 旧数组大小
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
=========2022.9.17
28. ArrayList和Vector的区别
(1)这两个类都实现了List接口(List接口继承了Collection接口)。
(2)他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,并且其中的数据是允许重复的,这是与HashSet之类的集合的最大不同处,HashSet之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。
(3)同步性,Vector是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList是线程序不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
(4)数据增长,ArrayList与Vector都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList与Vector的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector默认增长为原来两倍,而ArrayList的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍)。ArrayList与Vector都可以设置初始的空间大小,Vector还可以设置增长的空间大小,而ArrayList没有提供设置增长空间的方法。
(5)总结上面的第四条来说,Vector增长原来的一倍,ArrayList增加原来的0.5倍。
29. List、Map、Set三个接口,存取元素时,各有什么特点?
(1)List与Set具有相似性,它们都是单列元素的集合,它们有一个共同的父接口Collection。Set里面不允许有重复的元素,即不能有两个相等(注意,不是仅仅是相同)的对象,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去,所以,Set集合的add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true,当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。Set取元素时,不能细说要取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。
(2)List表示有先后顺序的集合,注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obje)方法时,每次加入的对象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用add(intindex,Obj e)方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List中,每调用一次add方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个索引指向了这个对象,如图x所示。List除了可以用Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以调用get(index i)来明确说明取第几个。
(3)Map与List和Set不同,它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。取则可以根据key获得相应的value,即get(Object key)返回值为key所对应的value。另外,也可以获得所有的key的结合,还可以获得所有的value的结合,还可以获得key和value组合成的Map.Entry对象的集合。
(4)List以特定次序来持有元素,可有重复元素。Set无法拥有重复元素,内部排序。Map保存key-value值,value可多值。
30.说说ArrayList,Vector,LinkedList的存储性能和特性
(1)ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差。而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,索引就变慢了,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。
(2)LinkedList也是线程不安全的,LinkedList提供了一些方法,使得LinkedList可以被当作堆栈和队列来使用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。