Java学习笔记——集合框架(Collection)

最近学了关于集合框架的知识,做一个小结,有些地方略有不足或出现错误的地方敬请指正~

Collection

  • Collection是java.util包下的一个接口,它存放的是单一值,并且是无序,不唯一的对象。
  • 可以存放不同类型的数据,而数组只能存放固定类型的数据。
  • 在进行实例化的时候需要用子类进行实现,例如:
Collection collection = new ArrayList();
  • 注意:其中在Colllection.add()方法中,要求必须传入的参数是Object对象,因此当写入基本数据类型的时候,包含了自动拆箱和自动装箱的过程。

其他还有一些基础的api操作再次就不赘述了。

List

存储一组不唯一,有序(插入顺序) 的对象。

ArrayList

ArrayList实现了长度可变的数组,在内存中分配连续的存储空间,存放的是的对象。如果了解数据结构的话那么就可以知道ArrayList的优点和缺点分别是:

  • 遍历元素和随机访问元素的效率比较高。
  • 当添加和删除元素时需要进行大量的数据移动操作,这样使得增删数据时效率会比较低,并且按照内容查询的效率也比较低。

那么在ArrayList当中有一个经常会被问到的点就是它的扩容机制,下面一起来看一下。

ArrayList的扩容机制

一般想到数组的扩容都是new一个新的数组然后复制元素,但如果要涉及到具体细节的话就需要我们来一起分析一下源码,首先我们先来了解一下源码当中的一些基本的成员变量的定义,方便后面的理解

// 表示了ArrayList的默认容量大小是10
private static final int DEFAULT_CAPACITY = 10;
// 当在进行ArrayList初始化时参数传入0,则赋予一个空的数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认初始化时的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存放元素的数组,也代表了ArrayLiat的实际大小
transient Object[] elementData;
// 当前ArrayList的元素个数
private int size;

接下来看一下ArrayList的构造方法,分别为无参构造函数和带参构造函数。

// 无参构造函数
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
// 当参数为一个集合
public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // 如果是非空集合则进行复制
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 传入空集合则初始化为空
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
// 当参数为自定义容量大小时
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
        	// 传入的自定义容量大小不能为负数
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

好,了解完了一些基本的成员变量定义和构造函数后,我们会发现一个问题,那就是在构造函数中并没有出现DEFAULT_CAPACITY 这个参数,也就是说初始化时ArrayList的大小并没有赋值为10,那这个默认的容量大小究竟在哪里出现并且是如何进行扩容的,接下来我们来看一看ArrayList里面的add()方法吧。

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 此if判断成立的条件有两个
        // 1.当第一次插入元素时初始化数组大小为10
        // 2.当minCapacity(当前数组容量)超出默认容量大小(10)
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 进行扩容时是扩充为原来数组大小的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
        	// 当插入第一个元素时,MinCapacity会传入DEFAULT_CAPACITY
        	// 的值,即为10,在这一布,完成了默认容量大小的初始化
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
        	// 数组容量最大即为Integer的最大值
            newCapacity = hugeCapacity(minCapacity);
        // 将数据复制到扩容后的数组当中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

在上述代码的关键部分给予了注释,很直观的体现了ArrayList是怎样初始化默认大小为10还有怎样进行扩容和数据的转移,以上部分ArrarList的扩容机制就介绍完了,也不是很难,细心过一遍的话还是挺直观的,面试有可能会被问到。

fast-fail

快速失败是面试中经常会被问到的一个问题,那么它是什么呢,我们首先来介绍一下Iterator迭代器。

  • 所有的集合类都默认实现了Iterable的接口,实现此接口意味着具备了增强for循环的能力,也就是for-each

  • 增强for循环本质上使用的也是iterator的功能,iterator也是一个接口

  • 在iterator的方法中,要求返回一个Iterator的接口子类(定义的集合)实例对象(内部类),此接口中包含了hasNext()和next()两个常用方法。

  • 每次在使用此接口(list.iterator())时,就会返回一个iterator对象,并且具有指针可以实现上述两个功能(变成了迭代器)

// 如何利用Iterator进行遍历
ArrayList<Integer> list = new ArrayList<>();
Iterator iterator = list.iterator();
while(iterator.hasNext()){
	System.out.println(iterator.next());
}
  • iterator对象中含有两个指针(cursor、lastRet)。
public boolean hasNext() {
   return cursor != size;
}
public E next() {
   checkForComodification();
   int i = cursor;
   if (i >= size)
   		throw new NoSuchElementException();
   Object[] elementData = ArrayList.this.elementData;
   if (i >= elementData.length)
        throw new ConcurrentModificationException();
   cursor = i + 1;
   return (E) elementData[lastRet = i];
}

