日常小结-java-AbstractCollection

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;
    }

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

时间: 2024-11-01 22:25:54

日常小结-java-AbstractCollection的相关文章

equals小结 java

一.什么是equals方法 Equals方法是Object类中(所有的类都有equals这个方法)提供定义对象是否相等的逻辑. 方法如下: public boolean equals(Object obj) { return (this == obj); } 调用方法:x.equals(y):当x和y是同一个对象的应用时,返回true,否则返回false. 二.与"=="的关系? 在java中用"=="就可以比较两个对象是否相等,为什么还要用equals方法呢?对于

数据类型与运算符小结(JAVA)

初步学习了Java的数据类型和运算符,小结一下! 四种变量 1.属性(定义在类里)实例变量2.静态属性(定义在类里且有static)类变量3.局部变量(定义在方法里)4.参数 定义变量 数据类型 变量名1.直接加分号(未初始化)2.=value; 变量命名(规则) 1.必须以字母,下划线_或美元符$开头,汉字可以,但不建议2.之后的部分可以是字母,下划线,美元符以及数字3.变量名长度可以无限长4.变量名不可以是java关键字eg:static,public,final ,this,new ,tr

Enum 枚举小结 java **** 最爱那水货

1 import java.util.HashMap; 2 import java.util.Map; 3 4 /** 5 * 收单行 大写首字母 和对应的编码<br/> 6 * 7 * ABC 农业银行<br/> 8 BC 中国银行<br/> 9 CBC 建设银行<br/> 10 CITIC 中信银行<br/> 11 CMBC 招商银行<br/> 12 HSBC 汇丰银行<br/> 13 ICBC 工商银行<br/

浅谈并小结java内存泄漏

一.定义 首先什么是内存泄漏,简单点说就是用完了忘了回收,而其他对象等资源想用却没法用的一种"站着茅坑不拉屎"的浪费资源的情况.在C/C++中,多数泄漏的场景就是程序离开某一运行域时,如在某个方法体中new出的对象或者malloc出的结构体等,并且只有该方法体中的局部变量指向这块内存区域时,在该方法返回时,存在栈中的局部变量指针随着栈帧被一起销毁,那么就没有任何指针可以指向该内存区域了,那么这块内存区域便是泄漏了. 而java的内存泄漏呢?众所周知,java的内存回收是由gc管理的.g

日常小结-java-AbstractList-Itr和ListItr的实现

AbstractList API文档 AbstractList实现了List接口,又因为List继承自Collection,Collection继承自Iterable.因此List接口包含很多的方法. AbstractList实现了List接口的最小实现. 他是针对随机访问储存数据的方式的,如果需要使用顺序访问储存数据方式的List还有一个AbstractSequentialList它是继承自AbstractList的子类,顺序访问时应该优先使用它. 对于不可修改的list,只需要覆盖get和s

hibernate日常小结和优化

1.对于类之间是依赖不是关联关系的类之间的数据库设计,最好采用精粒度对象模型,建立组件模型,不是采用多对一什么的关联.

C语言 日常小结

1.当数组当作函数参数的时候会退化为指针 #include<stdio.h> #include<stdlib.h> void sort(int a[]){ int num = sizeof(a); printf("数组的大小num=%d\n", num);//打印4,此时a是一个指针 //打印数组 for (int i = 0; i < 10; i++) { printf("%5d", a[i]); } } void main(){ i

ansible日常小结

一.ansible优化 vim /etc/ansible/ansible.cfg host_key_checking = False #不进行验证 log_path = /var/log/ansible.log #打开日志 基于ssh vim /etc/ssh/sshd_config UseDNS no #重启sshd # systemctl restart sshd 二.常用模块 1.coyp 模块 ansible date -m copy -a 'src=/etc/selinux/confi

java枚举小结

在百度百科上是这样介绍枚举的: 在C#或C++,java等一些计算机编程语言中,枚举类型是一种基本数据类型而不是构造数据类型,而在C语言等计算机编程语言中,它是一种构造数据类型.枚举类型用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型. 而在java中,枚举扩大了这一概念,因为在java中,枚举已经称为一个类,因此完全具有类的特性. 我们都知道枚举是JDK1.5才推出的新概念,那么在此之前,我们如果想使用一些固定的常量集合,比如性别(2个),季节(4个),星期(7个)