Java容器——常见面试题汇总

目录

1、Java的集合框架

 2、List、Set、Map之间的区别

3、ArrayList、LinkedList和Vector的区别

4、HashMap、HashTable、TreeMap和WeakHashMap

5、HashMap是具体实现(存取、hash冲突、扩容)

6、并发下HashMap死循环的原因

7、JDK 1.8中HashMap的改进

8、为什么HashMap中String、Integer这样得包装类适合作为key键

9、迭代器

10、如何获取一个不能被修改的集合


1、Java的集合框架

Java集合框架图(转自:java集合框架综述)如下:

 2、List、Set、Map之间的区别

  • List又称为有序的Collection。它按对象进入的顺序保存对象,每个对象都有它的索引值,且第一个元素的索引值为0,所以他能对列表中的每个元素的插入和删除位置进行精确的控制。同时,它可以保存重复的对象。LinkedList、ArrayList和Vector都实现了List接口。
  • Set表示数学意义上的集合概念。其最主要的特点是集合中的元素不能重复,因此存入Set的每个元素都必须定义equals()方法来确保对象的唯一性。该接口有两个实现类:HashSet和TreeSet。其中TreeSet实现了SortedSet接口,因此TreeSet容器中的元素是有序的。
  • Map提供了一个从键映射到值的数据结构。它用于保存键值对,其中值可以重复,但键必须唯一,不能重复。实现该接口的类有:HashMap、TreeMap、LinkedHashMap、WeakHashMap和IdentityHashMap。其中HashMap是基于散列表实现的,采用对象的HashCode可以进行快速查询。LinkedHashMap继承了HashMap也是基于散列表的,只是它内部多一个维护键存储顺序的链表。TreeMap是基于红黑树的数据结构来实现的,内部的是按键的顺序排列的。

3、ArrayList、LinkedList和Vector的区别

ArrayList、LinkedList和Vector类都实现了List接口,所以它们对数据都是顺序保存的,且可以保存重复的数据和用索引查找

  • ArrayList是一个动态数组,初始话时数组有一个初始容量,当存储数据数量超过这个大小时,就需要动态的扩充它们的存储空间(ArrayList会将容量扩充为原来的1.5倍)。由于数组存储空间连续的特性,所以对数据的索引查找速度比较快,但是在插入元素时需要移动容器中的元素,所以对数据的插入操作执行起来比较慢
  • Vector跟ArrayList一样底层也是动态数组,只不过Vector默认扩容为原来2倍,且每次扩容空间的大小可以设置。此外,和ArrayList相比Vector是线程安全的,其大多方法都是synchronized的,所以对于单线程程序来说,性能上会略逊于ArrayList。
  • LinkedList是采用双向链表来实现的,对数据的索引需要从列表的表头开始遍历,因此随机访问效率比较低,但是插入元素时不需要对数据进行移动,因此插入效率较高。同时,跟ArrayList一样,它也是非线程安全的。

4、HashMap、HashTable、TreeMap和WeakHashMap

Map是用来存储键值对的数据结构,在Map中通过对象来索引对象,用来索引的对象叫做key,其索引的对象叫做value。HashMap、HashTable、TreeMap和WeakHashMap是Map的实现类。

  • HashMap是一个常用的Map,其内部定义了一个hash表数组(Entry[] table),他根据键的HashCode值对数据进行存储,根据键的HashCode值计算出元素在数组中的位置,如有冲突发生(两个不同的键计算出来的位置相同)则使用散列链表的形式将所有相同哈希值的元素串起来。由于采用了hash法进行索引,因此其具有很快的访问速度。
  • HashTable与HashMap都采用hash法进行索引,其区别如下:
  1. HashMap允许空(null)键值。而HashTable不允许。
  2. HashMap去掉了HashTable的contains方法,改成了containsKeycontainsValue
  3. HashTable是线程安全的。HashMap不是。
  4. HashMap用Enumeration,而HashMap用Iterator
  5. HashTable中,hash数组默认大小是11,增加方式是oldx2+1。在HashMap中,hash数组的默认大小是16,而且一定是2的指数。
  6. HashTable的hash算法为键的HashCode值模数组的大小。而HashTable的hash算法为键的HashCode值与上数组的大小减一。
  • TreeMap实现了SortMap接口,能够把它保存的记录根据键排序,因此,取出来的是排序后的键值对,如果需要按键的自然顺序或者自定义顺序遍历键,那么TreeMap会更好。TreeMap的底层实现是红黑树。
  • LinkedHashMap继承了HashMap也是基于散列表的,只是它内部多一个维护键存储顺序的链表。,它可以按元素的插入顺序来遍历元素。
  • WeakHashMap与HashMap类似,二者的不同之处在于WeakHashMap中key采用的是“弱引用”的方式,只要WeakHashMap中的key不再被外部引用,它就可以被垃圾回收器回收。而HashMap中key采用的是“强引用的方式”,当HashMap中的key没有被外部引用时,只有在这个key从HashMap中删除后,才可以被垃圾回收器回收。

