关于Java的Collection

Posted fntp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Java的Collection相关的知识,希望对你有一定的参考价值。

Collection的相关摸索

一个对象中可以存放多个类型不同的数据内容

问题是:如果需要很多个对象?

(1)第一中解决方法:对象数组

(2)第二种解决方法:集合 是一个容器 可以存放多个类型不同的对象

在Java中使用集合:Collection集合与Map集合

集合的contains方法的底层原理

我们都知道在集合中,调用contains方法可以实现查找集合中是否含有待定元素,如果调用contains返回的结果是true那么证明集合中含有该元素,如果调用方法后返回的是false那么证明集合中没有该元素。而contains的底层原理其实是调用了Objects类中的equals方法,objects的equals(o,e)方法,其中o代表contains方法的形式参数, e代表集合中的每个元素也就是contains的工作原理就是拿着参数对象与集合中已有的元素依次进行比较,比较的方式调用objects中的equaLs方法而该方法equals的工作原理如下:

public static boolean equals(object a, object b){
return (a == b) || (a != null && a.equals(b));     //其中a代表Person对象,b代表集合中已有的对象
    //值得注意的是,此时调用的equals方法默认是调用来自Object类的比较方法,如果目标类自己改写了equals方法那么调用之后的结果是按照改写后的equals方法来比较的,如果没有改写,那么最终调用的就是父类Object类的equals方法,比较的就仍旧是地址值而非内容。
}

根据以上代码要想返回值为true有两种不同的实现途径:

(1)a与b这两个对象不为空,他们的地址相同,可判断为true。

(2)a不为空,并且通过a与b类型的内部成员方法Equals方法的比较得出两个参照标准的参照值一致,可判断为true。

集合Collection的Add方法与AddAll方法的区别

AddAll是为了将参数对象的每一个元素都添加到调用对象的集合中,而这个被调用对象就是一个集合,参数对象也是一个集合。参数对象是一个准备好的集合,调用者需要是一个Collection类型的对象。以下代码为测试AddAll方法的调用之后的结果:结果显示,AddAll方法可以将Collection集合中的所有元素都添加进来。相比于AddAll方法,add方法则是为Collection集合添加单个的对象Object类型的对象。

package com.sinsy.Collection;
import java.util.ArrayList;
import java.util.Collection;
/**
 * @author Fntp
 */
public class ListTest {
    public static void main(String[] args) {
     addAllTest();
    }
    //检验Collection的AddAll方法
    public static void addAllTest(){
        Collection<String> list1 = new ArrayList<>();
        list1.add("scx");
        list1.add("gd");
        Collection<String> list2 = new ArrayList<>();
        list2.addAll(list1);
        System.out.println("list1:"+list1);
        System.out.println("list2:"+list2);
    }
 }

以上代码执行的结果为:

image-20210523192205740

针对于addAll方法与add方法,常见的笔试题考点是在于对方法实现的了解:

如下代码:

//检验对Collection的Add方法的理解:
public static void addTest(){
    Collection<Object> list1 = new ArrayList<>();
    list1.add("scx");
    list1.add("gd");
    Collection<Object> list2 = new ArrayList<>();
    list2.add(list1);
    System.out.println("list2含有list1的所有元素吗?答案是:"+list2.containsAll(list1));
    System.out.println("list2集合中含有list1这个整体做为元素吗?答案是:"+list2.contains(list1));
}

执行结果为:

image-20210523192257699

在使用Collection集合时需要注意,使用Collection集合的addAll方法时候是将做为方法参数的集合对象的每一个元素都添加进如调用该方法的集合对象,而add方法则是将作为参数的集合对象直接封装成一个元素可供调用方法的Collection对象直接添加进入,由于这里的Collection集合的元素类型是Object类型的,因此支持添加Collection类型的元素,因为Collection类型隶属于Object类,是Object的子类,因此调用方法的时候,会将整个Collection对象装在进入调用方法的集合对象,这样一来,在后续检测Collection的containsAll方法的时候容易出错,因为Collection的contains方法是比较该方法的调用者的Collection对象是否包含目标参数中的每一个元素,而上文是直接将整个集合装载进入的因此次数必定为False。而如果调用的事contains方法则必定返回true。