可以看出如果在进行一个集合的遍历时,cursor和lastRet两个指针是同步进行更新的。
关键来了: 上述的遍历操作只是进行了简单的打印输出,如果我们要在遍历的过程中对集合中的元素进行添加或删除操作呢,例如:

        ListIterator iterator = list.listIterator();
        while(iterator.hasNext()){
            Object p = iterator.next();
            if(p.equals(3)){
                list.remove(p);
            }
            System.out.println(p);
        }

可以发现,当我们在利用迭代器进行遍历过程中根据判断条件然后利用集合list进行删除时,会报一个错为:java.util.ConcurrentModificationException(并发修改异常),这是为什么呢?

  • 当遍历到指定元素进行删除的时候,lastRet指向的元素会被删除,后边的元素会向前移动,cursor的指针指向的数据就不是原来的数据,发生指针错乱,两个对象同时操作一个集合,所以会报并发修改异常错误。

解决方法:利用iterator.remove()进行删除,原因:

  • 在每次进行删除时,将cursor指针指向lastRet的位置,lastRet指向-1,并在hasNext判断为True后的next操作里再次更新至cursor指针前面
  • 但必须使用list.ListIterator()的方法,该接口可以向前/后遍历,向前遍历时需要进行向后遍历的操作。

上述的错误是通过下列方法进行实现的:

final void checkForComodification() {
   if (modCount != expectedModCount)
   		throw new ConcurrentModificationException();
}

其中,modCount记录了集合的修改次数,对集合内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断 modCount 跟expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 集合,并抛出ConcurrentModificationException异常,这就是fast-fail策略。

Iterator与for-each

For-each循环

  • 增强的for循环,遍历array或Collection的时候相当简便。
  • 无需获得集合和数组的长度,无需使用索引访问元素,无需循环条件。
  • 遍历集合时底层调用Iterator完成操作。

For-each缺陷

  • 数组:
    • 不能方便的访问下标值。
    • 不要在for-each中尝试对变量赋值,只是一个临时变量。
  • 集合:
    • 与使用Iterator相比,不能方便 的删除集合中的内容。

For-each总结:除了简单的遍历并读出其中的内容外,不建议使用增强for。

Vector

  • Vector也是List接口的一个子类实现
  • Vector跟ArrayList一样,底层都是使用数组进行实现的
  • ArrayList是线程不安全的,效率高,Vector是线程安全的效率低
  • ArrayList在进行扩容的时候,是扩容1.5倍,Vector扩容的时候是原来的两倍。
private void grow(int minCapacity) {
      // overflow-conscious code
      int oldCapacity = elementData.length;
      // 注意这个地方与ArrayList不同的是这里是两倍的扩容
      int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                       capacityIncrement : oldCapacity);
      if (newCapacity - minCapacity < 0)
          newCapacity = minCapacity;
      if (newCapacity - MAX_ARRAY_SIZE > 0)
          newCapacity = hugeCapacity(minCapacity);
      elementData = Arrays.copyOf(elementData, newCapacity);
  }

LinkedList

LinkedList采用链表的方式进行存储,其底层是利用双向链表实现的,存放的是的对象,同样如果了解数据结构的话就会发现它与ArrayList就能形成一个很好的优缺点对比:

  • 插入、删除元素时效率比较高(不用进行元素的移动操作)
  • 遍历和随机访问元素的效率低(不能对随机下标进行直接访问,需要从头进行遍历)

Set

Set接口存放的是一组无序,唯一的对象。常用的子类有HashSet与TreeSet

HashSet

采用了Hashtable哈希表存储结构,底层为HashMap,其特点为:

  • 优点:添加速度快,查询速度快,删除速度快。
  • 缺点:无序。
  • LinkedHashSet:采用哈希表存储结构,并采用链表维护次序
    • 有序(添加顺序)。

注意:在计算hashCode时为什么选择31作为乘子?

  • 31N可以被编译器优化为左移5位后减1即31N = (N<<5)-1,有较高的性能。使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!

那么HashSet如何保证元素的唯一性呢?

  • 通过元素的两个方法,hashCode和equals方法来完成
  • 如果元素的HashCode值相同,才会判断equals是否为true
  • 如果元素的hashCode值丌同,丌会调用equals方法

TreeSet

采用二叉树(红黑树的存储结构)

  • 优点:有序(排序后的升序),查询速度比List快(因为是树结构)
  • 缺点:查询速度没有HashSet快(直接计算Hash值)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值