集合框架和数据结构

集合框架的好处

  • 容量自增长;
  • 提供了高性能的数据结构和算法,使编码更加方便,提高了程序的速度和质量;
  • 允许不同API之间的互操作,不同API之间可以互相传递集合;
  • 可以方便的扩展或修改接口,提高了代码的复用性和可操作性;
  • 通过使用jdk提供的集合框架,降低了代码维护和学习新API的成本。

常用集合类

在这里插入图片描述
在这里插入图片描述

  • Collection和Map是所有集合框架的父接口,Collection子接口有set,list,queue;
  • set的实现类主要有:HashSet,TreeSet,LinkedHashSet;
  • list的实现类主要有:ArrayList,LinkedList,Vector,stack;
  • queue的实现类主要有:PriorityQueue
  • Map的实现类主要有:HashMap,TreeMap,LinkedHashMap,HashTable

数组跟集合的区别

  • 数组是固定长度的,集合是不固定长度的;
  • 数组中的元素都是同一类型的,集合中的元素可以不是同一类型的;
  • 数组可以存放基本数据类型,集合不可以,只能存储引用类型。

List,Set,Map三者的区别

  • List和Set都是Collection的子类,而Map不是,Map里面存放的是键值对;
  • List里面的元素是有序的(取出元素的顺序跟插入元素的顺序能保持一致),而Set跟Map都是无序的;
  • List可以存储重复元素,Set和Map的key键不能存放重复元素;
  • List可以放null值,Set和Map的实现类如果是HashSet和HashMap那就可以存放null值;
  • List可以通过for循环来遍历,Set和Map只能通过迭代器来进行遍历。

集合中的Inerator

  • Iterator是面向对象的一种设计模式,屏蔽了不同集合的特点,不需要确切知道集合,只要是Collection的子类,就可以用Interator来进行迭代;
  • Iteretor是“fail—fast”机制,在迭代过程中不能对元素内容进行修改,如果修改了,那下次迭代就会抛异常。Iterator在迭代过程中会直接访问集合元素中的内容,在迭代过程中会使用一个modcount变量。如果集合元素中的内容修改了,那modcount也会修改。每次在用hasNext()和next()来判断时,都会检测modcount是否符合预期的modcount,如果符合就迭代,如果不符合就会终止迭代并抛出异常。

Iterator和ListIterator的区别

  • ListIterator只能遍历list,Iterator可以遍历整个Collection
  • Iterator只能单向遍历,ListIterator可以双向遍历
  • ListIterator实现了Iterator接口,而且还添加了别的API

怎么确保一个集合不能被修改

Collection<String> clist = Collections. unmodifiableCollection();

可以用Collections.unmodifiableCollection(Collection c),如果被修改,就会抛出异常。

遍历List的方式

  • for循环,基于计数器原理。在外部维护一个计数器,依次遍历每一个位置的元素;
  • Iterator,用迭代器来进行遍历;
  • for each语句,本质上还是迭代器,只是写法更加简洁。

ArrayList跟LinkedList的区别

  • Arraylist底层结构是动态数组,LinkedList底层结构是双向链表;
  • 因为ArrayList底层结构是数组,而且也实现了RandomAcces接口,支持随机访问,所在查询效率非常高,可以达到O(1);而LinkedList只能从头依次遍历,查询比较慢
  • 在插入和删除元素时,ArrayList每次都要移动元素,在插入时还需要判断是否需要扩容,所以比较繁琐;LinkedList在插入和删除时只需要改变一下节点之间的指向,非常方便。
  • 在空间上,LinkedList的每个节点还需要再多开辟出两块内存来存放该节点的前驱和后继;ArrayList也可能会造成空间浪费,可能开辟了长度为50的数组,但只存入了30个元素。
  • 所以,ArrayList适用于频繁查询元素增删少的场景,LinkedList适用于插入和删除操作比较多的场景。
  • 两者都不是线程安全的。

ArrayList和Vector的区别

  • ArrayList是非线程安全的,Vector是线程安全的,因为是线程安全的,所以性能就比ArrayList低一些;
  • ArrayList扩容时每次扩到原来的1.5倍,Vector扩容到原来的2倍

多线程场景下如何使用ArrayList

可以通过Collections.synchronizedList()将其转化为线程安全的集合

为什么ArrayList的element会加上transient

ArrayList实现了serializable接口,说明ArrayList支持序列化,但element前面又加上了transient,说明不希望将element序列化。重写了writeObject方法,在序列化时,先调用defaultWriteObject方法将非transient元素序列化,然后再遍历element数组,只将存入的元素序列化,这样既加快了序列化的速度,也减小了序列化后的文件大小。

HashSet的实现原理

HashSet底层是用HashMap实现的,HashSet的值放在HashMap的key,value统一为present,HashSet的API也大都是调用HashMap的相关API来实现的。

Queue中的poll和remove有什么区别

当队列中没有元素时,poll()会返回null,remove会抛出异常。

PriorityQueue

  • PriorityQueue中的元素必须要能比较大小,而且不能为null
  • PriorityQueue默认使用的是comparable来比较大小

HashMap的实现原理

  • HashMap的数据结构:在java中,有两种基本的结构,一种是数组,一种是链表,基本所有的数据结构都可以由这两种结构构造出来,HashMap也不例外,HashMap采用的是数组+链表的结构,也就是链地址法,数组寻址容易但增删比较麻烦,链表增删容易但寻址比较麻烦,HashMap将两者结合起来,发挥各自的优势,在没有发生冲突时,用数组来存放元素,发生冲突时就用链表来存放冲突元素。jdk1.8中,当某条链表过长时又会转化为红黑树来进行优化。
  • 当往HashMap中放入元素的时候,先将key的hashCode重新hash一下,然后再将hash值跟数组长度-1按位与得到在数组中的下标i,如果存在与key的hash值相同的元素就直接覆盖,否则就将键值对放在链表中;当取元素的时候,同样先找到hash值所对应的下标,然后再找到key相同的值。

