List从入门到源码

前言

List是java重要的数据结构之一,我们经常接触到的有ArrayList、Vector(由于此集合基本上被ArrayList取代,本文不做讲解)和LinkedList三种。他们的类图如下:

数组可变大小: 大概是把数组复制进新数组、扩展

  • Vector:内部是数组数据结构;同步的,线程安全;JDK1.0的时候就只有一种集合结构Vector;几乎不用了;增删查询都很慢。
  • ArrayList:内部是数组数据结构;不同步;替代了Vector;查询速度快。
  • LinkedList:内部是链表数据结构;不同步;增删元素速度快。

数组和链表

  • 数组增删慢是因为,牵一发而动全身,要中间加入或去掉一个元素,后面元素都得动,所以比较慢
  • 链表增删快是因为,增加元素就是改动前后元素对下一元素或上一元素的指向,删除元素就是把下一元素的地址交给上一元素
  • 数组查询快是因为在一遍连续的空间
  • 链表查询慢是因为需要丛头开始查找

List讲解

  • 添加 void add(index,ele) void add(index,collection)
  • 删除 Object remove(index)
  • 查询 Object get(index) int indexOf(object) int lastIndexOf(object) List subList(from,to)
  • 修改 Object set(index,ele)

list的基本操作

        List list = new ArrayList();
        list.add("abc1");
        list.add("abc2");
        list.add("abc3");
        System.out.println(list); //[abc1, abc2, abc3]
        list.add(1, "222");
        System.out.println(list); //[abc1,222, abc2, abc3]
        System.out.println(list.remove(1)); // 222
        System.out.println(list.set(1, "333")); // abc2
        System.out.println(list.get(1)); // 333
        System.out.println(list.subList(1, 2)); // [333]
        Iterator it = list.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
        //list特有取值方式
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

简单讲了下List通用的方法后,开始学习在实际开发过程中使用最多的ArrayList.

ArrayList详解

  • ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
  • 它继承于 AbstractList,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。
  • 在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1)
  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
  • ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。
      和 Vector 不同,ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector(不建议使用) 或者 CopyOnWriteArrayList。

ArrayList的全部方法

List list = new ArrayList();

//add
list.add("hello");
list.add("world");
//AbstractCollection中定义了toString直接输出数组格式
System.out.println(list); //[hello, world]

//size
System.out.println(list.size()); //2
//empty
System.out.println(list.isEmpty()); //false

//add addAll
List list2 = new ArrayList();
list2.add("hello");
list2.add(123); //自动装箱
list.addAll(list2);
System.out.println(list); //[hello, world, hello, 123]

//remove removeAll
list.remove("hello");
System.out.println(list); //[world, hello, 123]
//list.remove(123);  //java.lang.IndexOutOfBoundsException
list.remove(new Integer(123));
System.out.println(list); //[world, hello]
list.remove(0);
System.out.println(list); //[hello]
System.out.println(list2); //[hello, 123]
list.add("world");
list.add(123);
list.removeAll(list2); //删除交集
System.out.println(list);

//retainAll
list.add("hello");
list.add(123);
System.out.println(list);
list.retainAll(list2); //交集
System.out.println(list2);
System.out.println(list); //[hello, 123]

//contains containsAll
list.add("world");
System.out.println(list.contains(123)); //true
System.out.println(list.containsAll(list2)); //true

//clear
list2.clear();
System.out.println(list2); //[]

//get
System.out.println(list); //[hello, 123, world]
System.out.println(list.get(0)); //hello
//set
list.set(1, 456);
System.out.println(list); //[hello, 456, world]
//add
list.add(1,true);
System.out.println(list); //[hello, true, 456, world]
//indexOf
System.out.println(list.indexOf(true)); //1

//foreach
for (Object object : list) {
	System.out.println(object);
}
//for
for(int i=0;i<list.size();i++) {
	System.out.println(list.get(i));
}
//iterator
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
	System.out.println(iterator.next());
}
//listIterator
ListIterator iterator2 = list.listIterator();
while(iterator2.hasNext()) {
	System.out.println(iterator2.next());
}
while(iterator2.hasPrevious()) {
	System.out.println(iterator2.previous());
}

为了进一步吃透ArrayList。开始进行紧张而又刺激的源码学习

  • 首先我们看下ArrayList的构造方法
    /**
     * Default initial capacity //默认容量.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances. 空对象
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; // non-private to simplify nested class access

    private int size;
    
     /**
     *默认构造函数,使用初始容量0构造一个空列表(无参数构造)
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    /**
     * 带初始容量参数的构造函数。(用户自己指定容量)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//初始容量大于0
            //创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始容量等于0
            //创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    
    /**
    *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
    *如果指定的集合为null,throws NullPointerException。 
    */
     public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

以上为ArrayList的构造方法,其中无参构造方法默认创建一个为空的list.若实现可以判断list的大小,我们可以在创建之初就定义其长度大小。了解了ArrayList的创建,下面学习下list添加元素时的过程吧。