数组到集合的转换

数组到集合的转换使用的是JavaArrays类中的API方法:

image-20210524095910788

Iterator接口的使用

(1)什么是迭代?

上一个任务与下一个任务时间存在关联。

(2)Collection接口继承自Iterator接口

(3)开发中常用的三种输出方法

第一种便是toString方法,第二种是通过迭代器的方式进行的,第三种是通过foreach的方式进行的

关于List集合的ArrayList集合

ArrayList的Add方法的底层实现:

(1)第一步:确定底层所使用的数据结构

(2)第二步 :ArrayList底层的使用原理与扩容机制

首先第一步,ArrayList再使用无参构造方法之后创建了一个ArrayList对象,一个简单的ArrayList集合。

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

进入ArrayList的无参数构造方法,其中elementData这个成员变量使用了一个常量进行了初始化,这个变量是一个数组的缓冲区一维数组,负责将我们添加的所有元素保存起来。这个常量是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 * 共享空数组实例用于默认大小的空实例,我们将此与EMPTY_ELEMENTDATA区分开来,以了解何时应充气多少添加了第一个元素。
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

以上代码注释已经将为什么需要DEFAULTCAPACITY的原因写的很详尽了:

将此与EMPTY_ELEMENTDATA区分开来,以了解何时应充气多少添加了第一个元素。

image-20210524141910765

我们可以看的很清晰,经过赋值,这时候虽然ArrayList的底层数组不再是null了但是长度仍然为0;

我们通过给ArrayList的创建以及添加元素的代码打上断点调试我们可以很清晰的发现:

image-20210524142130965

image-20210524142116760

这里出现了端倪,我们在摸索底层源码时,意外发现,在ArrayList进行创建对象初始化的时候,竟然附带了一个size成员变量,这个size成员变量是什么呢?

image-20210524142305785

我们通过进一步的溯源我们发现了ArrayList的成员变量的四个重要组成:

  • EMPTY_ELEMENTDATA
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • elementData
  • size

而代码中对size变量的解释是:The size of the ArrayList (the number of elements it contains).ArrayList的大小(它包含的元素数)。

这样我们就很清晰了,ArrayList中,创建ArrayList对象在初始化的过程中,首先将本地的Object类型的数组elementData进行初始化赋值,赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这么做是为了一是为了区别于EMPTY_ELEMENTDATA,二是为了凸显添加元素后的外部表象,当数组添加完元素后就可以通过elementData进行表现,这也就间接得知ArrayList的对象在创建的时候并没有及时的申请内存空间而是简单的赋值,其底层同样是一个数组,创建并初始化对象只是在更改一个对象的地址引用,并没有对ArrayList底层的数组做实质性的改变。而紧随其后的就是size,接下来,初始化完毕后,进行add操作的时候,轮到了size上场。

image-20210524142422385

add方法中,首先第一步便是对modCount进行了自增处理:

将指定的元素追加到此列表的末尾。
参数:是一个泛型对象
返回:是一个True(由Collection.add指定)

image-20210524143430907

add方法中的modCount是干什么用的呢?我们可以来看一下JDK文档对modCount的描述:

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.
This field is used by the iterator and list iterator implementation returned by the iterator and listIterator methods. If the value of this field changes unexpectedly, the iterator (or list iterator) will throw a ConcurrentModificationException in response to the next, remove, previous, set or add operations. This provides fail-fast behavior, rather than non-deterministic behavior in the face of concurrent modification during iteration.
Use of this field by subclasses is optional. If a subclass wishes to provide fail-fast iterators (and list iterators), then it merely has to increment this field in its add(int, E) and remove(int) methods (and any other methods that it overrides that result in structural modifications to the list). A single call to add(int, E) or remove(int) must add no more than one to this field, or the iterators (and list iterators) will throw bogus ConcurrentModificationExceptions. If an implementation does not wish to provide fail-fast iterators, this field may be ignored.

如何理解呢?大致的意思就是:

