面试-java 集合一

1. ListSetMap区别

List,Set,Map将持有对象一律视为Object型别。 Collection、List、Set、Map都是接口,不能实例化。继承自它们的 ArrayList, Vector, HashTable, HashMap是具象class,这些才可被实例化。vector容器确切知道它所持有的对象隶属什么型别。vector不进行边界检查。

List的元素以线性方式存储,可以存放重复对象,List主要有以下两个实现类: ArrayList : 长度可变的数组,可以对元素进行随机的访问,向ArrayList中插入与删除元素的速度慢。 JDK8 中ArrayList扩容的实现是通过grow()方法里使用语句newCapacity = oldCapacity + (oldCapacity >> 1)(即1.5倍扩容)计算容量,然后调用Arrays.copyof()方法进行对原数组进行复制。LinkedList: 采用链表数据结构,插入和删除速度快,但访问速度慢。

Set(集合) Set中的对象不按特定(HashCode)的方式排序,并且没有重复对象,Set主要有以下两个实现类:HashSet: HashSet按照哈希算法来存取集合中的对象,存取速度比较快。当HashSet中的元素个数超过数组大小*loadFactor(默认值为0.75)时,就会进行近似两倍扩容(newCapacity = (oldCapacity << 1) + 1)。TreeSet :TreeSet实现了SortedSet接口,能够对集合中的对象进行排序。

Map(映射)Map是一种把键对象和值对象映射的集合,它的每一个元素都包含一个键对象和值对象。 Map主要有以下两个实现类: HashMap:HashMap基于散列表实现,其插入和查询<K,V>的开销是固定的,可以通过构造器设置容量和负载因子来调整容器的性能。 LinkedHashMap:类似于HashMap,但是迭代遍历它时,取得<K,V>的顺序是其插入次序,或者是最近最少使用(LRU)的次序。TreeMap:TreeMap基于红黑树实现。查看<K,V>时,它们会被排序。TreeMap是唯一的带有subMap()方法的Map,subMap()可以返回一个子树。

HashMap底层实现:HashMap底层整体结构是一个数组,数组中的每个元素又是一个链表。每次添加一个对象(put)时会产生一个链表对象(Object类型),Map中的每个Entry就是数组中的一个元素(Map.Entry就是一个<Key,Value>),它具有由当前元素指向下一个元素的引用,这就构成了链表。

        存储原理:当向HsahMap中添加元素的时候,先根据HashCode重新计算Key的Hash值,得到数组下标,如果数组该位置已经存在其他元素,那么这个位置的元素将会以链表的形式存放,新加入的放在链头,最先加入的放在链尾,如果数组该位置元素不存在,那么就直接将该元素放到此数组中的该位置。

        去重原理:不同的Key算到数组下标相同的几率很小,新建一个<K,V>放入到HashMap的时候,首先会计算Key的数组下标,如果数组该位置已经存在其他元素,则比较两个Key,若相同则覆盖写入,若不同则形成链表。

        读取原理:从HashMap中读取(get)元素时,首先计算Key的HashCode,找到数组下标,然后在对应位置的链表中找到需要的元素。

        扩容机制:当HashMap中的元素个数超过数组大小*loadFactor(默认值为0.75)时,就会进行2倍扩容(oldThr << 1)。

2. Arraylist

a)查询起来快?ArrayList从原理上就是数据结构中的数组,也就是内存中一片连续的空间,这意味着当get(index)的时候,可以根据数组的(首地址+偏移量),直接计算出访问的第index个元素在内存中的位置。

LinkedList可以简单理解为数据结构中的链表(说简单理解,因为其实是双向循环链表),在内存中开辟的不是一段连续的空间,而是每个元素有一个[元素|下一元素地址]这样的内存结构。当 get(index)时,只能从首元素开始,依次获得下一个元素的地址。
用时间复杂度表示的话,ArrayList的get(n)是o(1),而LinkedList是o(n)。