ArrayList的扩容机制

         //添加方法 add(Object obj)
    	 public boolean add(E e) {
    	        //检测空间容量是否够用
		        ensureCapacityInternal(size + 1);
		        //添加元素    
		        elementData[size]=e; 
		        size++;
		        elementData[size++] = e;
		        return true;
    	}
    	
        private void ensureCapacityInternal(int minCapacity) {
            //判断list是否为空,若为空,minCapacity和DEFAULT_CAPACITY取较大的那个
            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方法进行扩容,调用此方法代表已经开始扩容了
                grow(minCapacity);
        }
    
         /**
         * 要分配的最大数组大小
         */
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

        /**
         * ArrayList扩容的核心方法。
         */
        private void grow(int minCapacity) {
            // oldCapacity为旧容量,newCapacity为新容量
            int oldCapacity = elementData.length;
            //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
            //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
           // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
           //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
            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);
        }
        
        // 从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8
        private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            //对minCapacity和MAX_ARRAY_SIZE进行比较
            //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
            //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
            //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
        }
  • 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 hugeCapacity 方法。数组容量为10,add方法中 return true,size增为1。
  • 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。

注意

  • java 中的 length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.
  • java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法.
  • java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

在学习ArrayLsit源码中,发现了方法ensureCapacity();这个方法有什么作用呢?那我们就读下这个方法吧~

        public void ensureCapacity(int minCapacity) {
            int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                // any size if not default element table
                ? 0
                // larger than default for default empty table. It's already
                // supposed to be at default size.
                : DEFAULT_CAPACITY;
    
            if (minCapacity > minExpand) {
                ensureExplicitCapacity(minCapacity);
            }
        }

通过这个段代码的学习,我们可以知道: 输入一个容量值,将list与空list进行对比。若两者相等,则minExpand为10,否则为0;
然后将输入值与minExpand进行对比。判断一次扩展多少。
此方法作用为: 最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

        ArrayList<Object> list = new ArrayList<Object>();
        final int N = 10000000;
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < N; i++) {
            list.add(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); //使用ensureCapacity方法前:2096

        list = new ArrayList<Object>();
        long startTime1 = System.currentTimeMillis();
        list.ensureCapacity(N);
        for (int i = 0; i < N; i++) {
            list.add(i);
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); //使用ensureCapacity方法后:435

ArrayList操作机制

        //在list的某个位置添加元素
        public void add(int index, E element) {
            //检查index是否在list的范围内
            rangeCheckForAdd(index);
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
             //源数组,源数组开始位置,新数组,新数组开始位置,拷贝的元素的个数
            System.arraycopy(elementData, index, elementData, index + 1, size - index);
            elementData[index] = element;
            size++;
        }

        //获取list中下标为index的数据
          public E get(int index) {
            rangeCheck(index);
            return elementData(index);
            //其中elementDate(index)函数为:
            return (E) elementData[index];
        }
        
        //元素的个数size的大小
        public int size() {
            return size;   //用于记录集合中元素的个数
        }
        
        //清空list
        public void clear() {
            // clear to let GC do its work
            for (int i = 0; i < size; i++)
                elementData[i] = null;
    
            size = 0;
        }

LinkList学习

LinkedList是一个实现了List接口和Deque接口的双端链表。LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:

List list=Collections.synchronizedList(new LinkedList(...));

LinkList对象属性

    transient int size = 0;

    /**
     * Pointer to first node.
    transient Node<E> first;

    /**
     * Pointer to last node.
    transient Node<E> last;

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

    /**
     * Constructs a list containing the elements of the specified
    **/
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

通过LinkList对象属性可以发现,其底层为双端链表。

LinkList源码–操作

        //链表末尾添加一个元素
        void linkLast(E e) {
        	final Node<E> l = last;
        	final Node<E> newNode = new Node<>(l, e, null);
        	last = newNode;
        	if (l == null)
        		first = newNode;
        	else
        		l.next = newNode;
        	size++;
        	modCount++;
        }
        
        //链表头部添加一个元素
        private void linkFirst(E e) {
            final Node<E> f = first;
            final Node<E> newNode = new Node<>(null, e, f);
            first = newNode;
            if (f == null)
                last = newNode;
            else
                f.prev = newNode;
            size++;
            modCount++;
        }
        
        

Arraylist 与 LinkedList 区别

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  • 底层数据结构:Arraylist底层使用的是 Object 数组;LinkedList底层使用的是 双向链表 数据结构
  • 插入和删除是否受元素位置的影响: ① Arraylist采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候,Arraylist会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度应为o(n)因为需要新创立一个新的链表,复制前i-1个元素并在第i位加入新的元素,最后附上n-i个元素。
  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 Arraylist支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

总结一下 list 的遍历方式选择:

实现了 RandomAccess 接口的list,优先选择普通 for 循环 ,其次 foreach,
未实现 RandomAccess接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的,),大size的数据,千万不要使用普通for循环
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值