站在使用者的角度研究 List 源码

1.前言

国庆有时间,正好可以学习一些优秀的源代码,于是便选择了常用的集合 List。

List 是一个为有序集合设计的接口,全称是 java.util.List,诞生于 JDK1.2,核心能力有 添加、删除、替换、查找、排序、遍历、集合运算。

java.util包里面实现了 List接口的普通类有四个:

​ ArrayList,LinkedList,Stack,Vector

其中除了 Stack(继承自 Vector),其他三个类都是直接实现了 List接口。

开发环境:JDK1.8。

2.List核心能力

2.1 添加元素

涉及的方法如下:

// 添加单个元素,可以指定位置
boolean add(E e);
void add(int index, E element);

// 批量添加元素,可以指定位置,被添加的集合需要实现 Collection<E>接口
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);

? extends E”的含义:

​ E指代的是 list中的元素类型,?指代的是其他未知集合里的元素类型,而 ?需要是 E的子类。比如 Integer类继承自 Number类,E指代 Number,则 ?可以指代 Integer或 Number的其他子类。

上述四个方法均没有默认实现,需要实现类自己去实现。

2.2 删除元素

// 删除单个元素
boolean remove(Object o);
E remove(int index);

// 删除所有元素
void clear();

上述三个方法均没有默认实现,需要实现类自己去实现。

2.3 替换元素

E set(int index, E element);

2.4 查找元素

boolean contains(Object o);

// 目标 o从左往右第一次出现的下标
int indexOf(Object o);

// 目标 o从左往右最后一次出现的下标
// 或 从右往左第一次出现的下标
int lastIndexOf(Object o);

2.5 元素排序

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

这里可以看到是先拷贝 List中的元素创建了一个数组,然后使用 Arrays进行排序,并且可以指定排序的规则。

2.6 遍历元素

Iterator<E> iterator();

这里可以使用迭代器去遍历 List中的所有元素。

2.7 集合运算

// 这个方法的效果相当于两个集合 A和 C:A交C
boolean retainAll(Collection<?> c);

// 这个方法的效果相当于两个集合 A和 C:A-A交C
boolean removeAll(Collection<?> c);

// 将 List中的所有元素统一执行某个操作
default void replaceAll(UnaryOperator<E> operator) {
    Objects.requireNonNull(operator);
    final ListIterator<E> li = this.listIterator();
    while (li.hasNext()) {
        li.set(operator.apply(li.next()));
    }
}

removeAll和 replaceAll虽然字面意思分别是 移除和替换,但是其实际含义用集合运算来表示更易于理解。

3.ArrayList源码分析

ArrayList是 List接口的实现类之一,之所以选择它做为分析对象,是因为实际工作中用得比较多。

这里着重于分析 ArrayList对 List接口的部分核心能力是如何实现的,有什么特点。

3.1 具体实现:添加元素

首先,先看最简单的添加元素是如何实现的:

transient Object[] elementData;

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

这里可以看到实际数据是存在于一个对象数组 elementData中,transient用于阻止该变量被序列化。elementData如何被初始化取决于使用的是哪个构造方法,如下:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};    
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
 
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);
    }
}

接着再解释下 ensureCapacityInternal(size + 1),这个操作目的是修改 modCount,使其值加 1,关于 modCount可以简单地理解为并发场景中用于判断集合是否被别的线程修改

3.2 具体实现:查找元素

这个方法比较有意思的地方是:支持查找 null元素。

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

具体的查找逻辑跟笔者猜测一致,使用了顺序遍历和 equals方法。

3.3 具体实现:集合运算

public boolean retainAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, true);
}

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

如上,retainAll是两个集合求交集的一个运算,实现逻辑略长,部分逻辑耐人寻味。

先看一个与核心逻辑无关的代码: Objects.requireNonNull©,用它来判断对象是否为空是不是比下面的方式优雅多了?

if(c==null){
    throw new NullPointerException();
}

下面是求交集的实现逻辑,利用集合的 contains方法,比较简短:

for (; r < size; r++)
    if (c.contains(elementData[r]) == complement)
        elementData[w++] = elementData[r];

这里需要注意的是,该方法大部分逻辑在 finally里面,那么这里是在做些什么呢?

if (r != size) {
    System.arraycopy(elementData, r,
                     elementData, w,
                     size - r);
    w += size - r;
}
if (w != size) {
    // clear to let GC do its work
    for (int i = w; i < size; i++)
        elementData[i] = null;
    modCount += size - w;
    size = w;
    modified = true;
}

先想一下什么情况下 r != size?是不是前面的抛异常了就会出现,然后 w += size - r 这一行就会导致 elementData中未与集合 c对比过的元素也会出现在最后的结果里。举个例子如下:

​ elementData= a,b,c,d,e

​ c=b,c,d

当 r=3时,抛出了运行时异常,这时最后求交集的结果将是:

​ b,c,d,e

这个结果有些奇怪,笔者暂时也搞不清楚,不过既然求交集的过程中抛出了异常,那么结果和预期不符也说得过去。

4.用法展示

这里继续以 ArrayList作为研究对象,添加、删除、替换、查找和遍历等方法使用起来比较简单就不做研究了,主要研究 自定义排序功能和集合运算。

4.1 自定义排序

// 自定义排序:根据字符串长度逆序排列
List<String> sortList=new ArrayList<>();
Collections.addAll(sortList,"R","Java","C++","Python");
System.out.println("origin list is: "+sortList.toString());

sortList.sort((a,b)->b.length()-a.length());
System.out.println("sort list is: "+sortList.toString());

运行效果如下:

origin list is: [R, Java, C++, Python]
sort list is: [Python, Java, C++, R]

正如第 2节分析的那样,list 提供的 sort方法可以指定排序规则,配合 lambda表达式使用,简单又高效。

4.2 集合运算

  1. 将集合中的元素全部进行某个转换

    // 把整数集合中的所有元素都加倍
    List<Integer> intList=new ArrayList<>();
    Collections.addAll(intList,2,5,8);
    System.out.println("origin list is: "+intList.toString());
    
    intList.replaceAll(num -> num*2);
    System.out.println("origin list is: "+intList.toString());
    

    运行效果如下:

    origin list is: [2, 5, 8]
    all integer is double: [4, 10, 16]

  2. 求两个集合的交集

    List<Integer> A=new ArrayList<>();
    Collections.addAll(A,2,1,5,8);
    List<Integer> B=new ArrayList<>();
    Collections.addAll(B,2,5,6,9);
    System.out.println("A: "+A.toString()+",B: "+B.toString());
    
    A.retainAll(B);
    System.out.println("A和B的交集: "+A.toString());
    

    运行效果如下:

    A: [2, 1, 5, 8],B: [2, 5, 6, 9]
    A和B的交集: [2, 5]

  3. 求 A-A交B

    A.removeAll(B);
    System.out.println("A-A交B: "+A.toString());
    

    运行效果如下:

    A: [2, 1, 5, 8],B: [2, 5, 6, 9]
    A-A交B: [1, 8]

list提供的集合运算种类虽少,但胜在实用。

5.总结

List接口设计的方法比较全面,除了增删改查,还有部分实用的集合运算。ArrayList作为其实现类之一,方法的实现逻辑较为清晰简洁,在集合运算中还调用了 native方法。在代码风格上也很有借鉴意义,比如 使用 Objects.requireNonNull()方法进行 null判断,使用 'E’来表示本集合元素类型,用 '?'来表示其他未知集合的元素类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值