美文网首页
Java梳理之理解集合(一)

Java梳理之理解集合(一)

作者: _小二_ | 来源:发表于2017-12-08 12:41 被阅读0次

    在开发中,除了基本数据类型和String类型,还有集合类为我们带来了很大的便利,所以在这里梳理一下相关的内容。从字面就可以简单的理解集合类的作用,即数据集,或者说容器。能力有限,有误请指出。

    Java为我们提供了丰富的集合类,常见的就有ArrayListHashSetHashMapTreeMap等,如下所示即为简单的集合关系图:

    集合类结构图.png

    在上图中,个人认为可以为集合类部分分成5个大块,即以Collection为中心的集合块,Iterator为中心的迭代器,Map为中心的键值对,Comparable/Comparator比较工具,以处理集合类的Collections/Arrays的工具类。从简单的Iterator迭代器开始看。

    Iterator接口

    在我们的代码中经常可以看到迭代器,其本身只是一个接口,内部拥有三个方法next()hasNext()remove()来进行相应的操作,使用示例如下所示:

    /**
    **Iterator迭代器示例
    **/
    public static void main(String[] arg0){
            LinkedList<String> list = new LinkedList<String>();
            list.add("test1");
            list.add("test2");
            list.add("test3");
            Iterator<String>  iterator = list.iterator();
            while(iterator.hasNext()){
                System.out.println(iterator.next());
                iterator.remove();
            }
        }
    输出:
    test1
    test2
    test3
    

    因为这个接口本身并没有实现类,所以可以通过Collection接口中的iterator()方法转化为迭代器,然后再调用相应方法。其中的remove()方法删除的是刚刚next()的对象,如果在调用next()方法之前使用remove()方法,则会报错IllegalStateException
    当然,在Iterator接口下,还存在ListIterator子接口,从名字就看的出来,这是个List的迭代器,其中内置了些对List处理的操作,如hasPrevious()方法,对应于hasNext()previous()方法对应于next()方法,当然,List还有下标indexListIterator也有nextIndex()previousIndex()来获取下标;ListIterator还有set()add()方法来处理。如下所示:

    /**
    **ListIterator迭代器示例
    **/
    public static void main(String[] arg0){
            LinkedList<String> list = new LinkedList<String>();
            list.add("test1");
            list.add("test2");
            list.add("test3");
            ListIterator<String>  iterator = list.listIterator(0);
            iterator.add("test4");
            while(iterator.hasNext()){
                System.out.println("next: index="+iterator.nextIndex()+",obj="+iterator.next());
            //  iterator.remove();
            }
        }
    输出:
    next: index=1,obj=test1
    next: index=2,obj=test2
    next: index=3,obj=test3
    

    在上面可以看到,index是从1开始打印的,说明每调用一次add()remove()previous()next()都会使相应的参数index移动,故而才会出现这种情况。

    Comparable & Comparator接口

    从名字上“比较”就可以看出来,这两个接口是用来做什么的,对于在集合中的元素,并不是都像List一样有顺序的,但是通过使用这个两个接口就可以用Collections.sortArrays.sort让集合中存储的对象变成有序集合。有两个这样的接口,自然是因为这两个接口之间是有区别的,查看源码如下:

    /**
    **Comparator & Comparable接口源码
    **/
    public interface Comparator {
        public abstract int compare(Object obj, Object obj1);
        public abstract boolean equals(Object obj);
    }
    public interface Comparable {
        public abstract int compareTo(Object obj);
    }
    

    可以看到,这两个接口中Comparator接口拥有两个方法,其中一个为equals(obj),这处理的是两个对象相等的问题,但是由于Object对象默认就有的equals方法所以可以不用显示实现,不显示实现时使用默认的equals方法。这两个接口比较排序的功能都是其中的comparecompareTo方法来完成,如下所示:

    /**
    **Comparable使用示例
    **/
    public static void main(String[] arg0){
            List<Person> list = new ArrayList<Person>();
            list.add(new Person("test3",3));
            list.add(new Person("test1",1));
            list.add(new Person("test2",2));
            System.out.println("--before---");
            for(Person p:list){
                System.out.println(p.toString());
            }
            System.out.println("--after---");
            Collections.sort(list);
            for(Person p:list){
                System.out.println(p.toString());
            }
        }
    输出:
    --before---
    Person [name=test3, age=3]
    Person [name=test1, age=1]
    Person [name=test2, age=2]
    --after---
    Person [name=test1, age=1]
    Person [name=test2, age=2]
    Person [name=test3, age=3]
    

    在这里的Person实现了Comparable接口,表示的就是这个类可排序,故而可以通过集合的方法来实现排序功能,当然,对于Comparator方法也是这样,它使用的场景是在于这个原始类并不能被修改的情况下,直接使用Comparator接口来比较两个类型,在Collections中存在sort方法的这种使用方式,这里就不放示例了。

    Map集合

    查看结构图可知Map下拥有个接口SortedMap和抽象类AbstractMap,其中SortedMap下还有子接口NavigableMapjava.util下的map集合主要围绕这个抽象类和接口进行,其中存在常见的HashMapLinkedHashMapTreeMap,也有不怎么用的IdentityHashMapEnumMap。在Map接口的源代码中存在一个静态的内部接口Entry,其中就包含了几个方法如getKey(),getValue(),setValue(Obj),equals()hashCode(),这个内部接口在子类的源码中肯定是属于核心。下面从抽象类HashMap开始:

    HashMap

    查看源码可知,这个类继承了AbstractMap类,并实现了Map,Cloneable, Serializable三个接口,这些接口中也没有很特殊的,所以可以从AbstractMap中入手.在类AbstractMap中,提供了两个Map.Entry接口的内部实现,其中SimpleImmutableEntry类并不能对Value进行设置;同时,可以看到AbstractMap类拥有两个字段Set类型的keySetCollectionvalues,需要注意的是其中返回Set的方法entrySet(),在这个抽象类中并没有它的实现,但是大体上也可以清楚它是一个返回Map.Entry集合的方法。
    跳出这个抽象类,回到这个类HashMap本身,由源码可知,在HashMap类中存在8个静态内部类,看起来可能很多,但是简单的划分一下就可以得出跟Iterator相关的就有5个类,跟Set相关的则有3个,说明内部结构只用了这2个,横向来看可以看到这里有EntryKeyValue,说明HashMap内部会有关于这三部分的内容,在这里查看一下Entry类,可以发现它有如下属性:

    /**
    **HashMap.Entry 属性
    **/
    final Object key; //键
    Object value; //值
    Entry next; //下一个节点
    final int hash; //hash值
    

    可以看到,其中一个很重要的属性是Entry类型的next字段,说明这是个链表结构的类型。切回HashMap类查看其属性可知:

    /**
    **HashMap的属性
    **/
    static final int DEFAULT_INITIAL_CAPACITY = 16; //默认初始化容量大小
    static final int MAXIMUM_CAPACITY = 1073741824; //最大容量值
    static final float DEFAULT_LOAD_FACTOR = 0.75F; //默认的加载因子
    transient Entry table[]; //数据存储
    transient int size; //长度
    int threshold; //变长的临界值
    final float loadFactor; //加载因子
    volatile transient int modCount; //修改次数
    private transient Set entrySet; //Entry的set集合
    

    根据如上的属性字段,查看HashMap的构造器就会有所发现,如下所示:

    /**
    **HashMap的某一构造器
    **/
    public HashMap(int i, float f) {
            entrySet = null;
            if (i < 0)
                throw new IllegalArgumentException(
                        (new StringBuilder()).append("Illegal initial capacity: ").append(i).toString());
            if (i > 1073741824)
                i = 1073741824;
            if (f <= 0.0F || Float.isNaN(f))
                throw new IllegalArgumentException(
                        (new StringBuilder()).append("Illegal load factor: ").append(f).toString());
            int j;
            for (j = 1; j < i; j <<= 1)
                ;
            loadFactor = f;
            threshold = (int) ((float) j * f);
            table = new Entry[j];
            init();
        }
        ...
    

    在第一个构造器中就可以发现HashMap构造时,就会初始化它的属性,其中包括loadFactorthreshold,table,并将entrySet置空,这样对于一个普通的HashMap就产生了。
    上面这些理解后,在看下其中的put(key,val)方法,源码如下:

    /**
    **HashMap源码 put()
    **/
    public Object put(Object key, Object val) {
            if (key == null)
                return putForNullKey(val);
            int i = hash(key.hashCode());
            int j = indexFor(i, table.length);
            for (Entry entry = table[j]; entry != null; entry = entry.next) {
                Object obj2;
                if (entry.hash == i && ((obj2 = entry.key) == key || key.equals(obj2))) {
                    Object obj3 = entry.value;
                    entry.value = val;
                    entry.recordAccess(this);
                    return obj3;
                }
            }
    
            modCount++;
            addEntry(i, key, val, j);
            return null;
        }
    void addEntry(int i, Object key, Object val, int j) {
            Entry entry = table[j];
            table[j] = new Entry(i, key, val, entry);
            if (size++ >= threshold)
                resize(2 * table.length);
        }
    

    在如上的代码中,首先判断key值是否为null,如果为null那么就将val放入entry数组下标为0null对应值中;如果key不为null,那么就将通过hash(hashcode)得到一个数i,再通过indexFor()iEntry的数组长度进行&运算,获取这个值在这个Entry数组的下标值,然后遍历这个下标下的链表,找出是否已经存在这样一个key满足他们的hashcodekey都相等,如果存在,更新这个链表中Entry的值,不存在则调用后面的addEntry()方法放入这个键值对。

    通过这个方法,就可以知道整个hashMap的结构是内部存在一个数组用来存放链表,而在链表中才是真正存放数据的地方。而对于数组来说,需要结合实际情况来构造适当长度的数组来满足性能上的需求,因为链表结构上就不适合做循环遍历。理解了如上过程,那么hashMap就没有多少秘密可言了,其他的方法大体上也是这样一个操作过程。还有就是刚开始说的那几个SetIterator内部类,实现的就是entrySet或者keySet之类的功能,并没有很特别的地方。

    TreeMap

    查看TreeMap源码可知,这个类继承了抽象类AbstractMap同时实现了NavigableMapCloneableSerializable这三个接口,其中NavigableMap属于SortedMap的子接口。作为一个有序的Map规则,SortedMap定义了一些方法如:comparator()subMap()headMap()firstKey()等操作边界的方法,在它的子接口NavigableMap中则多添加了一些以某个有序key为标准的操纵方法,如lowerEntry(key)返回小于且最靠近这个keyEntry
    查看源码可知TreeMap拥有13个内部类,其中有8个内部类和HashMap类似,在这里可以看一下最重要的一个内部类Entry,从field属性中就可以看出一二,源码如下:

    /**
    **TreeMap.Entry 字段field
    **/
    Object key; //键
    Object value;  //值
    Entry left;  //左子节点
    Entry right;  //右子节点
    Entry parent;  //父节点
    boolean color;  //颜色
    

    可以看到,这个Entry和之前HashMap中的Entry不同,在这里它是一个树形结构,拥有左子节点left,右子节点right,父节点parent与颜色color,看到这里,大概就知道它是使用的红黑树来写的,所以在代码中不可避免的会使用到红黑树的一些操作,如左旋、右旋、着色,如果感兴趣的可以去继续了解红黑色
    知道了内部结构之后,可以继续查看TreeMap几个重要的属性,如下所示:

    /**
    **TreeMap 源码 field
    **/
    private final Comparator comparator; //比较器
    private transient Entry root;   //根节点
    private transient int size;  //容器大小
    private transient int modCount;     //结构修改的次数
    private static final boolean RED = false;
    private static final boolean BLACK = true;
    

    其中的comparator用来比较存放的实体大小,查看TreeMap的构造器,如下所示:

    /**
    **TreeMap的构造器
    **/
    public TreeMap() {
            root = null;
            size = 0;
            modCount = 0;
            entrySet = null;
            navigableKeySet = null;
            descendingMap = null;
            comparator = null;
    }
    

    在这个构造器中,并不会给comparator传值,但是也能正常使用TreeMap,查看到这里,就可以看一下重要的方法put(key,val),源码如下所示:

    /**
    ** TreeMap put(key,val)源码 
    **/
    public Object put(Object key, Object val) {
            Entry entry = root;
            if (entry == null) {
                root = new Entry(key, val, null);
                size = 1;
                modCount++;
                return null;
            }
            Comparator comparator1 = comparator;
            int i;
            Entry entry1;
            if (comparator1 != null) {
                do {
                    entry1 = entry;
                    i = comparator1.compare(key, entry.key);
                    if (i < 0)
                        entry = entry.left;
                    else if (i > 0)
                        entry = entry.right;
                    else
                        return entry.setValue(val);
                } while (entry != null);
            } else {
                if (key == null)
                    throw new NullPointerException();
                Comparable comparable = (Comparable) key;
                do {
                    entry1 = entry;
                    i = comparable.compareTo(entry.key);
                    if (i < 0)
                        entry = entry.left;
                    else if (i > 0)
                        entry = entry.right;
                    else
                        return entry.setValue(val);
                } while (entry != null);
            }
            Entry entry2 = new Entry(key, val, entry1);
            if (i < 0)
                entry1.left = entry2;
            else
                entry1.right = entry2;
            fixAfterInsertion(entry2);
            size++;
            modCount++;
            return null;
        }
    

    这部分代码看起来一大串,其实并不复杂,主要逻辑如下:1.将root根节点赋值给变量entry,判断这个entry是否为null,如果为entrynull,则将这个存储的数据放在根节点上;如果entry不为null,则判断comparator是否为null,如果comparator为空,则将key强转为Comparable,然后调用comparaTo方法,而对于不为空的情况,就可以直接对比两个对象了,这两个只是对比的方式不同,在循环对比后就是给这个对象安家落户,到这里已经完成大部分的代码了,但是这并没有完成;既然属于红黑树,那么就会对可能造成失去平衡的情况进行纠正,也就是之前说的需要进行左旋右旋着色等操作让这个二叉树变平衡,即fixAfterInsertion(entry2)。可以想到的是,为了保持平衡,在插入数据的时候会进行修复操作,那么删除的时候,肯定也是会进行操作的,也就是说,当原有的树结构发生变化的时候就会修复一次。

    到这里Map就大体的梳理了一下,其实梳理的并不完整,在TreeMap这部分原本打算把红黑树也放进去,但是关于数据结构的部分,觉得还是专门放在数据结构部分写吧,明年会找个时间总结一下。越到年底越忙,争取在12月结束之前完成梳理基础系列的内容。最近越来越觉得自己太菜了,,能力有限,如果有误,请多多指教。

    参考
    Java程序设计语言
    http://blog.csdn.net/chenssy/article/details/26668941

    相关文章

      网友评论

          本文标题:Java梳理之理解集合(一)

          本文链接:https://www.haomeiwen.com/subject/olleixtx.html