5、HashMap是具体实现(存取、hash冲突、扩容)

参考:HashMap实现原理分析

6、并发下HashMap死循环的原因

参考:老生常谈,HashMap的死循环

总结:其发生死循环的本质就是并发执行put()操作导致触发扩容行为,从而导致在resize()时形成环形链表,使得在获取数据遍历链表时形成死循环。

HashMap死循环是在JDK 1.7中存在的问题,在新的JDK 1.8中已经对HashMap进行了修改。

7、JDK 1.8中HashMap的改进

参考:Java源码分析:HashMap 1.8 相对于1.7 到底更新了什么?

总结变化:

  1. JDK1.7的时候使用的是数组+单链表的数据结构。但是在JDK1.8及之后,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结果,从而把时间复杂度有O(n)变为了O(logN),从而提高了效率)。
  2. JDK1.7在resize()的时候对hash冲突生成链表的时候使用的是头插法,而JDK1.8及之后使用的都是尾插法。这是由于使用尾插法会造成上述死循环问题。
  3. JDK1.8在resize()的时候重新计算键key的索引的时候使用了之前的位置信息,具体如下图所示:

      4.再插入元素和查找元素时JDK 1.7和JDK 1.8对hash值得计算方式不同:

      // JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作  = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
      static final int hash(int h) {
        h ^= k.hashCode(); 
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
     }

      // JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
      // 1. 取hashCode值: h = key.hashCode() 
      // 2. 高位参与低位的运算:h ^ (h >>> 16)  
      static final int hash(Object key) {
           int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
            // a. 当key = null时,hash值 = 0,所以HashMap的key 可为null      
            // 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
            // b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
     }

 

8、为什么HashMap中String、Integer这样得包装类适合作为key键

  • String、Integer等包装类都时不可变类,即保证了key得不可更改性,所以不会出现放入与获取时哈希码不同的情况。
  • 内部重写了equals()、hashcode()方法。有着优秀的hash值的计算方法,可以有效的避免hash碰撞。

9、迭代器

迭代器(Iterator)是一个对象,它的工作是遍历容器中的对象,它提供了一种访问一个容器(container)对象中的各个元素1,而又不必暴露该对象内部细节的方法。迭代器的使用须注意以下内容:

  • 使用容器的iterator()方法返回一个Iterator,然后通过Iterator的next()方法返回第一个元素。
  • 使用Iterator的hasNext()方法判断容器中是否还有元素,如果有,可以使用next()方法获取下一个元素。
  • 可以通过remove()方法删除迭代器返回的元素。

Iterator支持派生的兄弟成员。ListIterator只存在于List中,支持在迭代期间向List中添加或删除元素,并且可以在List中双向滚动。

在使用iterator()方法时经常会遇到ConcurrentModificationException异常,这通常是由于在使用Iterator遍历容器的同时又对容器做增删操作,或者由于多线程操作导致。容器中维护者一个modCount值,这个值相当于是容器的版本号,每当进行对容器的修改操作时,这个值就会变化,当我们使用iterator()方法返回一个Iterator时,这个Iterator内部就记录了当前modCount的值(将其赋给了expectedModCount),然后在迭代过程中,容器每次都会判断modCount与expectedModCount的值是否相等,若不相等则抛出异常。

10、如何获取一个不能被修改的集合

可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,改变集合的任何操作都会抛出Java.lang.UnsupportedOperationException异常。

示例代码:

List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值