ArrayList源码逐条解析(续)

一家之言 姑妄言之 絮絮叨叨 不足为训

笔者废话:

   这篇文章是ArrayList源码逐条解析续篇。要想读通这篇文章,请一定要仔细阅读ArrayList源码逐条解析这篇文章。
   这里解释一下为什么来个续篇呢?因为:上篇文章篇幅过大,而且这个markdown编辑器因为篇幅问题已经无法正常响应了,所以就需要另起这篇文章进行解析。
   好,我们现在开始吧(>ω<)。

ArrayList的截取方法的解析:

/**
 * @param fromIndex 起始位置
 * @param toIndex 结束位置
 * @param size 元素个数
 */ 
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
    if (fromIndex < 0)
        throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
    if (toIndex > size)
        throw new IndexOutOfBoundsException("toIndex = " + toIndex);
    if (fromIndex > toIndex)
        throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                           ") > toIndex(" + toIndex + ")");
}

   我们在介绍ArrayList的截取方法之前,先介绍这一个方法。这是一个范围检查方法。目的很直接,就是判断我们我们截取元素的起始位置和结束位置是否符合规范。也是代码健壮性的一种体现。那么,我们来看看这个方法是一个什么步骤。
   第一步,先判断我们的起始位置fromIndex是否小于0。很简单,数组位置小于0肯定是不合规的。所以这个时候就抛出了IndexOutOfBoundsException异常,并通知开发者这个起始位置是哪里。
   第二步,然后判断我们的结束位置toindex是否大于元素个数size。这点是因为我们大多数的截取方法都是左闭右开区间,形如这样:[1,5)。所以索引为size处的元素其实也只能取到最后一位。那么这一步就和第一步类似了,检查数组位置是否合规。如果不合规就抛出IndexOutOfBoundsException异常,并通知开发者这个结束位置是哪里。
   第三步,上述判断都合理了还差一步范围是否合理,不可能存在起始位置比结束位置还要大的现象,例如[3,-1)这个区间你怎么取?所以这里还要判断一下起始位置是否大于了结束位置。如果大于了,则抛出IllegalArgumentException异常,并通知开发者哪个起始位置大于了哪个结束位置。
   我们需要注意的是,这个方法被static修饰,也是就代表这个方法是一个类方法。另外,它并没有特定的权限修饰符,也就是默认为default修饰,所以这个方法也只能在与ArrayList类的同一个类内同一个包内进行访问。

/**
 * 返回指定的fromIndex(包含)和toIndex(不包含)之间的列表部分的视图。
 * (如果fromIndex和toIndex相等,则返回的列表为空。)返回的列表由这
 * 个列表支持,因此返回列表中的非结构性更改将反映在这个列表中,反之亦然。
 * 返回的列表支持所有可选的列表操作。这种方法不需要显式的范围操作(数组中
 * 通常存在的排序)。通过传递子列表视图而不是整个列表,任何期望列表的操作
 * 都可以用作范围操作。例如,下面的习惯用法从列表中删除一系列元素:
 * 			list.subList(from, to).clear();
 * 可以为indexOf(Object)和lastIndexOf(Object)构造类似的习惯用法,
 * 而Collections类中的所有算法都可以应用于一个子列表。该方法返回的列表
 * 的语义将变得未定义,如果支持列表(即此列表)除通过返回的列表外,以任何
 * 方式对其进行结构修改。(结构修改是指改变列表的大小,或者以一种正在进行
 * 的迭代可能产生错误结果的方式扰乱列表。)
 * 
 * @param fromIndex 子列表的低端(包括)
 * @param toIndex 子列表的高端(不含)
 * 
 * @throws 如果出现用于非法端点索引值(fromIndex<0||toIndex>size
 * ||fromIndex>toIndex),则抛出IndexOutOfBoundsException异常
 * @throws 如果端点索引无序(fromIndex>toIndex),则抛出
 * IllegalArgumentException异常
 */
