源码分析-java-AbstractCollection

来源:互联网 发布:淘宝鞋店的名字大全 编辑:程序博客网 时间:2024/06/05 21:15

AbstractCollection

概述

概述首先AbstractCollection是java自己提供的一个最基本的Collection的实现。当然它依然是一个抽象类。

对于一个不可更改的集合,只要继承这个类并且实现迭代器和size()方法就行。

对于一个可更改的集合,需要实现add和返回Iterator的方法,当然可选的实现remove方法

通常应该提供两个构造器,一个无参的,一个是包含集合元素的

public Object[] toArray()

API文档

这个方法返回一个包含集合内所有元素的数组。这个数组的顺序必须是与根据迭代器产生的是一致的。这样就可以根据迭代器的实现来确定顺序。

这个方法是安全的,也就是说这个方法会自己产生一个新的数组,并且返回的这个数组必须是可以被调用者修改

这个方法是基于数组和基于集合的API之间的桥梁

这个方法的实现还确保了即使在迭代器期间修改了集合。比如说size的返回的大小不对还是会根据迭代器迭代到的元素的数量得到正确的数组。

源码分析

总的来说实现算是比较简单的。关键在于如何实现size返回不正确的时候的数组生成。这里使用了两个内部的静态函数

private static <T> T[] finishToArray(T[] r, Iterator<?> it)private static int hugeCapacity(int minCapacity)