b) Arraylist能改变数组大小吗?ArrayList在存之前会判断一次,看看下标索引,有没有到达初始化长度最大索引的临近位置,如果到了,它就新建一个临时数组长度为当前数组长度的1.5倍,然后在把当前数组内容拷贝到临时数组里面去。最后把当前数组指向临时数组引用,这样就完成当前数组的扩容!

1)ArrayList通过new ArrayList()生成时默认的数组容量为10。

2)当ArrayList中对象数量超出数组容量arg时,数组容量会增加arg>>1。此时的数组容量为arg+(arg>>1)。>>表示带符号右移,如:int i=15; i>>2的结果是3,移出的部分将被抛弃。转为二进制的形式可能更好理解,0000 1111(15)右移2位的结果是0000 0011(3),0001 1010(18)右移3位的结果是0000 0011(3)。10>>1 =5, 15>>1=7, 22>>1=11。

JDK1.7(饿汉式) 当实例化ArrayList时,创建长度为10的object[ ] ;当add添加到11个的时候,扩容,扩容为原来的1.5倍。将原来的数据复制到新的数组中。建议使用new ArrayList(int capacity)直接声明数组的大小;

1.8(懒汉式)的变化,当实例化是,创建object[ ] ,初始化为 { },并没有长度。当添加第一个元素时,创建长度为10的数组。后续一致。延迟数组的创建,节省内存。

3. arrayListlinkedList, Vector

共性:ArrayList与LinkedList都是List接口的实现类。

区别:List接口的实现方式不同

a):ArrayList实现了List接口,以数组的方式来实现的,利于查询,不利于增删;

b):LinkedList是采用链表的方式来实现,利于增删,不利于查询。

c): Vector为什么是线程安全的,Vector和ArrayList 实现了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 关键修饰。但对于复合操作,Vector 仍然需要进行同步处理。这样做的后果,Vector 应该尽早地被废除,因为这样做本身没有解决多线程问题,反而,在引入了概念的混乱的同时,导致性能问题,因为 synchronized 的开销是巨大的:阻止编译器乱序.

4. CopyOnWriteArrayList/Set

CopyOnWriteArrayList线程安全的变体, 使用写时复制策略保证list的一致性,而获取–修改–写入三个步骤不是原子性,所以需要一个独占锁保证修改数据时只有一个线程能够进行。另外,CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照。

CopyOnWriteArraySet是一个支持并发访问的容器, 实现是借助CopyOnWriteArrayList实现的,只不过CopyOnWriteArraySet是在CopyOnWriteArrayList上使用indexOf不允许存入重复元素,正好符合Set的特性!

5. HashMap构造方法

1) HashMap() 使用默认初始容量16与默认负载因子0.75构造一个空的HashMap。

2) HashMap(int initialCapacity, float loadFactor)传入初始容量和负载因子来构造一个空的HashMap。由于HashMap的容量必须为2的幂次方,且int类型范围为-2^32 ~ 2^32-1,所以MAXIMUM_CAPACITY为int类型中为2的幂次方且最大的值。

3) HashMap(Map<? extends K, ? extends V> m)根据已有的Map接口创建一个元素相同的HashMap,使用默认初始容量与默认负载因子。

4) Hash冲突,HashMap底层是由数组+链表/红黑树构成的,当我们通过put(key, value)向hashmap中添加元素时,需要通过散列函数确定元素究竟应该放置在数组中的哪个位置,当不同的元素被放置在了数据的同一个位置时,后放入的元素会以链表的形式,插在前一个元素的尾部,这个时候我们称发生了hash冲突。解决hash冲突?

6. Hashmap循环方法

1) 使用For-Each迭代entries

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

for(Map.Entry<Integer, Integer> entry : map.entrySet()){

System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue()) }

2) 使用For-Each迭代keys和values

如果你只需要用到map的keys或values时,你可以遍历KeySet或者values代替entrySet

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

//iterating over keys only

for (Integer key : map.keySet()) { System.out.println("Key = " + key); }

// iterating over values only

