<Java> 集合框架概览

前言

    集合框架就是提供一个存放东西的对象或叫容器,再说易懂一点就像是个数据库,提供了对数据的增删改查等功能,Java对集合框架有非常好的支持。都放在java.util里面,核心接口有Collection、Set、List、Map、SortedMap、SortedSet等,常用实现类有ArrayList、HashMap、HashSet等。

List

基本介绍 :

    List接口是Collection的一个子接口,中文名可叫"列表"或"清单",它的顺序和长度是固定的。数组结构和链表结构都是列表的一种特殊形式。

常用实现类:

    ArrayList:它是一个基于数组的结构,里面的节点相互之间没有特别的联系,默认的大小是10,最大大小Integer.MAX_VALUE - 8。当大小不够时会自动增长,它可以通过get,set来直接获取、修改某一个节点数据。

    LinkedList:它是一个基于双向链表的结构,每一个节点都有两个指针来分别指向上一个节点和下一个节点。它是可变长度的。

       这两个实现类的区别在于,ArrayList的get()/set()效率比LinkedList高,而LinkedList的add()/remove()效率比ArrayList高。

       具体来说:数组申请一块连续的内存空间,是在编译期间就确定大小的,运行时期不可动态改变,但为什么ArrayList可以改变大小呢,因为在add如果超过了设定的大小就会创建一个新的更大的(增长率好像是0.5)ArrayList,然后将原来的list复制到新的list中,并将地址指向新的list,旧的list被GC回收,显然这是非常消耗内存而且效率非常低的。但是由于它申请的内存空间是连续的,可以直接通过下标来获取需要的数据,时间复杂度为O(1),而链表则是O(n),

       而链表结构不一样,它可以动态申请内存空间。在需要加入的节点的上一个节点的指针解开并指向新加节点,再将新加节点的指针指向后一个节点即可。速度很快的。

     所以说ArrayList适合查询,LinkedList适合增删。

其他:

    对List排序可以通过辅助工具Collections.sort()来完成,但是要注意对list里面对象的compare方法进行重写。下面是一个Java8下的引用例子。

package com.vogella.java.collections.list;

import java.util.ArrayList;
import java.util.List;

public class ListSorter {
  public static void main(String[] args) {
    System.out.println("Sorting with natural order");
    List<String> l1 = createList();
    l1.sort(null);
    l1.forEach(System.out::println);
    
    System.out.println("Sorting with a lamba expression for the comparison");
    List<String> l2 = createList();
    l2.sort((s1, s2) -> s1.compareToIgnoreCase(s2));  // sort ignoring case
    l2.forEach(System.out::println);
    
    System.out.println("Sorting with a method references");
    List<String> l3 = createList();
    l3.sort(String::compareToIgnoreCase);   
    l3.forEach(System.out::println);
         
  }
  
  private static List<String>  createList() {
    List<String> list = new ArrayList<>();
    list.add("iPhone");
    list.add("Ubuntu");
    list.add("Android");
    list.add("Mac OS X");
    return list;
  }

}

      顺便介绍一下Collections这个类,相信大家做java基础面试题的时候碰到过collection和collections区别的问题,那么现在应该了解了,它们一个是集合框架的上层接口,List和set都是它的子接口,一个是集合框架的辅助类,除排序外,Collections还提供很多其他方法,比如copy(list,list)将一个list拷贝到另外一个list(注意这里和list = list)不一样,前者是在堆里有两个list,虽然内容是一样的,而后者在堆中只有一个list,只是栈中有两个引用指向它。还有reverse(list),显然是将list反向,还有shuffle(list)打乱list的顺序,以及其他很多,所以很多时候需要对list的数据进行操作的时候,不要急着自己抓头挠腮地实现,先去Collections找找有没有已经写好的方法。

Set

基本介绍:

  Set也是Collection的一个子接口。中文名可叫"集合",学过高中数学的都知道,集合是数学上的概念,它有三个特性

  确定性:集合中每一个元素都是确定的。

  无序性:集合中的每个元素的位置都是任意的。

  互异性:集合中每一个元素都不相同。{1,a}那么a就不能等于1;

  那么在计算机领域,或者高级语言里面的集合也是拥有这三种特性。在Java里面就是Set。

  确定性:就不用解释了。(最多有一个null值)

  无序性:说明刚才的那个Collections.sort()对set无效,也从侧面说明了list顺序的重要性。

  互异性:Set接口只继承了Collection的方法,并添加了互异约束,在Set中,是通过equals来判断元素是否相同的,这就要求Set的元素的equals和hashcode两个方法非常重要,有一点要清楚,如果两个set里面的元素是一样的,那么这两个set实例就是相同的。

  

        Set<String> stringSet = new HashSet<>();
        stringSet.add("aaa");
        stringSet.add("bbb");
        stringSet.add("ccc");
        stringSet.add("ddd");
        stringSet.add("eee");
        Set<String> stringSet1 = new TreeSet<>();
        stringSet1.add("aaa");
        stringSet1.add("bbb");
        stringSet1.add("ccc");
        stringSet1.add("ddd");
        stringSet1.add("eee");
        System.out.println(stringSet.equals(stringSet1));    
     /////
     Output>>true

 

