1、“拖泥带水”的删除

测试代码:

package com.demo;
import java.util.ArrayList;
public class TestArrayList {
public static void main(String[] args) {
// TODO Auto-generated method stub
ArrayList<String> arylist = new ArrayList<String>();
arylist.add("aa");
arylist.add("bb");
arylist.add("bb");
arylist.add("cc");
// arylist.add("aa");
// arylist.add("bb");
// arylist.add("cc");
// arylist.add("bb");
String target = "bb";
for (int i = 0; i < arylist.size(); i++) {
if (arylist.get(i).equals(target)) {
arylist.remove(i);
}
}
for(String value:arylist){
System.out.println(value);
}
}
}

这段代码的本意在于删除动态数组arylist中元素等于target的元素,通过遍历数组的每一项,当equals判定相等就删除之。如果数组中的所有元素唯一,不会存在任何问题,对应的元素得到正确的删除;如果数组中元素有重复,那么你可能理所当然地认为所有重复元素也应该全部被删除,因为似乎遍历到了每项元素。但实际结果有可能不是你想的样子。运行上述代码得到输出:

aa
bb
cc

有一个元素"bb"应该被删除但是未被删除。

但如果在调整一下动态数组中重复元素“bb”的位置(如注释的代码)为:{"aa","bb","cc","bb"},其结果是两个“bb”全部被删除。

这里的重点在于remove()方法的实现。查看其源码:

    public E remove(int index) {
rangeCheck(index); modCount++;
E oldValue = elementData(index); int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work return oldValue;
}

从以上删除的处理逻辑可以看出,删除操作主要是调用了System.arraycopy()方法,在System类中该方法的定义为:

    public static native void arraycopy(Object src,  int  srcPos,
Object dest, int destPos,
int length);

该方法的功能是将源数组src从srcPos位置开始,复制数量为length的元素到目标数组dest中,其中目标数组从destPos位置开始往后填充。

由此可知,remove方法中删除操作的处理办法是将elementData数组从index+1位置开始其后的numMoved各元素原地复制到index处。简单点说就是将index+1位置开始的元素依次往前挪动一个位置,因为index位置的元素反正要被删除,就直接覆盖它,最后在处理elementData最末尾无用的元素和更新size。

这样处理的问题在于,如果arylist中位置为i和i+1的元素重复且刚好是需要删除的元素,那么由于一轮删除之后,第二个元素占据第一个元素的位置,但是i++自增了,第二个元素被跳过了,未被遍历到,删除实际上是不完整的。实际上只要待删除元素存在相邻的情况都会出现这种“拖泥带水”不完整的删除。

remove(Object o)、fastRemove(int index)都是基于类似的实现方式。

正确的写法可以在必要的时候手动修正计数器i的值,但是这种实现实际上不是很优雅:

        for (int i = 0; i < arylist.size(); i++) {
if (arylist.get(i).equals(target)) {
arylist.remove(i);
i--;
}
}

更好的实现是反向遍历删除:

        for (int i = arylist.size()-1; i >= 0; i--) {
if (arylist.get(i).equals(target)) {
arylist.remove(i);
}
}

2、不一样的初始化

ArrayList是基于对象数组实现的。

在版本JDK1.8中

其中几个常用的成员变量有:

默认的初始化容量:DEFAULT_CAPACITY

空的对象数组变量两个:EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA

真正存储元素的对象数组变量:elementData

大小:size

/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] 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.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; // non-private to simplify nested class access private int size;

源码的实现者特地用两个空的对象数组来区别有参和无参实例化,以满足不同条件下的使用。

