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. FormData对象的使用

    一.概述 FormData类型是XMLHttpRequest 2级定义的,它是为序列化表以及创建与表单格式相同的数据提供便利. 作用:1.利用一些键值对来模拟一系列表单控件:即将form中的所有表单元 ...

  2. CSS效果:CSS select样式优化 含jquery代码

    CSS 下拉选择菜单基本的CSS样式不怎么好看,通过一些简单的样式优化,可以得到如下图这样的: html结构如下: <div class="sel_wrap"> < ...

  3. 【代码笔记】Web-ionic-select

    一,效果图. 二,代码. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...

  4. Java字符串占位符(commons-text)替换(转载)

    Java字符串占位符(commons-text)替换 https://blog.csdn.net/varyall/article/details/83651798 <dependency> ...

  5. 程序员Web面试之JSON

    JSON是什么? JSON(JavaScript对象表示法), 是在网络通信下,常用的一种数据表达格式,它有助于我们于一个自描述的,独立的和轻的方式呈现并交换数据.这些数据可以易于和转换为JavaSc ...

  6. plt 数据可视化

    1.plt.plot(x,y,color) 折线坐标图 import matplotlib.pyplot as plt h = np.linspace(1, 10, 10) v = np.linspa ...

  7. 生产者与消费者+Queue(线程安全)

    from queue import Queue from lxml import etree import requests from urllib import request from threa ...

  8. MySQL崩溃恢复与组提交

      Ⅰ.binlog与redo的一致性(原子) 由内部分布式事务保证 我们先来了解下,当一个commit敲下后,内部会发生什么? 步骤 操作 step1 InnoDB做prepare redo log ...

  9. sql server 转置 和实现随机分配和一串代码的含义拼在一行

    1.sql server 转置很容易搜到方法,一般需要手动写转置的列项,如果多时会比较烦,下面试了省事的方法: --案例需求数据 ----方法一:if object_id('tempdb.dbo.#s ...

  10. Linux小技巧之:两种方法统计访问web网站的前10个IP

    获得访问前10位的IP地址: 10.46.170.7 218.202.70.147 77.72.83.87 5.39.217.107 185.197.74.234 185.197.74.231 第二种 ...