Java集合框架梳理(含经典面试题)

Java Collections Framework是Java提供的对集合进行定义,操作,和管理的包含一组接口,类的体系结构。

1. 整体框架

Java容器类库一共有两种主要类型:Collection和Map。层次结构如下: 

蓝色椭圆框为接口类(不可实例化),黑色矩形框为实现类或子类。

java.util.Collection [I]
+--java.util.List [I]
   +--java.util.ArrayList [C]
   +--java.util.LinkedList [C]
   +--java.util.Vector [C]
      +--java.util.Stack [C]
+--java.util.Set [I]
   +--java.util.HashSet [C]
   +--java.util.SortedSet [I]
      +--java.util.TreeSet [C]
 
java.util.Map [I]
+--java.util.SortedMap [I]
   +--java.util.TreeMap [C]
+--java.util.Hashtable [C]
+--java.util.HashMap [C]
+--java.util.LinkedHashMap [C]
+--java.util.WeakHashMap [C]
 
[I]:接口
[C]:类
 
2. Collection接口
Collection是最基本的集合接口,一个Collection代表一组Object的集合,这些Object被称作Collection的元素。子类如下:
  • List:有序,元素可重复的集合。因为用户能够使用索引来访问List中的元素。
    • ArrayList:底层的数据结构使用的是数组,实现了可变大小的数组。非同步的(unsynchronized)。擅长随机访问,查询快、插入删除移动元素较慢。方法:size(),isEmpty(),get(),set(),add()。
    • LinkedList:底层使用的链表数据结构。非同步。增删移元素快,随机访问(查询)较差。提供额外的get,remove,insert方法在 LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
    • Vector:类似ArrayList,但是是同步的(synchronized),当一个Iterator被创建且正在被使用,另一个线程改变了Vector的状态时,调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。
      • Stack:继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push()和pop(),还有peek()得到栈顶的元素,empty()测试堆栈是否为空,search()检测一个元素在堆栈中的位置。Stack刚创建后是空栈。
  • Set:无序,元素不可重复的集合,因为没有索引。Set最多有一个null元素。
    • HashSet:底层数据结构是哈希表。非同步。保证元素的唯一性:hashCode和equals()。适合存取和查找。
      • LinkedHashSet:链表+哈希表。需维护元素的插入顺序,因此性能略低于HashSet,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表适合进行遍历)。
    • TreeSet:底层数据结构是二叉树。有序,用二叉树排序。保证元素的唯一性:compareTo()。
  • Queue:先进先出的容器。新元素插入(offer)到队列尾部,访问元素(poll)操作会返回队列头部的元素,不允许随机访问队列中的元素。
    • PriorityQueue:优先级队列,保存队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行重新排序。
 
3. Map接口
保存具有"映射关系"的数据。Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value。key和value都可以是任何引用类型的数据。Map的key不允许重复。
  • HashTable:同步。不允许空。
  • HashMap:非同步。允许null,即null value和null key(最多一个)。
  • WeekHashMap:一种改进的HashMap,对key实行“弱引用”:如果一个key不再被外部所引用,那么该元素可以被GC回收。
  • TreeMap:有序。
 
Map的遍历: keySet()和entrySet()
3.1  Map.keySet()
将Map中所有的键存入到set集合中。因为set具备迭代器。所有可以迭代方式取出所有的键,再根据get方法。获取每一个键对应的值。迭代后只能通过get()取key 。取到的结果会乱序,是因为取得数据行主键的时候,使用了HashMap.keySet()方法,而这个方法返回的Set结果,里面的数据是乱序排放的。示例如下:
Map map = new HashMap();
map.put("key1","lisi1");
map.put("key2","lisi2");
map.put("key3","lisi3");
//Map的keySet()方法先获取所有键的set集合,并通过Set的迭代器进行迭代遍历
Iterator it = map.keySet().iterator();
while(it.hasNext()){
Object key = it.next();
//通过迭代器遍历Map的值对象
System.out.println(map.get(key)); 
}

 

