浅谈ArrayList

浅谈ArrayList

废话不多说(事实是不会说),让我们直接进入正题
首先讲一讲最基本的ArrayList的初始化,也就是我们常说的构造函数,ArrayList给我们提供了三种构造方式,我们逐个来查看

Arraylist();

无参的构造方法,这种方式的初始化,ArrayList内部会为我们声明一个长度为0的列表,但在我们调用add方法加入一个元素时,它内部会经历add->ensureCapacityInternal->calculateCapacity->ensureExplicitCapacity->grow->调用Arrays.copyOf方法返回新生成的列表(内部最终调用System.arraycopy完成元素的转移)->grow->ensureExplicitCapacity->ensureExplicitCapacity->add方法,该执行流程最终生成一个容量为十(默认值)的列表,并将元素加入到列表中。以下列出部分关键源码

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; 
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
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) { 
        //该处的DEFAULT_CAPCITY=10,minCapcity=1
        return Math.max(DEFAULT_CAPACITY, minCapacity); 
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    //列表结构发生改变的记录值,没发生一次结构改变(扩容、元素增加、元素删除),该值加一
    modCount++;

    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    ...
    elementData = Arrays.copyOf(elementData, newCapacity);
}

也许你会问,既然有初始默认容量值,为什么不在调用构造函数的时候就初始化一个默认长度的列表呢?
个人意见:我认为在存在一种情况,就是仅声明一个ArrayList的列表对象,但是后面并未使用,在这种情况下,如果在声明是就初始化一个长度为10的列表,会造成空间的浪费,而且在真正使用列表时(即调用add方法),才初始化列表,有一种懒加载的思想在其中,避免了耗资源操作的集中,但也并非所有的构造方法中都不会初始化列表容量,在ArrayList(int capacity)构造方法中,只要输入的初始容量为正整数,那么就会在构造函数中就定义出指定大小的列表对象

ArrayList(int capacity);

该方式的初始化在执行构造函数的时候传入了一个初始容量值,不过要求这个初始容量值必须为正整数,而且如果为0的话等同于无参的构造方法,假定初始化容量为5,那么在调用add方法时,经历的方法依次为add->ensureCapacityInternal->calculateCapacity->ensureExplicitCapacity->add,下面我们来看看部分关键源码:

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        //按指定容量初始化列表,此时elementData.length==initialCapacity
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //传入值等于0,初始化一个容量为0的初始列表
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        //传入值为不符合要求,报错
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}
private static final Object[] EMPTY_ELEMENTDATA = {};
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //不满足条件,不会进入该分支
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //直接返回最小容量,即列表当前元素值+1(给新增元素的空间)
    return minCapacity;
}

ArrayList(Collection<? extends E> c);

该方式的初始化需要传入Collection的子类对象,并根据该对象来初始化ArrayList

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        //如果不是Object[]类型的列表,转化为Object[]类型
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

到这里对ArrayList的三个构造函数做了简单的介绍

在继续进行ArrayList中方法的介绍之前,需要注意ArrayList不是线程安全的,所以如果涉及并发操作,建议在初始化的时候就调用Collections.synchronizedList(list)来将线程不安全的列表对象转化为线程安全的对象,内部的实现原理:使用代理模式在原来的方法执行之前嵌套了一个同步机制,这里摘取其中的size()方法,源码如下:

public int size() {
    synchronized (mutex) {  //同步代码块
        return c.size();    //c为被代理对象
    }
}

因为给所有的方法加上锁会降低代码的执行效率,而且有些方法是不需要加锁的,如果不想对所有的方法都加锁,可以在需要加锁的特定方法调用之前手动的做同步处理
下面进入ArrayList中的部分方法介绍,首先介绍从List父接口中继承过来的方法,其中使用到的[]代表对应参数可以存在也可以不存在,不过存在与不存在构成了不同的重载方法:

add([int index,] E element);

该方法用于向列表中添加元素,存在单参方法和双参两个重载方法,单参直接往列表末尾添加新元素,双参方法则可以手动指定插入到列表的哪个索引位置。每当列表元素的个数(size)超过列表容量(elementData.length)时,会触发grow方法(扩容方法),每次在老容量的基础上增加一半的容量,然后通过Arrays.copyOf方法进行新列表的创建和拷贝原列表内容

//在grow方法中关于新列表容量定义的关键代码
int newCapacity = oldCapacity + (oldCapacity >> 1);

addAll([int index,] Collection<? extends E> c);

该方法用于将另一个集合中的所有元素加入到当前列表中,该方法也存在单参和双参两个重载方法,单参方法把所有新元素添加到列表末尾,双参方法则在指定索引位置开始插入所有元素,扩容方法的触发和过程同add

set(int index, E element);

该方法用于替换列表中指定索引位置的值,内部会先进行索引范围检查,返回值为被替换下的老元素

isEmpty();

该方法用于判断列表中是否存在元素,返回值是一个布尔类型,源码的返回值为size == 0

get(int index);

该方法用于获取列表中指定位置的值并返回

remove(int index);

该方法用于删除列表中特定索引位置的值,返回值为被删除的老元素

remove(Object o);

该方法用于删除列表中第一个为特定元素的值,可以传入null,代表删除列表中第一个为null的元素,返回值为布尔类型

clear();

该方法用于清空列表中的所有元素,内部对elementData列表对象的所有元素置空,并记录modCount(列表结构记录变量)值,最后将size置0

removeAll(Collection<?> c);

该方法用于删除集合中存在于传入列表中的所有指定元素,要注意的是,该方法为全列表查找删除,不同于remove方法只删除第一次出现的位置

retainAll(Collection<?> c);

该方法用于跟removeAll的作用刚好互补,用于保留传入集合中包含的所有指定元素,即删除指定集合之外的所有元素