常用实现类:

  HashSet:它是一个非常优秀的Set实现类,是基于哈希表存储的,但它不保证返回的Iterator(迭代器)的顺序。

  TreeSet:它是一个基于红黑树存储的,它将Set元素根据值来排序,效率相比HashSet慢了不少。

  LinkedHashSet:它也是一个基于哈希表存储的,但是加上了一个链表,来保存它的元素插入顺序,也就是说循环输出它时,会根据插入的顺序来输出。那么显然的,为了达到这个效果,也是通过牺牲比较大的效率来实现的。

Map

基本介绍

  map就是许多键值对的集合,因为它的原理和集合的数据结构不太一样,所有它不是继承自Collection,而且Map就是最高层接口。map的key不允许重复,而且每个key最多对应一个值。(值得注意的是Map接口取代了老版本的Dictionary抽象类)。Map提供了三个集合来遍历map,分别是keySet()、values()、entrySet(),要注意视图和元素集合副本的不同,前者的效率要高很多。

常用实现类

  HashMap:*

  TreeMap:*

  LinkedHashMap:*

  可以看到这几个Map和上面的Set几个常用类是一样的,为什么?其实在于set其实就是就是用map来实现的,set中的所有值就是map中的key,而所有key对应的value,则是一个空的Object()对象。


  上次map这块说错了,来来看看HashMap的具体实现:

    上面我们说了数组和链表两种数据结构,它们有各自的优缺点,那么如果将两者结合起来的话,性能怎么样呢。其实这就是map的基本概念,它实际上是申请了一块数组类型的存储空间,然后在内部实现了静态内部类Entry,Entry里面有四个重要的属性key、value、next、hash,将Entry放入数组中,这就形成了一个Entry[]。

    

    当我们插入一个key/value的时候,其实内部是先取hash(key),然后将得到int值“取余”数组的长度(这是为了让hash值平均分布),将取余得到的值作为数组的下标,然后指向给定的value。下面三图分别是put方法,hash算法,取下标算法的源码:

      

      

 

    这里有几个问题:

    1.如果key是null怎么办呢?

    看上面put方法的代码,可以知道它会先判断key是不是null值,如果是的话,直接放在数组的第一个位置上--table[0],如下:

    

    2.如果下标计算出来的值是一样的怎么办?

    如果IndexFor(hash(key))计算出来的值一样,会将新的放在index上,然后将next指向旧的那个对象,也就是put方法里面,for循环那段做的工作。这里的next也就是实现链表结构,所以说map是两者的结合。get的时候会先定位数组位置,再对链表依次遍历,直到找到hash相同的对象。

    3.如果第一次申请的数组大小不够怎么办?

    

    自动调用reszie,生成一个新的数组,然后将旧的数组映射过来。要注意的是新表大小改变,会影响hashseed的值,也就会影响hash的值。也就是说新表中Entry的hash值都是改变过的。

    还有一点就是在用containKey和containValue两个方法时,前者是能先确定在数组的位置的,后者要全map遍历,所以两者的效率相差map.size倍,所以如果碰到经常需要用containValue,而且map中存储数量比较大的时候,可以考虑以map的value为key来新建一个map,然后通过containKey来实现同样的效果。

    最后HashMap为什么线程不安全,比如当你一个线程调用get(key)的时候,已经进行到确定某个位置了,但是由于另外一个线程的添加操作使map.resize()了,那么也就是所有Entry的hash值改变了,那么刚才那个线程的旧hash值就get不到value了。

结语

  综上,我们在选用集合框架前,需要考虑的问题有:

  1.什么类型的数据?-->决定用Map还是Collection。

  2.顺序是否重要?-->决定用list还是set。

  3.数据量是否大?-->决定用效率高的还是牺牲了效率的Linked*/Tree*。

  4.是否有线程安全方面的问题(我上面没提到这个)?-->ArrayList是线程不安全的而推荐使用Vector,HashMap是线程不安全的而推荐使用ConcurrentHashMap。

posted @ 2015-04-02 10:38  丶千纸鸢  阅读(336)  评论(0编辑  收藏  举报