Java 集合

使用

数组转List

List<String> list = Arrays.stream(array).collect(Collectors.toList()); //推荐!

List<String> list = Arrays.asList(array);//不可变list,修改操作会报错

List转数组

String[] array = list.toArray(new String[0]); //推荐!

String[] array = list.stream().toArray(String[]::new);

List转Set

List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>(list); //利用构造函数 推荐!

Set<String> set = list.stream().collect(Collectors.toSet());

Set转List

Set<String> set = new HashSet<>();
List<String> list = new ArrayList<>(set);//利用构造函数 推荐!

List<String> list = set.stream().collect(Collectors.toList());

List转Map

List<Person> list = new ArrayList<>();
list.add(new Person("John", 30));
list.add(new Person("Jane", 25));
list.add(new Person("Tom", 35));

Map<String, Integer> map = list.stream()
    .collect(Collectors.toMap(Person::getName, Person::getAge));

List

快速失败、快速安全

使用增强for循环时修改集合元素会抛异常

主要原因是 foreach底层是迭代器,迭代器在调用next方法时,会比较两个值,一个是modCount、一个是expectModCount,如果不同就会抛异常,而调用集合的修改方法,只修改了modCont,没有修改expectModCount。因此要使用迭代器本身的修改方法。

COW是一种快速安全。

在遍历时需要修改

  1. 普通for
  2. 迭代器 用迭代器的修改方法
  3. COW(CopyOnWrite写时复制)
  4. 线程安全的CurruentHashMap
  5. stream流

排序

自然排序 Compareble实现compareTo

定制排序 Comparetor实现compare

stream流的sort,底层基于compareble

判断一致?compareTo返回值为0

ArrayList

ArrayList的扩容机制?

ArrayList默认的初始容量是10,当准备添加元素时,会检查当前元素是否满了,如果满了,会创建一个1.5倍容量的数组,然后把原数组复制到新数组中。

ArrayList插入和删除元素的时间复杂度?

插入:头部插入,所有元素需要后移,时间复杂度On;尾部插入,时间复杂度O1;指定位置插入,平均需要后移n/2个元素,时间复杂度On。

删除:头部删除,所有元素需要前移,时间复杂度On;尾部删除,时间复杂度O1;指定位置删除,平均需要前移n/2个元素,时间复杂度On。

ArrayList和数组的区别?

ArrayList底层是基于数组实现的,支持插入删除等操作,并且可以动态扩缩容,比静态数组更加灵活。

ArrayList可以添加null值吗?

可以,ArrayList通常情况下可以添加任意引用类型对象,包括null,但是存null没有任何意义,并且可能导致空指针异常。

ArrayList指定了泛型为Integer,那么可以添加String吗?

可以通过反射添加,泛型工作在编译阶段,到了运行阶段就失效了,而反射工作在运行时阶段。

通过获取List的Class对象,进一步获取add方法的Method对象,然后调用method对象的invoke方法,传递String。

LinkedList

LinkedList插入和删除元素的时间复杂度?

因为LinkedList底层基于双向链表,维护了头尾指针的地址,因此头部或尾部的插入和删除的时间复杂度都是O1,只需要修改头尾节点的指针。

指定位置插入或删除,需要先遍历,寻找到该节点,才能移动指针,遍历的时间复杂度为On。

对比

ArrayList和LinkedList的对比?

前者基于数组,后者基于双向链表;

前者向尾部插入元素时间复杂度是O1,其他地方插入元素时间复杂度是On;后者向头部或尾部插入元素时间复杂度是O1,其他地方插入元素时间复杂度是On。因此除非需要大量头插的操作,否则选择ArrayList就行了,并且ArrayList随机访问数据的时间复杂度是O1。

CopyOnWriteArrayList

ArrayList是线程不安全的,而线程安全的Vector使用了sychronized,性能比较低。