public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

   接下来我们就进入了正题,介绍ArrayList的截取方法解析。事实上,我们如果从源码解析的角度来看的话,这个方法的代码行数也就两行。当然,我们确实就解释这两行而已~具体细节,我们如下说。
   首先,它会引用我们的subListRangeCheck(int fromIndex, int toIndex, int size)方法进行索引判断,以确定我们想要获取的列表范围是否合理。
   其次,会创建并返回一个ArrayList类的私有类SubList类,向这个类的有参构造器传入当前对象,偏移量0,开始位置和结束位置。
   那么到此就结束了,并不是不讲实现细节,因为关于这个SubList类我们会挪到另一篇文章里面进行讲解。既然是一个新接触的类,我们就需要给他另开一篇解析。所以不要着急,参考文章即可。
   其实到这里,我们还是说一下这个方法的目的吧。这个方法目的就是为了返回指定范围内,当前ArrayList的自列表,而指定范围左闭右开。
   需要注意的是,获取的这个“子列表”仅仅就是一个“子视图”而已,这意味着,如果我们修改了当前获取的子列表,那么当前的父列表,也就是当前的ArrayList内的元素也会进行修改。这也就是注释中:“返回列表中的非结构性更改将反映在这个列表中”。
   我们来举个例子:

/**
 * 正确示例,放心运行
 */
public static void main(String[] args) {
    ArrayList<Integer> arrayList = Lists.newArrayList(1, 2, 3, 4, 5);
    List subList = arrayList.subList(0, 4);
    System.out.println(subList);
    subList.set(1, 10);
    System.out.println(subList);
    System.out.println(arrayList);
}
/* 
 * 运行结果:
 * [1, 2, 3, 4]
 * [1, 10, 3, 4]
 * [1, 10, 3, 4, 5]
 */

   你会发现我们创建了一个含有5个元素的列表。现在我们获取其中的前四个,也就是左闭右开[0,4)。这个时候我们就得到了子列表subList,我们能确定的是,这个子列表的元素是列表[1, 2, 3, 4]。这样,我们修改其中第二个参数为10,看看什么效果。
   通过上述的结果我们发现,子列表subList变为了列表[1, 10, 3, 4]。最重要的是,我们原有的arrayList内的元素变为了列表[1, 10, 3, 4, 5]。这意味着当我们修改子列表的时候,其父列表也会跟着变动。
   所以说,如果我们采用这种方式获取了子列表,一般来说是不建议进行修改内部元素的。通常情况下,我们拿到固定的数据后仅仅进行非增、删、该这种操作的业务就可以了。