3.2 Map.entrySet()

Set<Map.Entry<K,V>> entrySet();  //把(key-value)作为一个整体一对一对地存放到Set集合当中的。Map.Entry表示映射关系。entrySet():迭代后可以e.getKey(),e.getValue()两种方法来取key和value。返回的是Entry接口。示例:

Map map = new HashMap();
map.put("key1","lisi1");
map.put("key2","lisi2");
map.put("key3","lisi3");
//将map集合中的映射关系存入到Set集合,并获取迭代器用来遍历
Iterator it = map.entrySet().iterator();
while(it.hasNext()){
//迭代遍历,并转类型为Entry
Entry e =(Entry) it.next();
//Entry.getKey ()、Entry.getValue()可以分别得到Entry对象的键和值
System.out.println("键"+e.getKey () + "的值为" + e.getValue());
}

对比:推荐使用entrySet(),因为效率更高。

对于keySet()方法,遍历了两次Map,一次用来从HashMap中取出所有key到Set集合,一次从HashMap中取出所有key对应的value。而entrySet()只遍历了一次,因为直接将key-value作为整体放入了Entry对象中,所以效率更高。

 

4. 主要实现类对比

4.1 Vector和ArrayList

相同点:都实现了List接口,元素有序可重复;都基于数组的数据结构实现,都允许直接序号索引元素,所以随机性较好,适合用于查询,但是增删移动数据比较慢(用LinkedList);

不同点:Vector是线程同步的,所以它也是线程安全的,而ArrayList是线程异步的,是不安全的;Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差;在集合中使用数据量比较大的数据,用Vector有一定的优势。

 

4.2ArrayList和LinkedList
相同点:都实现了List接口,元素有序可重复;

不同点:具体实现:ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构;效率:对于随机访问get和set,ArrayList优于LinkedList,但对于新增和删除操作add和remove,LinedList比较占优势。因为LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,而ArrayList每插入一条数据,要移动插入点及之后的所有数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList。 

 

4.3 HashMap与TreeMap
都实现了Map接口,是一种键值对的映射关系。 

HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。
在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。

使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。
两个map中的元素一样,但顺序不一样,导致hashCode()不一样。
同样做测试:
在HashMap中,同样的值的map,顺序不同,equals时,false;
而在TreeMap中,同样的值的map,顺序不同,equals时,true,说明,treeMap在equals()时是整理了顺序了的。

 

4.4 HashTable与HashMap
都实现了Map接口,是一种键值对的映射关系。采用的hash/rehash算法类似,性能差异不大。

HashMap不是同步的,是线程不安全的;允许一个null键和多个null值。HashTable是同步的,线程安全;不允许有null的键或值。

HashTable有contains()方法,在HashMap中只有containsKey()和containsValue()来检测哈希表中是否有某个key或者value,有则返回true。

 

总结:
  • 有序:实现List接口的ArrayList、LinkedList、Vector等有序,和实现了SortedXX的TreeXX有序:TreeSet、TreeMap;其他通通无序。在除需要排序时使用TreeSet,TreeMap外,都应使用HashSet,HashMap,因为他们的效率更高。
  • 接口:Collection、List、Set、SortedSet、Queue、Map、SortedMap。共7个,接口不可被实例化。
  • 同步的类:Vector、HashTable。同步的会使得效率降低。所以如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类,保证安全性。
  • 如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。  
  • 要特别注意对哈希表的操作,作为key的对象要正确覆盖equals()和hashCode()方法。
  • 容器类仅能持有对象引用(指向对象的指针),而不是将对象信息copy一份至数列某位置。一旦将对象置入容器内,便损失了该对象的型别信息。
  • 尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。
 
注意:
1、Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。
2、Set和Collection拥有一模一样的接口。
3、List,可以通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get)
4、一般使用ArrayList。用LinkedList构造堆栈stack、队列queue。
5、Map用 put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value。
      HashMap会利用对象的hashCode来快速找到key。
 
 
