2019Java面试整理——集合

1.HashMap与HashTable的区别

  • HashTable是线程安全,方法上添加了synchronized同步修饰,HashMap非线程安全

  • HashMap的key和value可以为空,HashTable的key不可以为空

  • HashMap继承AbstractMap,HashTable继承Dictionary,都实现了map接口

  • HashMap的初始容量是16,填充因子默认0.75,扩充时2n,HashTable的初始容量是11,填充因子0.75,扩充时2n+1

  • HashMap对key的hashcode进行了二次hash,然后对数组长度取模,HashTable是对key的hashcode直接取模

  • HashTable是Enumeration遍历,HashMap是Iterator遍历

  • HashMap和HashTable底层是数组+链表结构实现

  • HashMap在1.8以后底层是数组+链表+红黑树(链表长度大于8)

2.HashMap实现原理 

  • 底层实现是数组+链表结构(HashMap在1.8以后底层是数组+链表+红黑树)

  • HashMap默认初始化时会创建一个默认容量为16的Entry数组,默认加载因子为0.75,同时设置临界值为16*0.75

  • HashMap会对null值key进行特殊处理,总是放到table[0]位置

  • put过程是先计算hash然后通过hash与table.length取摸计算index值,然后将key放到table[index]位置,当table[index]已存在其它元素时,判断key的值是否相等(equals方法),若相等直接覆盖;若不相等,会在table[index]位置形成一个链表,将新添加的元素放在table[index],原来的元素通过Entry的next进行链接,这样以链表形式解决hash冲突问题,当链表的长度大于8时,会转换为红黑树。当元素数量达到临界值(capactiyfactor)时,则进行扩容,是table数组长度变为table.length*2

  • 同样当key为null时会进行特殊处理,在table[0]的链表上查找key为null的元素

  • get的过程是先计算hash,然后通过hash与table.length取摸计算index值,然后遍历table[index]上的链表,直到找到key,然后返回

  • remove方法和put get类似,计算hash,计算index,然后遍历查找,将找到的元素从table[index]链表移除

3.重新调整HashMap大小存在什么问题 

  • 多线程的情况下,可能产生条件竞争(race condition)

  • 当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。因此,多线程条件下,Hashmap是线程不安全的。

4.我们可以使用自定义的对象作为Map的键吗

  • 可以使用任何对象作为键

  • 只要它遵守了equals()和hashCode()方法的定义规则

  • 并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象是不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

  • 如果这个对象是可变的,当属性值改变了后,他的hash相应改变了,get的时候将找不到原对象了

5.如何让HashMap线程安全

  • 使用Collections.synchronizedMap()方法来获取一个线程安全的集合(Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理)

  • 使用并发类ConcurrentHashMap

6.ArrayList、LinkedList、Vector的区别 

  • Arraylist和Vector是采用动态数组的数据结构,LinkedList基于链表的数据结构

  • 对于随机访问get和set,ArrayList优于LinkedList,因为ArrayList可以随机定位,而LinkedList要移动指针一步一步的移动到节点处。

  • 对于新增和删除操作add和remove,LinedList比较占优势,只需要对指针进行修改即可,而ArrayList要移动数据来填补被删除的对象的空间。

  • Vector使用了synchronized方法-线程安全,性能上比ArrayList差一点

  • ArrayList数组的起始容量是10.当数组需要增长时,新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长50%。

  • Vector数组的起始容量是10,可以自定义初始容量,新容量=新容量+旧容量

 7.Comparable与Comparator的区别

  • Comparable & Comparator 都是用来实现集合中元素的比较、排序的,只是 Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序,所以,如想实现排序,就需要在集合外定义 Comparator 接口的方法或在集合内实现 Comparable 接口的方法。

  • Comparator位于包Java.util下,而Comparable位于包 java.lang下

  • Comparator定义了俩个方法,分别是 int compare(T o1, T o2)和 boolean equals(Object obj),用于比较两个Comparator是否相等。有时在实现Comparator接口时,并没有实现equals方法,可程序并没有报错,原因是实现该接口的类也是Object类的子类,而Object类已经实现了equals方法

  • Comparable接口只提供了 int compareTo(T o)方法

8.HashMap 扩充时候是否允许插入?原始长度为什么设置为 16

  • HashMap是线程不安全的,允许扩充的时候插入,不过插入的数据将不存在

  • HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标;算法如下:

      /*
    • Returns index for hash code h.*/

    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:

  h & (table.length-1)     hash       table.length-1

    8 & (15-1):             0100   &   1110           =     0100

    9 & (15-1):             0101   &   1110           =     0100

    8 & (16-1):             0100   &   1111           =     0100

    9 & (16-1):             0101   &   1111           =     0101

​从上面的例子中可以看出:当8、9两个数和(15-1)2=(1110)进行“&运算”的时候,产生了相同的结果,都为0100,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。
同时,我们也可以发现,当数组长度为15的时候,hash值会与(15-1)2=(1110)进行“&运算”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1(这是一个奇妙的世界),这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

9.ArrayList使用时常遇到的问题以及解决方法 

  • 集合元素大小为空,get之后没有判断
 list.isEmpty() list.size()>0
  • ArrayList在迭代的时候不能去改变自身的元素集合,否则会抛异常:java.util.ConcurrentModificationException 
    List list = new ArrayList();  
      list.add(new Random().nextInt(10));  
      list.add(new Random().nextInt(10));  
      //开始迭代  
      Iterator iter = list.iterator();  
      while (iter.hasNext()) {  
          System.out.println("迭代:" + iter.next());  
          list.add(new Random().nextInt());  
      } 

     

  • ArrayList在遍历的时候直接删除,出现ConcurrentModificationException

  •  ArrayList在迭代的时候可以用迭代器删除ArrayList中的元素
List list = new ArrayList();  
  list.add(new Random().nextInt(10));  
  list.add(new Random().nextInt(10));  
  //开始迭代  
  Iterator iter = list.iterator();  
  while (iter.hasNext()) {  
      System.out.println(iter.next()+"元素被删除");  
      iter.remove();  
  }  

 

 

  • CopyOnWriteArrayList是线程安全的集合类,该集合在迭代的时候,可以改变自身的元素集合
     List syncList = new CopyOnWriteArrayList();  
      syncList.add(1);  
      syncList.add(5);  
      Iterator iter = syncList.iterator();  
      int flag;  
      while (iter.hasNext()) {  
          flag = iter.next();  
          System.out.println("迭代:" + flag);  
          syncList.remove(new Integer(flag));  
      }  
      System.out.println("集合的大小:" + syncList.size()); 
     
  • CopyOnWriteArrayList是线程安全的集合类,该集合在迭代的时候,不能用迭代器去删除集合中的元素 ,否则会抛异常:java.lang.UnsupportedOperationException
      List syncList = new CopyOnWriteArrayList();  
      syncList.add(1);  
      syncList.add(5);  
      Iterator iter = syncList.iterator();  
      while (iter.hasNext()) {  
          System.out.println("迭代:" + iter.next());  
          iter.remove();  
      }  
      System.out.println("集合的大小:" + syncList.size()); 

     

 

 

 

 

 

 

你可能感兴趣的:(java)