@Override
public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    // 在此阶段,从筛选器谓词抛出的任何异常都将使集合保持不变
    int removeCount = 0;
    final BitSet removeSet = new BitSet(size);
    final int expectedModCount = modCount;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        @SuppressWarnings("unchecked")
        final E element = (E) elementData[i];
        if (filter.test(element)) {
            removeSet.set(i);
            removeCount++;
        }
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    // 将剩余的元素移动到被移除元素所留下的空间上
    final boolean anyToRemove = removeCount > 0;
    if (anyToRemove) {
        final int newSize = size - removeCount;
        for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
            i = removeSet.nextClearBit(i);
            elementData[j] = elementData[i];
        }
        for (int k=newSize; k < size; k++) {
            elementData[k] = null;  // Let gc do its work
        }
        this.size = newSize;
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }
    return anyToRemove;
}

   我们这里介绍一个删除方法,这是一个具有函数式编程特性的方法。其参数是传入一个Predicate,这个单词翻译成谓语,不过我们更喜欢把它翻译成过滤条件。也就是说我们可以自拟定删除条件,符合过滤条件的元素可以被删除。
   其实这个方法本应该在ArrayList源码逐条解析这篇文章的“”模块下进行解析介绍。不过因为其具有函数式编程特性,所以在这里就单独的介绍出来。
   我们先来看一下源码,然后再举一个例子。
   第一步,先来判定我们的过滤条件是否为空。如果为空的话就抛出NullPointerException异常。
   第二步,这里有一个单行注释,也就是从这个步骤往下开始,从过滤条件抛出的任何异常都将使集合保持不变。
   第三步,声明一个计数器removeCount,用来记录删除元素的个数。
   第四步,声明一个BitSet用来存储二进制位类型的对象,命名为removeSet
   第五步,将当前ArrayList的操作值modCount赋予新声明的预期修改值exceptModCount。同时获取当前数组的元素个数,并定义其为final不可修改的状态。
   第六步,开始遍历整个ArrayList元素。我们先说一下这里到底干了什么,这里其实就是拿我们的过滤条件对各个元素进行检测。假如不通过,那就遍历下一个元素。假如通过了,那么就向这个BitSet内传入这个元素的索引并将计数器removeCount进行加1操作。其实我们到时候删除的就是这个BitSet里面记录索引所对应的元素。这里需要注意一点的是,遍历的终止条件是操作值modCount和预期修改值exceptModCount不相等。
   这步可记住了,你没删除元素,你根本就没有删除元素。元素一直都在。只不过是在BitSet里面进行了区分,要删除的标记为true,不删除的标记为false
   第七步,对操作值modCount和预期修改值exceptModCount进行判断,如果不符合预期条件(也就是不相等),就抛出ConcurrentModificationException异常。
   第八步,从这里开始,才是真正的删除元素。说是“删除”,其实就是挪动元素。这里首先会进行一次判断,问你你的计数器removeCount是否大于0。也就是说是否有可删除的元素。这个计数器removeCount是在第六步的时候已经计算好了的。
   第九步,我们通过了判断,再次进行计算。你看,这里面有一步size - removeCount操作。这其实是在计算我们进行删除操作后新数组的元素个数newSize
   第十步,这里是进行了一次遍历,我们先解释一下这个遍历的效果是什么。这里进行遍历其实就是对原始数组中的非删除元素进行保留操作。那么这里我们声明了两个索引值,一个是i,另一个是j。索引i用于标定原始数组,索引j标定新数组。其中遍历的通过条件是原始数组的索引不得大于原始数组的元素个数同时新数组的索引不得大于新计算出来的元素个数
   第十一步,在第十步之后获取removeSet中标记为false的原始数组索引。然后将这些索引对应的元素挪动到新数组中。再次强调的是,这里的操作是一种保留
   第十二步,经过保留完毕后,这时候,我们才开始所谓的“删除”操作。这个时候我们开始遍历新数组,其实位置就是我们新计算的新数组的元素个数newSize。这里还是需要提示一点的是索引是从下标0开始的,所以newSize所对应就是新数组最后一个元素的后一位,从这一位开始,之后的元素都不再要。
   第十三步,既然都不要,那么这个新数组打从newSize这个位置开始,其后的所有元素全部置为null这样,整个删除步骤就完毕了
   第十四步,开始一些扫尾工作,重新更新当前ArrayList数组的元素个数,将newSize赋予size。然后判断操作值modCount和预期修改值exceptModCount是否不相等。最重要的则是判断通过之后,对操作值modCount进行加1操作。
   最后,返回anyToRemove来告知调用者是否进行了指定过滤条件的删除操作。而这个值则是由我们第九步removeCount > 0计算出来的。
   这里我们来例举一个示例:

/**
 * 正确示例,放心运行
 */
public static void main(String[] args) {
    ArrayList<Integer> arrayList = Lists.newArrayList(1, 10, 2, 3, 5);
    arrayList.removeIf(s -> s > 3);
    System.out.println(arrayList);
}
/*
 * 运行结果:
 * [1, 2, 3]
 */

   上述这个示例表示,我们需要删除集合中大于3的元素。你看,在removeIf(Predicate<? super E> filter)方法内,填入我们的过滤条件就可以了~这种操作非常适合我们的业务查询筛选操作。一旦我们获取了一部分数据,但是还需要从这些数据里面进行筛选的话,利用这个操作就可以把无用的数据删除,从而保留我们想要的结果。