Java的ArrayList使用了modCount来标记调用者对List进行结构化修改的次数。
结构性修改是指更改List大小或以其他方式来修改了List,使得进行中的迭代可能会产生不正确的结果。
该字段由迭代器和listIterator方法返回的迭代器和List迭代器实现使用。
如果该字段的值意外更改,迭代器(或List迭代器)将抛出ConcurrentModificationException以响应下一次、删除、上一次、设置或添加操作。
这提供了快速失败行为,而不是在迭代期间面对并发修改时的不确定行为。
子类使用此字段是可选的。
如果一个子类希望提供快速失效迭代器(和列表迭代器),那么它只需在其add(int,E)和Remove(Int)方法(以及它覆盖的导致列表结构修改的任何其他方法)中递增此字段。
对add(int,E)或remove(Int)的一次调用必须向该字段添加不超过一个,否则迭代器(和列表迭代器)将抛出虚假的ConcurrentModificationExceptions。
如果实现不希望提供快速失效迭代器,则可以忽略该字段。

其实什么意思呢?我的理解就是确保操作的原子性,确保对list的操作不受其他因素的干扰:如何去证明这一点?其实也很简单:

写一个测试用例来对此种情况进行简单验证:

package com.sinsy.Collection;
import com.sinsy.bean.Person;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
/**
 * @author fntp
 */
public class CollectionPrint {
    public static void main(String[] args) {
        Collection c1 = new ArrayList();
        c1.add("one");
        c1.add(2);
        c1.add(new Person("scx","男",18));
        Iterator iterator1 =c1.iterator();
        while (iterator1.hasNext()) {
            if ("one".equals(iterator1.next())) {
//                iterator1.remove();
                c1.remove("one"); //ConcurrentModificationException 并发修改异常
            }
        }
        System.out.println(c1);
        for (Object obj : c1){
            System.out.println(obj);
        }
    }
}

程序输出的结果证实了我们对modCount这个变量作用的猜想:

image-20210524144305015

这也就意味着,有modCount的存在,ArrayList的每一次涉及对底层原始数组的修改的操作终究都会被记录下来,如果影响了数据的记录,比如线程不安全等问题,越级处理等问题他会直接抛出异常终止程序的执行。

了解完Add中的modCount之后,我们在继续看,add方法

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

这里其实是调用了另一个add方法我们继续跟进去看:

/**
 * This helper method split out from add(E) to keep method
 * bytecode size under 35 (the -XX:MaxInlineSize default value),
 * which helps when add(E) is called in a C1-compiled loop.
 */
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

这个方法内部的核心则是调用了grow方法,这个方法的含义是:增长,那么也就是将数组进行增长,前面我们知道了elementData在初始化的时候是一个空数组,然后进行add操作,操作的核心也就是将elementData进行增长,这样一来只用看看grow方法是如何实现的就可以了:

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 * @throws OutOfMemoryError if minCapacity is less than zero
 */
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth 右移动一位 */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

我们发现,grow方法是通过Arrays.copyOf(elementData, newCapacity);的核心操作来进行修改的,使用了Arrays的copyof来生产新的数组对象并最后返回出去。而且这里做了一个巧妙的判断:

以下有两种不同的情况:

(1)如果待处理的ArrayList的底层数组也就是elementData数组不是空的,他的长度不为0,或者将elementData与DEFAULTCAPACITY_EMPTY_ELEMENTDATA的地址进行匹配判断,如果不一致则说明elementData内部含有元素,那么则进行新生产数组,容量扩展,内容复制,最后返回。在JDK11的时候,他的底层扩容其实是1.5倍的elementData数组容量。

(2)如果一开始elementData就是空的那么根据传入的参数与DEFAULT_CAPACITY进行比较来决定创建一个指定容量的数组,最后将创建完成的数组进行返回。由下图我们可以看出来,其实,这里的DEFAULT_CAPACITY就是一个常量,就是10,那么最后,由初始状态零个元素到最后添加一个元素的结果就是创建了一个容量为10的数组并将元素存放进入数组中。

image-20210524163617484

image-20210524163902554
未完待续…

以上是关于关于Java的Collection的主要内容,如果未能解决你的问题,请参考以下文章

关于Java的Collection

如何在片段中使用 GetJsonFromUrlTask​​.java

Java学习关于集合框架的基础接口--Collection接口

关于java中的Iterator:

关于java Map和Collection接口

java 里的 Collection接口有啥作用