for (Integer value : map.values()) { System.out.println("Value = " + value); }

3) 使用Iterator迭代, 使用泛型

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();

while (entries.hasNext()) {

Map.Entry<Integer, Integer> entry = entries.next();

System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); }

不使用泛型 Map map = new HashMap();

Iterator entries = map.entrySet().iterator();

while (entries.hasNext()) { Map.Entry entry = (Map.Entry) entries.next();

Integer key = (Integer)entry.getKey(); Integer value = (Integer)entry.getValue(); }

它是遍历老java版本map的唯一方法。另外一个重要的特性是可以让你在迭代的时候从map中删除entries的(通过调用iterator.remover())唯一方法.如果你试图在For-Each迭代的时候删除entries,你将会得到unpredictable resultes 异常。

 

7. hashMap底层实现原理

JDK1.7数据结构则是采用的位桶和链表相结合的形式完成了,即拉链法,用的是数组+ 单链表的数据结构。默认的大小是16,一旦>0.75*16之后,就会调用resize()进行2倍扩容,扩容非常耗时,所以如果需要保存较多的话,最好在创建一开始就制定好HashMap的初始容量。

JDK1.7的时候是先进行扩容后进行插入,而在JDK1.8的时候则是先插入后进行扩容的呢?

其实就是当这个Map中实际插入的键值对的值的大小如果大于这个默认的阈值的时候(初始是16*0.75=12)的时候才会触发扩容,这个是在JDK1.8中的先插入后扩容。

JDK1.7中的话,是先进行扩容后进行插入的,就是当你发现你插入的桶是不是为空,如果不为空说明存在值就发生了hash冲突,那么就必须得扩容,但是如果不发生Hash冲突的话,说明当前桶是空的(后面并没有挂有链表),那就等到下一次发生Hash冲突的时候在进行扩容,但是当如果以后都没有发生hash冲突产生,那么就不会进行扩容了,减少了一次无用扩容,也减少了内存的使用。

常见的有数据结构有三种结构:1.数组结构 2.链表结构 3.哈希表结构。来看看各自的数据结构的特点:1.数组结构: 存储区间连续、内存占用严重、空间复杂度大。优点:随机读取和修改效率高,原因是数组是连续的(随机访问性强,查找速度快)缺点:插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中都要往后移动,且大小固定不易动态扩展。2.链表结构:存储区间离散、占用内存宽松、空间复杂度小。优点:插入删除速度快,内存利用率高,没有固定大小,扩展灵活缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)。3.哈希表结构:结合数组结构和链表结构的优点,从而实现了查询和修改效率高,插入和删除效率也高的一种数据结构 常见的HashMap就是这样的一种数据结构。

HashMap中的put()和get()的实现原理:

Java7中Hashmap底层采用的是Entry对数组,而每一个Entry对又向下延伸是一个链表,在链表上的每一个Entry对不仅存储着自己的key/value值,还存了一个当前对象的hash值和指向下一个地址的next Node节点。Java8中的Hashmap底层结构有一定的变化,还是使用的数组,但是数组的对象以前是Entry对,现在换成了Node对象(可以理解是Entry对,结构一样,存储时也会存key/value键值对、当前对象的hash值和指向下一个地址的next Node节点),以前所有的Entry向下延伸都是链表。

1). put(k,v)实现原理(1)首先将k,v封装到Node对象当中(节点)。(2)然后它的底层会调用K的hashCode()方法得出hash值。(3)通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。

2). get(k)实现原理(1)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。(2)通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个value。

红黑树原理分析: 1.好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢。 2.红黑树查询:其访问性能近似于折半查找,时间复杂度 O(logn); 3.链表查询:这种情况下,需要遍历全部元素才行,时间复杂度 O(n); 4.简单的说,红黑树是一种近似平衡的二叉查找树,其主要的优点就是“平衡“,即左右子树高度几乎一致,以此来防止树退化为链表,通过这种方式来保障查找的时间复杂度为 log(n)。