@Override
@SuppressWarnings("unchecked")
public void replaceAll(UnaryOperator<E> operator) {
    Objects.requireNonNull(operator);
    final int expectedModCount = modCount;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        elementData[i] = operator.apply((E) elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

   我们这里再介绍一个替换方法,这也是一个具有函数式编程特性的方法。其参数是传入一个UnaryOperator,这个单词翻译成一元操作,不过我们更喜欢把它翻译成处理业务。也就是说我们可以在这里传入一组对数组内元素进行操作的业务处理逻辑,然后将处理后的元素再一次返回回来。
   我们还是先来看一下源码,然后再举一个例子。
   第一步,先来判定我们的过滤条件是否为空。如果为空的话就抛出NullPointerException异常。
   第二步,同步我们当前ArrayList的操作数modCount到新声明的预期修改值exceptModCount上。
   第三步,同步我们当前ArrayList的元素个数size。这里的元素个数size与上一步的预期修改值exceptModCount都是final修饰的,也就是不可变的。
   第四步,开始进行遍历。这个一步操作其实是可以预见的,因为说要对所有元素进行业务处理,所以肯定会有一次遍历。那么遍历都干嘛呢?我们看下面。
   第五步,到这里已经进入了遍历体内,我们发现它会对每一个元素进行operator.apply((E) elementData[i])操作。而这一步操作就我们当初传入的业务处理逻辑。当我们处理完毕后,又把这个元素重新赋予当前这个索引位置上的元素,也就是替换了当前的元素
   第六步,还是老样子,对操作值modCount和预期修改值exceptModCount是否不相等做判断,如果不相等则抛出ConcurrentModificationException异常。
   最后,对操作值modCount进行加1操作就可以了。
   这里我们来例举一个示例:

/**
 * 正确示例,放心运行
 */
public static void main(String[] args) {
    ArrayList<String> arrayList = Lists.newArrayList("对象1", "对象2", "对象3", "对象4", "对象5");
    arrayList.replaceAll((s) -> {
        s = "对\"" + s + "\"进行操作";
        return s;
    });
    System.out.println(arrayList);
}
/*
 * 运行结果:
 * [对"对象1"进行操作, 对"对象2"进行操作, 对"对象3"进行操作, 对"对象4"进行操作, 对"对象5"进行操作]
 */

   这个示例的目的其实很明确了,就不做具体解释了。不过我们可以通过这个示例代码联想一下这个replaceAll(UnaryOperator<E> operator)方法的应用场景。我这里联想到了两个:
   场景一:联想到我们函数式编程的含义,我们其实可以在这里进行一些业务处理。譬如我们对获取到的列表数据做统一的配货操作。就像我们示例那样,我们拿过数据来对集合内的数据进行操作,然后再返回这些操作结果以供上层使用。
   其实这里是省去了一些步骤。你看,一般我们获取到数据后(我们暂且称之为原始数据),我们会对这些原始数据做操作。这里不可避免的需要进行遍历。但是你遍历完毕后,处理完的元素你也得找个地方存起来,然后返回给调用者。那么这里其实你执行了4步:遍历—>处理—>封装—>返回。但是,replaceAll(UnaryOperator<E> operator)方法却只让你执行了一步——处理即可。
   场景二:这里也是场景一的一个衍生吧(对,不是延伸)。
   还记得我们写的SQL语句查询吗?还记得我们可能随手就来一个DATE_FORMAT吗?还记得我们可能随手就来一个CONCAT吗?还记得…
   对,我们可以不用这些函数来进行字段处理了。一般我们从数据库获取数据的时候首先会对数据进行处理,类似于日期格式化,类似于字符串拼接…但是,如果语句复杂的话这种操作是要耗费数据库性能的。所以,我们可以不用这些方法,直接返回最原始的数据。把数据库计算的压力抛给我们的Java程序,让Java程序进行处理(Java表示压力不大
   以上,就是我能想到的两种应用场景。

@Override
@SuppressWarnings("unchecked")
public void sort(Comparator<? super E> c) {
    final int expectedModCount = modCount;
    Arrays.sort((E[]) elementData, 0, size, c);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    modCount++;
}

   最后,这真的是最后,我们最后开始介绍我们ArrayList类内的最后一个方法——sort(Comparator<? super E> c)。本身最后一个方法可能需要更加仪式感的去讲解。但是,奈何这个方法不争气,它就是一个带有函数式编程接口参数的一个排序方法。
   我们还是先来看源码,然后举例子
   第一步,这步有取。你看,这里竟然不进行判空操作了。也就是说明,我们这个比较器(参数c)是可以为空的。
   第二步,同步我们当前ArrayList的操作数modCount到新声明的预期修改值exceptModCount上,并将其定义为final修饰,意味着它不可变。
   第三步,最重要的一步。调用Arrays类中的排序方法对整个ArrayList数组进行排序。而比较器则取自我们传入的c。这里我们并不对这个Arrays类中的排序方法做详细介绍,因为我会在另一篇文章Arrays源码逐条解析里面介绍它。
   第四步,对操作值modCount和预期修改值exceptModCount是否不相等做判断,如果不相等则抛出ConcurrentModificationException异常。
   最后,对操作值modCount进行加1操作就可以了。
   这里我们来例举几个示例:

/**
 * 示例一,正确示例,放心运行
 */
public static void main(String[] args) {
    ArrayList<String> arrayList = Lists.newArrayList("对象5", "对象2", "对象3", "对象4", "对象1");
    arrayList.sort(null);
    System.out.println(arrayList);
}
/*
 * 运行结果:升序排序
 * [对象1, 对象2, 对象3, 对象4, 对象5]
 */

/**
 * 示例二,正确示例,放心运行
 */
public static void main(String[] args) {
    ArrayList<String> arrayList = Lists.newArrayList("对象5", "对象2", "对象3", "对象4", "对象1");
    arrayList.sort((a, b) -> {
        return a.compareTo(b);
    });
    System.out.println(arrayList);
}
/*
 * 运行结果:升序排序
 * [对象1, 对象2, 对象3, 对象4, 对象5]
 */

/**
 * 示例三,正确示例,放心运行
 */
public static void main(String[] args) {
    ArrayList<String> arrayList = Lists.newArrayList("对象5", "对象2", "对象3", "对象4", "对象1");
    arrayList.sort((a, b) -> {
        return b.compareTo(a);
    });
    System.out.println(arrayList);
}
/*
 * 运行结果:降序排序
 * [对象5, 对象4, 对象3, 对象2, 对象1]
 */

   我们这里一连举了三个示例,其中并没有对Lambda表达式做优化,是因为我希望这个对比效果要强烈些。不过这里都是在进行排序。
   我们来看第一个示例,我们根据源码解析的提示,这里的比较器传入了null。那么得出的结果是一个升序排序的结果,也就是说如果我们不规定比较条件,那么返回的结果默认为升序排序。
   接下来我们一同说明第二个和第三个示例,因为这两个示例具有代表性。他们分别表示了升序排序和降序排序。例子还是那个例子,我们指出结论。
   当我们对数组内的元素排序时,如果想要升序排序,那么就用第一个参数与第二个参数作比较,形似:a.compareTo(b);而如果想要降序排序,那么就用第二个参数与第一个参数作比较,形似:b.compareTo(a)。也就是说,想要升,正着来,想要降,倒着来


   那么到此,我们的ArrayList源码逐条解析的续篇到此结束。
   同时,我们的ArrayList类,统共1469行代码就全部解析完毕,谢谢观看(ಥ_ಥ)。

发布了13 篇原创文章 · 获赞 8 · 访问量 9806
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览