对应的构造器如下:

    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);
}
} /**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

因此,通过如下两种方式实例化后,对象数组实际上都是一个空数组,但是是不同的空数组。

        ArrayList<String> list = new ArrayList<String>(0);
ArrayList<String> list1 = new ArrayList<String>();

第一种实例化得到的空数组是EMPTY_ELEMENTDATA,第二种实例化得到的空数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

但是随后的第一次add元素后就会有所区别。

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

add()方法调用ensureCapacityInternal()方法。

    private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
} ensureExplicitCapacity(minCapacity);
} private void ensureExplicitCapacity(int minCapacity) {
modCount++; // overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

ensureCapacityInternal()方法中特地有区别的对待了两个同为空的对象数组:

如果对象数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那么第一次add元素时分配的容量直接从10起步,随后一直到size为10为止,add元素不会在调用grow()方法。

如果对象数组是EMPTY_ELEMENTDATA,那么第一次add元素时分配的容量是1,并且随后的几次add都会比较频繁的调用grow(),这样的增幅视乎太小了,但是仅限于前面几次add操作,到后面阶段于第一种方式基本保持一直的步调。

为何要在这里对如此细微的细节给予差别对待?我其实也不是很懂,但是大体上,猜测作者是想传递这样一个实现意图:

如果程序员用ArrayList<String> list = new ArrayList<String>(0);来实例化,那么他应该是很明确的需要空的动态数组而不需要存储任何元素,即使需要存储元素,也是一两个很少量的元素,不会频繁的add,因此不应该在第一次add操作时分配10的容量,很可能造成浪费。

如果程序员 ArrayList<String> list1 = new ArrayList<String>();来实例化,那么他是需要这个动态数组来存放元素的,因此应该在第一次add是为其分配合适的容量10。

完结~~~

ArrayList源码中的两个值得注意的问题的更多相关文章

  1. ArrayList源码中EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别

    2018年7月22日09:54:17 JDK 1.8.0_162 ArrayList源码中EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别 ...

  2. Java入门系列之集合ArrayList源码分析(七)

    前言 上一节我们通过排队类实现了类似ArrayList基本功能,当然还有很多欠缺考虑,只是为了我们学习集合而准备来着,本节我们来看看ArrayList源码中对于常用操作方法是如何进行的,请往下看. A ...

  3. ArrayList源码解析--值得深读

    ArrayList源码解析 基于jdk1.8 ArrayList的定义 类注释 允许put null值,会自动扩容: size isEmpty.get.set.add等方法时间复杂度是O(1): 是非 ...

  4. Java中的容器(集合)之ArrayList源码解析

    1.ArrayList源码解析 源码解析: 如下源码来自JDK8(如需查看ArrayList扩容源码解析请跳转至<Java中的容器(集合)>第十条):. package java.util ...

  5. JAVA常用集合源码解析系列-ArrayList源码解析(基于JDK8)

    文章系作者原创,如有转载请注明出处,如有雷同,那就雷同吧~(who care!) 一.写在前面 这是源码分析计划的第一篇,博主准备把一些常用的集合源码过一遍,比如:ArrayList.HashMap及 ...

  6. ArrayList源码分析超详细

    ArrayList源码分析超详解 想要分析下源码是件好事,但是如何去进行分析呢?以我的例子来说,我进行源码分析的过程如下几步: 找到类:利用 IDEA 找到所需要分析的类(ztrl+N查找ArraLi ...

  7. 面试必备:ArrayList源码解析(JDK8)

    面试必备:ArrayList源码解析(JDK8) https://blog.csdn.net/zxt0601/article/details/77281231 概述很久没有写博客了,准确的说17年以来 ...

  8. ArrayList源码分析超详细(转载)

    ArrayList源码分析超详细   ArrayList源码分析超详解 想要分析下源码是件好事,但是如何去进行分析呢?以我的例子来说,我进行源码分析的过程如下几步: 找到类:利用 IDEA 找到所需要 ...

  9. Java ArrayList源码分析(有助于理解数据结构)

    arraylist源码分析 1.数组介绍 数组是数据结构中很基本的结构,很多编程语言都内置数组,类似于数据结构中的线性表 在java中当创建数组时会在内存中划分出一块连续的内存,然后当有数据进入的时候 ...

随机推荐

  1. CSS的引入方式及CSS选择器

    一 CSS介绍 现在的互联网前端分三层: a.HTML:超文本标记语言.从语义的角度描述页面结构. b.CSS:层叠样式表.从审美的角度负责页面样式. c.JS:JavaScript .从交互的角度描 ...

  2. 我的Java之旅 第一课 开发环境准备

    1.JDK JDK(Java Development Kit) 是 Java 语言的软件开发工具包(SDK). SE(J2SE),standard edition,标准版,是我们通常用的一个版本,从J ...

  3. 性能测试 Apache参数配置与性能调优

    Apache性能调优 by:授客 QQ:1033553122 环境: Apache 2.4 1.选择合适的MPM(Multi -Processing Modules, 多处理模块) Unix/Linu ...

  4. 安卓开发_浅谈ListView之分页列表

    前言: 在开发的过程中,有时候我们需要从网络解析一些数据,比如最近的一些新闻,我们需要把这些数据用ListView显示出来. 因为是解析一个网络数据源,这样将会一下子将所有的数据解析出来,当数据源数据 ...

  5. 安卓开发_浅谈ListView(自定义适配器)

    ListView作为一个实际开发中使用率非常高的视图,一般的系统自带的适配器都无法满足开发中的需求,这时候就需要开发人员来自定义适配器使得ListView能够有一个不错的显示效果 有这样一个Demo ...

  6. css文本属性用法总结

    稍稍总结了下css文本的一些属性用法,自己忘记的时候也可以用来查查,不用去查网站那么麻烦. 下面是部分总结,也希望对其他人有用 文本修饰 (1)text-decoration:  文本修饰(横线) 4 ...

  7. 记一次nginx php配置的心路历程

    1.本来搞好了php的配置,想把目录下移一层 从 www.abc.com 变成 www.abc.com/wxapi ,由于我的真实文件目录比路由少了一层public 尝试了很多办法都不行 甚至想到了u ...

  8. php中编码转换方法

    php里经常用到编码转换,在这记录一个常用的编码转换方法,字符串.数组.对象都可以使用,使用了递归来解决,比较普通 /* * php中编码转换 * @param $param 需要转换的数据 * @p ...

  9. 【PAT】B1044 火星数字(20 分)

    /* 火星文有两位,第二位为0不输出 */ #include<stdio.h> #include<algorithm> #include<string.h> #in ...

  10. linux 平均负载 load average 的含义【转】

    文章来源: linux 平均负载 load average 的含义 load average 的含义 平均负载(load average)是指系统的运行队列的平均利用率,也可以认为是可运行进程的平均数 ...