size();

该方法返回列表中元素的个数(size),但该值并不代表列表当前的容量(elementData.length)

indexOf(Object o);

该方法用于查询列表中特定元素出现的第一个索引位置,如果未找到则返回-1

lastIndexOf(Object o);

该方法用于查询列表中特定元素出现的最后一个下标位置,如果未找到则返回-1

contains(Object o);

该方法用于判断列表中是否存在指定的元素,返回值为布尔类型,源码的返回值为indexOf(o) >= 0

sort(Comparator<? super E> c);

该方法用于对列表元素按传入的指定排序规则进行排序,以下使用匿名内部类并结合Lambda表达式来作为示例构造一个二级排序规则,可根据需要增加更多层级的排序规则:

list.sort((o1,o2) -> {
    int result = -(o1.getClick() - o2.getClick());// 数值类型的比较写法,点击量降序
    if (result == 0) {// 如果点击量相同,进入二级排序
        result = o1.getDate().compareTo(o2.getDate());// 字符串类型的比较写法,时间升序
    }
    return result;// 返回比较结果
});

subList(int fromIndex,int toIndex);

该方法用于返回一个可操作性的子列表,不过要注意的是,子列表只不过是添加了偏移量的父列表,所以两个列表的对象是一致的,对于子列表中的操作,源码中都是在添加上对应的偏移量之后直接对父列表做对应修改,所以,对子列表的操作实际上就是对父列表的操作

iterator();

该方法用于返回当前列表的一个普通迭代器,提供了hasNext,next,remove和forEachRemaining方法,其中hasNext用于判断是否存在下一个迭代对象,next用于将cursor指向下一个待操作元素,并返回当前元素(由内部的lastRet索引值进行指定),remove用于删除当前元素(该方法不可连续调用多次,因为内部的lastRet索引在一次操作后会被置为-1,可以会在下一次next时重新指向当前元素),forEachRemaining用于对列表未迭代对象执行传入的钩子方法
注意:如果在迭代器迭代对象时使用迭代器外部的remove或add方法改变了列表结构(新增或删除元素),会导致继续遍历列表时抛出ConcurrentModificationException异常,使用迭代器内部的remove方法改变列表结构不会抛出此异常

listIterator([int index]);

该方法用于返回一个加强版的迭代器对象,由于内部继承了iterator的类,所以可以提供iterator的所有功能,除此之外,还提供了自己独有的功能和特性,首先在初始化时就可以指定迭代初始索引,其次还提供了hasPrevious,nextIndex,previousIndex,previous,set和add方法,其中的previous方法可以实现列表的逆序遍历,set可以替换列表的当前迭代对象,add可以在当前迭代位置添加新的列表元素
注意:如果在迭代器迭代对象时使用迭代器外部的remove或add方法改变了列表结构(新增或删除元素),会导致继续遍历列表时抛出ConcurrentModificationException异常,使用迭代器内部的remove和add方法改变列表结构不会抛出此异常

spliterator();

该方法用于切割列表元素,可用于并发编程时多线程操作列表,需要先进行切割,方法内部默认对半分,再对切割完成的列表进行并行处理,示例代码如下:

ist list = new ArrayList();
list.add(1);list.add(2);list.add(3);list.add(4);list.add(5);
list.add(6);list.add(7);list.add(8);list.add(9);list.add(0);
//对列表进行切割
Spliterator<Integer> a = list.spliterator();//将列表转化为可拆分列表
Spliterator<Integer> b = a.trySplit();//将可拆分的a列表对半分
Spliterator<Integer> c = a.trySplit();//继续将可拆分的a列表对半分
Spliterator<Integer> d = b.trySplit();//将对半分得到的b列表对半分
//对切割好的列表进行操作
a.forEachRemaining(x -> System.out.print(x + " "));
System.out.println();
b.forEachRemaining(x -> System.out.print(x + " "));
System.out.println();
c.forEachRemaining(x -> System.out.print(x + " "));
System.out.println();
d.forEachRemaining(x -> System.out.print(x + " "));

打印结果为:

8 9 0 
3 4 5 
6 7 
1 2 

toArray(T[] a);

该方法用于将列表转化为数组,可以使用参数来指定生成数组的数据类型,返回值为指定类型的数组,以下为代码示例:

String[] strs=list.toArray(new String[0]);// list.toArray(new Object[0]);等同于list.toArray();

下面的方法是ArrayList中特有的方法,如果在你的代码中调用不到,请检查下声明该ArrayList对象的时候对象声明部分是否为ArrayList喔~

clone();

该方法继承自Cloneable接口父类,用于创建并返回一个浅克隆的列表对象

ensureCapacity(int minCapacity);

该方法用于检查列表容量是否已经达到瓶颈,继而判断是否需要进行扩容操作

trimToSize();

该方法用于将列表容量调整至列表的元素总数,可以对不需要继续添加新元素的列表使用该操作用于释放部分资源,使用了该方法的列表,在添加新元素时会进行扩容操作,扩容方法依旧是扩容原容量的一半

forEach(Consumer<? super E> action);

用于对列表中的所有元素执行对应的操作,该处使用匿名内部类结合Lambda表达式的方式进行简单演示:

list.forEach(x -> System.out.println(x.getAge()+1));// 如果只是简单的打印,还可以使用list.forEach(System.out::println);

removeIf(Predicate<? super E> filter);

该方法用于移除列表中符合条件的所有元素,该处使用匿名内部类结合Lambda表达式的方式进行简单演示:

list.removeIf(x -> x.getAge() < 18);

如果对你有帮助,点个赞,或者打个赏吧,嘿嘿
整理不易,请尊重博主的劳动成果

原文地址:https://www.cnblogs.com/Mango-Tree/p/12721185.html