CopyOnWriteArrayList是线程安全版的ArrayList,采用了写时复制的思想,当需要进行修改操作,不会直接修改原数组,而是创建一个副本数组,对副本数组进行修改,修改完之后再将指针指向新数组,就可以保证写操作不会影响读操作,实现了只有写写才需要加锁,读写不需要加锁。

适合读多写少的场景。

缺点:需要占用一定内存开销,并且写操作需要复制,写操作频繁情况下性能会有影响。

Set

存储的元素是不可重复的

HashSet

底层是HashMap的key,可以有一个null值。

元素对象需要重写equals和hashCode方法来保证元素的唯一性,如果没重写,默认是根据内存地址来判断唯一性。

LinkedHashSet

底层是LinkedHashMap的key

可以保证元素的插入顺序,先进先出。

TreeSet

底层是TreeMap的key,不可以有null值。

可以对元素进行自定义排序。

1.需要元素实现Comparable接口,并实现compareTo方法,可以指定当前对象的成员变量和目标对象的成员对象进行比较,如果是前者减去后者,则升序排序。(因为前者减后者如果是正数,则在红黑树中,把前者放到后者的右边,红黑树排序即升序)

2.需要提供实现了Comparator接口的compare方法的比较器,可以指定两个目标对象进行比较,如果是前者减去后者,则升序排序。

判断元素相同的标准:compareTocompare返回值为0

为什么有了自然排序还需要定制排序:有些第三方类也需要排序,但没有提供自然排序方法。

Queue

queue是单向队列,符合先进先出。队尾插入数据,队头删除数据。

PriorityQueue

优先级队列 总是优先级最高的元素出队。

基于堆实现的。

BlockingQueue

阻塞队列,当队列中没有元素,阻塞,不允许删除,当队列中元素满了,阻塞,不允许添加。常用于生产者消费者模型。

ArrayBlockingQueue 数组实现的有界的阻塞队列

LinkedBlockingQueue 单向链表实现的无界的阻塞队列

SynchronousQueue 同步队列 没有容量 插入操作需要阻塞等待删除操作 删除操作也需要阻塞等待插入操作 通常用于高效率的线程传递数据操作

DelayQueue 延迟的阻塞队列 元素到达指定的延迟时间才能出队

PriorityBlockingQueue 无界的优先级阻塞队列 需要自然排序或定制排序

Map

通过key来快速搜索value,key是不可重复的、value是可重复的

HashMap💡💡

HashMap如何添加新元素?

当需要添加新元素时,会先计算元素key的hash值,然后取模计算元素存放的桶的位置,如果桶是空的就直接添加,如果桶不为空,就说明hash表这个索引位置上已经有元素了,产生了hash冲突,会遍历这个桶的链表或红黑树,通过equals方法进行判断key一不一样,如果没有一样的,就添加到桶的末尾,如果有一样的,就直接覆盖。

HashMap如何解决Hash冲突

JDK1.8之前是链表,1.8之后是链表+红黑树。

采用红黑树解决链表过长时遍历慢的问题,不用二叉树排序是因为二叉排序树可能导致斜树,退化成线性的。不用平衡二叉树是平衡二叉树插入需要较多的左旋右旋。红黑树不追求极致的平衡,旋转次数少。

当链表长度大于8时,会先进行判断,如果hash表的数组长度小于64,就会进行扩容,如果大于64,则会把链表转换成红黑树。

为什么1.8要引入红黑树

链表遍历复杂度大,二叉查找树极端情况下也会变成链表,平衡二叉树左旋右旋操作过多,插入时影响效率。

红黑树只保证基本平衡,效率高。

红黑树特性:叶子节点都是空的黑节点,不会有两个相邻的红节点,每条到达叶子节点路径的黑节点数量相同。

为什么到8才转:概率极低

什么时候转回来链表:6,如果是8的话,可能导致频繁转换。

数值都是经过大量科学实验得出的。

HashMap为什么线程不安全?