5. 面试题集合

  1、什么是Java集合API

  Java集合框架API是用来表示和操作集合的统一框架,它包含接口、实现类、以及帮助程序员完成一些编程的算法。简言之,API在上层完成以下几件事:编程更加省力,提高城程序速度和代码质量;非关联的API提高互操作性;节省学习使用新API成本;节省设计新API的时间;鼓励、促进软件重用。

  具体来说,有7个集合接口,最基本的是Collection接口,由三个接口Set、List、Queue、SortedSet继承,另外两个接口是Map、SortedMap,这两个接口不继承Collection,表示映射而不是真正的集合。

 

  2、什么是Iterator

  一些集合类(Set)提供了内容遍历的功能,通过java.util.Iterator接口。这些接口允许遍历对象的集合。依次操作每个元素对象。当使用 Iterators时,在获得Iterator的时候包含一个集合快照。通常在遍历一个Iterator的时候不建议修改集合本省。

  • Iterator与ListIterator有什么区别?

  Iterator:只能正向遍历集合,适用于获取移除元素。ListIerator:继承Iterator,可以双向列表的遍历,同样支持元素的修改。

 

  3、什么是HaspMap和Map?

  Map是接口,Java 集合框架中一部分,用于存储键值对,HashMap是用哈希算法实现Map的类。HashMap是异步的,线程不安全,允许有null键值。

 

HashMap基于hashing原理,我们通过put(key,value)和get(key)方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象(HashMap是在bucket中储存键对象和值对象,作为Map.Entry)。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

 

(1)首先equals()和hashcode()这两个方法都是从object类中继承过来的。

equals()是对两个对象的地址值进行的比较(即比较引用是否相同)。

hashCode()是一个本地方法,它的实现是根据本地机器相关的。计算出对象实例的哈希码,并返回哈希码,又称为散列函数。hashCode()方法的计算依赖于对象实例的D(内存地址),故每个Object对象的hashCode都是唯一的;当然,当对象所对应的类重写了hashCode()方法时,结果就截然不同了。之所以有hashCode方法,是因为在批量的对象比较中,hashCode要比equals来得快,很多集合都用到了hashCode,比如HashTable。

(2)两个对象,如果equals()相等,hashCode()一定相等;两个对象,如果hashCode()相等,equals()不一定相等(Hash散列值有冲突的情况,虽然概率很低)。

    可以考虑在集合中,判断两个对象是否相等的规则是:
    第一步,如果hashCode()相等,则查看第二步,否则不相等;
    第二步,查看equals()是否相等,如果相等,则两obj相等,否则还是不相等。

(3)覆盖equals()要先覆盖hashCode(),否则在应用HashMap时会出现各种错误。

 

  • 当两个不同的键对象的hashcode相同时会发生什么? 