put()方法的具体实现

  • 首先判断table是否为空和table长度是否为0,如果数组为空或者长度为零,则先扩容;
  • 将key的hashCode重新hash一下,再将hash值跟(table.length-1)按位与,得到key在数组中的索引下标i,如果table[i]为空,那就直接插入成功,如果不为空就进行后续操作;
  • 判断首元素是否跟key相同,如果相同就直接覆盖,如果不相同,就判断首元素是否为红黑树,如果为红黑树,那就在树中进行元素的插入,如果不为红黑树,那就遍历链表;
  • 在遍历链表时会统计节点个数,如果找到跟key相同的元素,直接退出遍历;如果找到最后一个元素还没有找到相同的元素,就尾插在后面,再判断是否达到转化成红黑树的条件,如果达到了就转化成红黑树;
  • 如果找到相同的元素就直接覆盖,返回旧值;
  • 判断table中的键值对数量是否超过threshold,如果超过了就扩容。

java7跟java8HashMap的不同

  • 存储结构:java7为数组+链表;java8为数组+链表+红黑树
  • 初始化方式:java7为单独的一个函数;java8集成到resize()中,而且并没有在构造方法中直接给数组开辟内存,而是延迟到了第一次put()的时候
  • hash计算的方式:java7中9次扰动,4次位运算+5次异或运算;java8中2次扰动,1次位运算+1次异或运算
  • 存储数据的规则:java7:当没有发生冲突时,存放在数组中,发生冲突时存放在链表中;java8:没有发生冲突时,存放在数组中,发成冲突时如果数组长度超过64并且链表长度超过8就会放在红黑树中,否则放在链表中
  • 插入数据的方式:java7:头插,先将首元素往后移动一个位置,然后再插入新元素,在多线程环境下可能会死循环;尾插,在多线程下虽然避免了死循环,但还是不安全的,可能会造成数据丢失
  • 扩容后存储位置的计算方式:java7:还是跟以前一样,将key的hashCode重新hash一下,然后计算在扩容后的数组中的位置;java8:将扩容后的容量跟hash值按位与,只需要看hash值中对应扩容后多出来的那一位为1还是为0

HashMap是如何解决hash冲突的

  • hash冲突就是不同的元素通过同一个hash函数计算出来的hash值一样
  • 采用数组+链表+红黑树的结构,也就是链地址法,链表用来存放hash冲突的键值对,当键值对过多时就将链表转化为红黑树,降低遍历的时间复杂度
  • hash函数的计算方式:hashCode的高16位不变,让低16位跟高16位按位异或,让更多的比特位参与运算,减少碰撞

HashMap的容量为什么总是2的N次幂

  • 在计算key在数字中的索引位置时,可以用hash&(table.length-1),在容量为2的N次幂时等价于除留余数法,但是位运算效率更高
  • 扩容后存储位置的计算方式:将扩容后的容量跟hash值按位与,只需要看hash值中对应扩容后多出来的那一位为1还是为0,如果为0就不需要移动,如果为1就移动到现在的位置+原容量的位置,效率也更高

能否将任何类作为key存入HashMap

可以,只要该类重写了hashCode和equals方法

  • equals相等hashCode一定相等
  • hashCode相等equals不一定相等
  • 两个相同的元素,它们的equals方法都返回true

HashMap为什么不直接用hashCode作为数组中的下标

  • hashCode的返回值是int,取值范围在-2^31~2^31-1,而HashMap的取值范围在16~2^30,而且通常情况下是取不到最大值的,就有可能会出现通过hashCode计算出来的值不在HashMap中,设备也很难一下子开辟出这么大的内存
  • 直接用hashCode碰撞率比较大,将hashCode的高16位跟低16位做个异或,让更多比特位参与运算,减少了碰撞。

HashMap跟HashTable的区别

  • HashMap是线程非安全的,HashTable是线程安全的,所以效率
    比较低一些
  • HashMap的key可以为null,HashTable不能
  • HashMap的默认容量大小为16,每次扩容都是2的N次幂;HashTable默认容量为11,每次都扩容到2n+1
  • HashMap底层数据结构是数组+链表+红黑树;HashTable底层数据结构是数组+链表

HashMap和ConcurrentHashMap

  • HashMap不是线程安全的,ConcurrentHashMap是线程安全的
  • HashMap的key可以存放null,ConcurrentHashMap不能

ConcurrentHashMap和HashTable

都是线程安全的,但是实现线程安全的机制不一样

  • HashTable对整个数组加了一把锁,并发效率比较低,只要访问数组中的元素,就需要竞争锁
  • ConcurrentHashMap1.7:采用分割分段的方法,给数组分段,每一段加上一把锁,如果两个线程访问的不是一个段内的数据就不用竞争锁,大大提高了效率,默认分配了16个Segment,Segment相当于锁,HashEntry数组存放键值对,如果想要访问HashEntry数组中某个键值对,就需要获取对应的Segment。
  • ConcurrentHashMap1.8:采用Node+CAS+synchronized来保证线程安全,synchronized只锁每个链表或红黑树的头结点

Comparable和Comparator

  • Comparable是java.lang包中,实现Comparable接口,需要重写comparTo方法
  • Comparator是java.uitl包中,实现Comparator接口,需要重写compare方法

Collection和Collections

  • Collection:是一个集合接口,提供了对集合对象进行操作的各种通用接口方法
  • Collections:是集合类的一个工具类,里面有一系列的静态方法,用于多Collection排除,查找,线程安全等操作
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值