因为HashMap内部没有对多线程进行同步操作,可能会导致数据丢失,比如put操作,put操作不是原子的,需要判断hash冲突,才能真正插入数据。假设有两个线程都要执行put操作,刚好这个key是不同的,但是会产生hash冲突,也就是hash值取模后的数组索引位置一样。线程1先拿到时间片,判断桶的位置是空的,还没插入数据之前,时间片耗尽来到线程2,线程2也判断桶是空的,插入了数据,然后时间片来到线程1,线程1继续执行插入数据的操作,就把线程2的数据覆盖了。本来是要两个key通过拉链法来存储在一个桶上,变成了只存储了一个值,造成了数据丢失。

HashMap如何扩容,负载因子为何是0.75?

HashMap的扩容条件不是等到数组满了才扩容,而是当元素数量达到负载因子x数组长度,就会扩容,负载因子默认是0.75.

创建新数组,大小是旧数组的2倍。

将原数组的元素重新计算hash,加入到新数组中。

负载因子太低会频繁扩容,太高容易造成hash冲突,通过泊松分布等数学依据推测出0.7左右是最适合,最终选择0.75是因为,hashMap容量为2的n次方,0.75和2的n次方相乘一定是整数。

HashMap的数组长度为什么是2的n次方?

hash值需要映射到数组上的索引位置,要进行取模操作,也就是hash%length,取余操作可以转换成位运算hash&(length-1),运算效率更高,但前提是length需要是2的n次方才能这样子转换。

HashMap多线程操作导致死循环问题?

死循环问题出现在JDK1.8之前,当进行扩容时,需要将旧的元素重新计算索引位置,加入到新的数组里。而在多个线程对同一个桶的链表的加入元素的操作是通过头插法,多线程下可能会形成一个环形链表,导致查询数据会进入死循环。为什么使用之前头插法,因为新插入的数据可能是热点数据。

1.8之后改成尾插法,让插入的节点放在链表末尾,避免多线程下出现环形链表。

HashMap如何遍历

通过map的entrySet()方法+foreach进行遍历。每一个entry都是Map的内部类Entry,通过entry.getKeygetValue来获取键值。

LinkedHashMap

链表代替数组,保证插入顺序。

TreeMap

需要自然排序或定制排序。

ConcurrentHashMap💡

ConcurrentHashMap在1.7和1.8如何保证线程安全的?

1.7中,采用分段锁,一个ConcurrentHashMap包含了一个Segment数组,长度默认是16,也就是同时支持16个线程并发读写。Segment数组的每个元素是一个hashEntry数组,hashEntry数组的每个元素是一个entry,也就是key-value结构。

Segment继承了ReenrantLock,说明Segment也是一种可重入锁,当要对一个entry进行修改时,首先要获得Segment锁,同一个Segment并发修改会阻塞,不同Segment的hashEntry可以并发执行。

1.8中,采用CAS+synchronized,并且锁的粒度更低了,不是分段锁了,而是锁一个node节点,node节点也是key-value结构,当需要对一个node进行修改时,先用CAS来尝试修改,如果CAS尝试修改失败,就用synchronized锁住节点,然后再次尝试。

这样的好处是,锁的粒度更低了,不同的node可以并发修改,而同一个node在并发下,先用CAS乐观锁,先让一个线程获取锁,其他线程全都失败,因为乐观锁的缺点就是可能会大量失败然后重试,所以只要CAS失败一次,就直接用悲观锁锁住,同步来修改。虽然说synchronized是悲观锁比较重,但它经过优化之后,有一系列锁升级的过程,会轻量级一些。

ConcurrentHashMap的key和value可以为null吗?

不可以,为了避免并发下的二义性,也称为歧义。

也就是说,以null作为key或value,无法区分到底是存储的null,还是没有存储。

对于HashMap,如果我们以null为key,我们可以通过contains方法来判断是存储地null还是没有存储,但是对于ConcurrentHashMap不行,因为高并发下contains不是原子性的,执行contains方法的期间可能有其他线程修改了值,因此无法确定到底是存储了null还是没有存储。

因此,单线程下可以容忍歧义,多线程不可以容忍歧义。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值