Java8变成链表和红黑树的组合,数据少量存入的时候优先还是链表,当链表长度大于8,且总数据量大于64的时候,链表就会转化成红黑树,所以你会看到Java8的Hashmap的数据存储是数组+链表+红黑树的组合,如果数据量小于64则只有数组+链表,如果数据量大于64,且某一个数组下标数据量大于8,那么该处即为红黑树。这样设计好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢。当红黑树上的节点数量小于6个,会重新把红黑树变成单向链表数据结构。中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

JDK8增加了Spliterator进行遍历。为了并行遍历元素而设计的一个迭代器。如示例:

Spliterator<user> users = list.spliterator(); users.forEachRemaining((n) -> print(n));

【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator

方式,如果并发操作,需要对 Iterator 对象加锁。

8. HashMapHashtable的区别

1) 继承的父类不同, HashMap继承自AbstractMap类。但二者都实现了Map接口。Hashtable继承自Dictionary类,Dictionary类是一个已经被废弃的类(见其源码中的注释)。父类都被废弃,自然而然也没人用它的子类Hashtable了。

2) HashMap线程不安全,HashTable线程安全

3)包含的contains方法不同:HashMap是没有contains方法的,而包括containsValue和containsKey方法;hashtable则保留了contains方法,效果同containsValue,还包括containsValue和containsKey方法。

4) 是否允许null值, Hashmap是允许key和value为null值的,用containsValue和containsKey方法判断是否包含对应键值对;HashTable键值对都不能为空,否则包空指针异常。

9.HashSetTreeSet

HashSet有以下特点:不能保证元素的排列顺序,顺序有可能发生变化;不是同步的;集合元素可以是null,但只能放入一个null,当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值来决定该对象在HashSet中存储位置。简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相等。注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对 象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算hashCode的值。

TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0 自然排序 自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。 obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是 负数,则表明obj1小于obj2。 如果我们将两个对象的equals方法总是返回true,则这两个对象的compareTo方法返回应该返回0 定制排序 自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法。

最重要:1)TreeSet是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值。 2)HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。3)HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例。

10. HashMapConcurrentHashMap

区别: 1). HashMap不支持并发操作,没有同步方法,ConcurrentHashMap支持并发操作,通过继承 ReentrantLock(JDK1.7重入锁)/CAS和synchronized(JDK1.8内置锁)来进行加锁(分段锁),每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。2).JDK1.8之前HashMap的结构为数组+链表,JDK1.8之后HashMap的结构为数组+链表+红黑树;JDK1.8之前ConcurrentHashMap的结构为segment数组+数组+链表,JDK1.8之后ConcurrentHashMap的结构为数组+链表+红黑树。

11. ConcurrentHashMap是怎么实现线程安全的

ConcurrentHashMap 1.7由Segment数组结构和HashEntry数组结构组成。将数据分别放到多个Segment中,默认16个,扩容因子默认0.75,2倍扩容。每一个Segment中又包含了多个HashEntry列表数组,对于一个key,需要经过三次hash操作,才能最终定位这个元素的位置,这三次hash分别为:

对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。

每一个Segment都拥有一个锁,当进行写操作时,只需要锁定一个Segment,而其它Segment中的数据是可以访问的。

static final class Segment<K,V> extends ReentrantLock implements Serializable {

transient volatile HashEntry<K,V>[] table;

transient int count; }

 1.8底层数据结构大致是以一个Node对象数组来存放数据,Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树。值得注意的是Node的value和next指针使用了volatile来保证其可见性!

多个线程同时进行put操作,在初始化数组时使用了乐观锁CAS操作来决定到底是哪个线程有资格进行初始化,其他线程均只能等待。

用到的并发技巧:volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功。ConcurrentHashMap 1.8中取消segments字段,直接采用transient volatile Node<K,V>[] table保存数据,采用table数组元素(链表的首个元素或者红黑色的根节点)作为锁,从而实现了对每一行数据进行加锁,减少并发冲突的概率。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值