首先对于toArray源码:

    public Object[] toArray() {        // Estimate size of array; be prepared to see more or fewer elements        Object[] r = new Object[size()];        Iterator<E> it = iterator();        for (int i = 0; i < r.length; i++) {            if (! it.hasNext()) // fewer elements than expected                return Arrays.copyOf(r, i);            r[i] = it.next();        }        return it.hasNext() ? finishToArray(r, it) : r;    }

思路上说:

  1. 先根据size()的大小生成一个数组,然后根据迭代器的hasNext来判断,当前迭代器后面还有没有值,并用i记录当前的已经存入的元素数量。
  2. 如果迭代器实际迭代的数量的大小比size小则使用Arrays.copyOf来截断当前的数组,然后直接返回。
  3. 需要说明的是Arrays的静态方法是java针对数组来实现的一系列方法。其中copyOf将传入的数组复制到一个新的数组中,新的数组的大小由第二个参数来指定。
  4. 但是如果size的大小小于迭代器迭代的元素。则调用finishToArray来完成之后迭代器的实现。

这里有几个有意思的地方

  • 首先一个是使用三目运算来调整返回值,比使用if方法显得简洁的多。我之前很少用这样的方法来实现返回值。需要说明的是if和三目运算的一个重大的区别是三目运算是必须要返回值的。
  • 二个有意思的地方,迭代顺序是根据具体的迭代器的实现来确定的,这就给了编程人员比较大的自由,比如说可以在这个基础上在写一个由迭代器参数的toArray方法。这样就可以从集合中间写迭代器
  • 第三个有意思的地方是这个返回的数组是Object类数组。也就说在使用的时候可能需要强制类型转换。
  • 第四个有意思的地方使我很好奇,这个方法应该是没有办法保证集合中间不产生的空隙的,比如说集合中的元素是这样的[1,null,2]这样的话,迭代器是否可以正确的实现呢?是添加一个null到数组还是只填入1而忽略后面的。不过这个类是个抽象类,没办法直接实例化,具体还是等看到后面的可实例化的类在说吧。

然后是第一个静态方法:

    @SuppressWarnings("unchecked")    private static <T> T[] finishToArray(T[] r, Iterator<?> it) {        int i = r.length;        while (it.hasNext()) {            int cap = r.length;            if (i == cap) {                int newCap = cap + (cap >> 1) + 1;                // overflow-conscious code                if (newCap - MAX_ARRAY_SIZE > 0)                    newCap = hugeCapacity(cap + 1);                r = Arrays.copyOf(r, newCap);            }            r[i++] = (T)it.next();        }        // trim if overallocated        return (i == r.length) ? r : Arrays.copyOf(r, i);    }

从思路上说

  1. 这个方法保存两个int。一个表示当前的迭代的元素的数量,总的来说i每次进入循环体就增加一个,而另一个cap表示当前的数组的大小。
  2. 每次进入循环体,先用cap确认下数组的长度。如果i还没有增长到cap相同的程度则只要将迭代器的下一个元素放入数组并将i加一就行
  3. 如果i==cap的时候说明需要增大cap。这里增加的cap的一半并加1的大小,然后判断下新的数组的大小是否超过VM的最大界限,如果没有就将继续循环体。
  4. 在返回的时候使用根据已经迭代的元素i来复制当前数组到一个新的数组中。
  5. 这里如果判断newCap大小超过了最大的数组大小则调动hugeCapacity调整大小。这里需要说明在AbstractCollection类中间声明了一个静态私有常量MAX_ARRAY_SIZE数值为Integer.MAX_VALUE - 8。按照源码里的说明是虚拟机通常会放几个头信息在数组中。

看几个有意思的地方:

  • 首先有意思的是这里返回的是T类型的数组,而toArray实现使用的是Object数组。不知道是不是历史原因或者有其他特殊的考量?
  • 其次为什么是cap=cap+cap>>1+1。加一当然好解释,防止cap等于2,但是为何不是使用ca<<1+1或者类似这种,这种似乎是更常用的扩展数组的方式,是否是因为通常size不会比迭代器迭代的元素小太多。
  • 数组的大小的极限也是个有意思的地方。

最后一个是用于确定确定数组极限的值:

    private static int hugeCapacity(int minCapacity) {        if (minCapacity < 0) // overflow            throw new OutOfMemoryError                ("Required array size too large");        return (minCapacity > MAX_ARRAY_SIZE) ?            Integer.MAX_VALUE :            MAX_ARRAY_SIZE;    }

思路:

  1. 如果当前的cap最大值不可在继续增大,即cap+1为负值则抛出一个超出内存大小的异常。
  2. 如果当前的值可以分配,则分配一个MAX_ARRAY_SIZE和MAX_VALUE 中的满足条件的值。

看几个有意思的地方:

  • 这里解释了为了输入参数为cap+1,因为需要考虑cap是否可以在分配,
  • 使用个MAX_ARRAY_SIZE和MAX_VALUE 中的满足条件的值,应该是为了适应不同虚拟机的条件,当然也可能有其他理由,但是目前我还想不到其他理由这样做。

public <T> T[] toArray(T[] a)

这个方法将对象先试图填充到给定的数组a中,如果a的数组不够用了,则新建一个新数组来保存集合中的元素

    @SuppressWarnings("unchecked")    public <T> T[] toArray(T[] a) {        // Estimate size of array; be prepared to see more or fewer elements        int size = size();        T[] r = a.length >= size ? a :                  (T[])java.lang.reflect.Array                  .newInstance(a.getClass().getComponentType(), size);        Iterator<E> it = iterator();        for (int i = 0; i < r.length; i++) {            if (! it.hasNext()) { // fewer elements than expected                if (a == r) {                    r[i] = null; // null-terminate                } else if (a.length < i) {                    return Arrays.copyOf(r, i);                } else {                    System.arraycopy(r, 0, a, 0, i);                    if (a.length > i) {                        a[i] = null;                    }                }                return a;            }            r[i] = (T)it.next();        }        // more elements than expected        return it.hasNext() ? finishToArray(r, it) : r;    }

思路:

  1. 先考虑到了当前的size的大小和传入的数组的大小。哪个大使用那个作为初始的引用。这里为了实现创建T类型的数组,使用了反射中的方法,根据传入的参数来得到元素类型,根据size得到数组大小。
  2. 就这样不断将迭代器的元素放入数组,直到迭代器迭代到尾,或者数组大小用完。
  3. 如果2是因为迭代器迭代到尾,需要分情况讨论,r引用的就是a,则让a数组i位标后面的值全部为null。
  4. 如果3中r引用不是a。而是新的数组说明是根据size新建的数组。如果a的长度小于i,说明这些值没办法放入到a中,则需要返回自建建立的数组而没办法返回原先的参数a,所有只要对r进行截断后返回就可以了,
  5. 如果4中的情况是a>i,这说明了传入的参数a是可以放入足够的参数的,这里将r中的所有元素复制到a中,并进行截断然后返回。对于a.中大于迭代器数量的的元素全部换成null。
  6. 对2中的情况则依照上一小节的内容继续进行即可

一个有意思的地方使这里的其实需要比较3个量,实际的迭代器迭代数量,传入的参数a的大小,以及size返回的大小。当然根据这里具体的实现来说,作者认为应该先判断size和传入参数的大小的哪个大,选用大的数组,当然这样的方式如果当实际迭代器迭代<传入参数a的长度< size的时候这样是多做了一个数组,但是综合考虑可能是一种较优的方案。

contains

这个就比较简单了:
需要注意的一点是这个类需要判断参数是否是null。并分情况讨论。

    public boolean contains(Object o) {        Iterator<E> it = iterator();        if (o==null) {            while (it.hasNext())                if (it.next()==null)                    return true;        } else {            while (it.hasNext())                if (o.equals(it.next()))                    return true;        }        return false;    }

其实这种策略只是一个简单的代表
不仅仅是contains对于
remove也需要一样的策略,需要确定是不是null。

    public boolean remove(Object o) {        Iterator<E> it = iterator();        if (o==null) {            while (it.hasNext()) {                if (it.next()==null) {                    it.remove();                    return true;                }            }        } else {            while (it.hasNext()) {                if (o.equals(it.next())) {                    it.remove();                    return true;                }            }        }        return false;    }

剩下的方法没有需要特别注意的地方。就省略了。

1 0