它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

 

  • 怎样使HashMap同步?

  (1)HashMap可以通过Map map = Collections.synchronizedMap(hashMap)来达到同步的效果。

       (2)ConcurrentHashMap

    (详细的ConcurrentHashMap源码解析:http://www.importnew.com/22007.html

 

  4、HashMap与HashTable有什么区别?对比Hashtable VS HashMap

  两者都是用key-value方式获取数据。Hashtable是原始集合类之一(也称作遗留类)。HashMap作为新集合框架的一部分在Java2的1.2版本中加入。它们之间有一下区别:

  ● HashMap和Hashtable大致是等同的,除了非同步和空值(HashMap允许null值作为key和value,而Hashtable不可以)。

  ● HashMap不是同步的,而Hashtable是同步的。

       ● 因为线程安全、哈希效率的问题,HashMap效率比HashTable的要高。

       ● HashTable有contains()方法,在HashMap中只有containsKey()和containsValue()来检测哈希表中是否有某个key或者value,有则返回true。

  ● 迭代HashMap采用快速失败机制,而Hashtable不是,所以这是设计的考虑点。

       ● HashMap没法保证映射的顺序一直不变,但是作为HashMap的子类LinkedHashMap,如果想要预知的顺序迭代(默认按照插入顺序),你可以很轻易的置换为HashMap,如果使用Hashtable就没那么容易了。

 

HashTable和ConcurrentHashMap都是hash表,都是同步的,不同点在于:

HashTable里使用的是synchronized关键字来确保同步,这其实是对对象加锁,锁住的都是对象整体。意味着在一个时间点只能有一个线程可以修改哈希表,任何线程在执行hashtable的更新操作前需要获取对象锁,其他线程等待锁的释放。当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间,效率低下。

ConcurrentHashMap算是对上述问题的优化,它引入了一个“分段锁”的概念来实现高并发。在ConcurrentHashMap中,就是把HashMap分成了N(默认为16)个Segment,诸如get,put,remove等常用操作只锁当前需要用到的桶,根据key.hashCode()算出放到哪个Segment中。在put的时候需要锁住该Segment,get时候不加锁,使用volatile来保证可见性,只有在求size等全局操作时才可能需要锁定整个表。

试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制),并发性的提升是显而易见的,因此ConcurrentHashMap的效率远高于HashTable。

 

  • 什么时候使用Hashtable,什么时候使用HashMap

  基本的不同点是Hashtable同步HashMap不是的,所以无论什么时候有多个线程访问相同实例的可能时,就应该使用Hashtable,反之使用HashMap。非线程安全的数据结构能带来更好的性能。如果在将来有一种可能—你需要按顺序获得键值对的方案时,HashMap是一个很好的选择,因为有HashMap的一个子类 LinkedHashMap。所以如果你想可预测的按顺序迭代(默认按插入的顺序),你可以很方便用LinkedHashMap替换HashMap。反观要是使用的Hashtable就没那么简单了。同时如果有多个线程访问HashMap,Collections.synchronizedMap()可以代替,总的来说HashMap更灵活。

 

  5、什么叫做快速失败特性

  从高级别层次来说快速失败是一个系统或软件对于其故障做出的响应。一个快速失败系统设计用来即时报告可能会导致失败的任何故障情况,它通常用来停止正常的操作而不是尝试继续做可能有缺陷的工作。当有问题发生时,快速失败系统即时可见地发错错误告警。在Java中,快速失败与iterators有关。如果一个iterator在集合对象上创建了,其它线程欲“结构化”的修改该集合对象,并发修改异常 (ConcurrentModificationException) 抛出。

 

  6、为什么Vector类认为是废弃的或者是非官方地不推荐使用?或者说为什么我们应该一直使用ArrayList而不是Vector

  你应该使用ArrayList而不是Vector是因为默认情况下你是非同步访问的,Vector同步了每个方法,你几乎从不要那样做,通常有想要同步的是整个操作序列。同步单个的操作也不安全(如果你迭代一个Vector,你还是要加锁,以避免其它线程在同一时刻改变集合).而且效率更慢。当然同样有锁的开销即使你不需要,这是个很糟糕的方法在默认情况下同步访问。你可以一直使用Collections.sychronizedList来装饰一个集合。

  事实上Vector结合了“可变数组”的集合和同步每个操作的实现。这是另外一个设计上的缺陷。Vector还有些遗留的方法在枚举和元素获取的方法,这些方法不同于List接口,如果这些方法在代码中程序员更趋向于想用它。尽管枚举速度更快,但是他们不能检查如果集合在迭代的时候修改了,这样将导致问题。尽管以上诸多原因,Oracle也从没宣称过要废弃Vector。

 
残留任务:HashSet、HashMap、ConcurrentHashMap等源码理解
 
参考链接:
集合框架介绍和总结:http://blog.sina.com.cn/s/blog_a345a8960101k9vx.html
实现类对比来源:http://www.cnblogs.com/leeplogs/p/5891861.html
集合方法介绍和部分面试题:http://blog.csdn.net/zsm653983/article/details/7562324
其他文中超链接
 
posted @ 2017-06-19 17:36  IUbanana  阅读(927)  评论(0编辑  收藏  举报