Java基础知识-第16章-List接口以及相关实现类介绍

197 阅读29分钟

1、List接口

1.1、List接口概述

鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组,List是继承了Collection接口,此接口能够精确的控制每个元素插入的位置。用户能够根据索引(元素在List接口中的位置)访问List中的元素,类似于Java中的数组。

List接口有如下特点:

  • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引,并提供一些索引的方法,供用户操作
  • 类似“动态”数组,长度可以扩展
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
  • JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。

List接口的继承体系:

image.png

通过上面类图可用看出,List接口下有4个实现类,分别为:LinkedList、ArrayList、Vector和Stack

下面分别介绍这几个实现类:

  • 同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
  • 不同:如下
序号实现类介绍
1LinkedList对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储,查询慢,增删快,线程不安全,效率高,可以存储重复元素
2ArrayList作为List接口的主要实现类;线程不安全的,查询快,增删慢,线程不安全,效率高;底层使用Object[] elementData数组结构存储,可以存储重复元素
3Vector作为List接口的古老实现类;查询快,增删慢,线程安全的,效率低;底层使用Object[] elementData数组结构存储,可以存储重复元素
4Stack底层数据结构是数组,先进后出(FILO, First In Last Out)的特性。

1.2、List接口中的方法(特有)

List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。

List是Collection的子接口,所以Collection中定义的那些方法List都能用,又因为List中的元素是有序的,所以List增加了有关索引的方法,这是Collection当中没有的,因为Collection还要兼顾SetSet中又没有索引。所有List接口的实现类都可以调用这些方法

注意ArrayList是典型的数组替换结构,凡是以前用数组的地方,全部可以替换成ArrayList,包括项目当中 索引和数组中的规定一样,因为底层就是用数组实现的,索引仍然从0开始

常用方法:

void add(int index, Object ele):在index位置插入ele元素

boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来

Object get(int index):获取指定index位置的元素

int indexOf(Object obj):返回obj在集合中首次出现的位置,如果没有就返回-1

int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置,如果没有就返回-1

Object remove(int index):移除指定index位置的元素,并返回此元素

Object set(int index, Object ele):设置指定index位置的元素为ele

List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合,本身的list没有变化

注意:操作索引时,一定要注意防止索引越界异常提示。

  • IndexOutOfBoundsException:索引越界异常,集合会报;
  • ArrayIndexOutOfBoundsException:数组索引越界异常;
  • StringIndexOutOfBoundsException:字符串索引越界异常。

1、void add(int index, Object ele)

//void add(int index, Object ele):在index位置插入ele元素

ArrayList list=new ArrayList();
list.add(123);//这个add是collection接口的add方法,每次添加都是在末尾
list.add(456);//自动装箱
list.add("AA");
list.add(new Person("Tom",12));
list.add(456);//可以放相同数据的

System.out.println(list);//[123, 456, AA, Person [name=Tom, age=12], 456]
list.add(1, "BB");
System.out.println(list);//[123, BB, 456, AA, Person [name=Tom, age=12], 456]

2、boolean addAll(int index, Collection eles)

//boolean addAll(int index, Collection eles),从index位置开始将eles中的所有元素添加进来
List list1=Arrays.asList(1,2,3);
list.addAll(list1);
//list.add(list1);//不会报错,是把list1整体当做一个元素,此时如果调用size方法结果为7
System.out.println(list);//[123, BB, 456, AA, Person [name=Tom, age=12], 456, 1, 2, 3]
System.out.println(list.size());//9

3、Object get(int index)

//0bject get(int index): 获取指定index位置的元素
System.out.println(list.get(0));

4、int indexOf(Object obj)

@Test
public void test2(){
    ArrayList list = new ArrayList();
    list.add(123);
    List.add(456);
    list.add("AA");
    list.add(new Person("Tom" ,12));
    list.add(456);
    //int index0f(0bject obj): 返回obj在集合中首次出现的位置。如果不存在。返回-1.
    int index = list.indexof(4567);
    System.out.printLn(index);
}

5、int lastIndexOf(Object obj)

//int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置,如果没有就返回-1
System.out.printLn(list.indexof(4567));

6、Object remove(int index)

注意:Collection中的remove是删除某个元素,这里是方法的重载而不是方法的重写 ,因为方法名一样,但形参类型不一样,在List中也可以按照对象去删除

//Object remove(int index):移除指定index位置的元素,并返回此元素
Object obj=list.remove(0);
System.out.println(obj);

7、Object set(int index, Object ele)

//Object set(int index, Object ele):修改指定index位置的元素为ele
list.set(1, "CC");
System.out.println(list);

8、List subList(int fromIndex, int toIndex)

List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合,本身的list没有变化
List subList = list.subList(2, 4);
System.out.println(subList);
System.out.println(list);

9、Size()

如果要计算 ArrayList 中的元素数量可以使用 size() 方法:

import java.util.ArrayList;

public class RunoobTest {
    public static void main(String[] args) {
        ArrayList<String> sites = new ArrayList<String>();
        sites.add("Google");
        sites.add("Runoob");
        sites.add("Taobao");
        sites.add("Weibo");
        System.out.println(sites.size());//4
    }
}

1.3、List接口遍历方法

List遍历的方式

  • Iterator迭代器方式
  • 增强for循环
  • 普通的 for 循环也可以,因为有索引
ArrayList list=new ArrayList();
list.add(123);
list.add(456);
list.add("AA");

//方式一:Iterator迭代器方式 
Iterator iterator=list.iterator();
while(iterator.hasNext()) {
    System.out.println(iterator.next());
}

//方式二:增强for循环
for(Object obj:list) {
    System.out.println(obj);
}

//方式三:普通的循环也可以,因为有索引 
//由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素,通过调用public E get(int index)方法遍历
for(int i=0;i<list.size();i++) {
    System.out.println(list.get(i));
}

三种遍历方式的性能

遍历ArrayList时,在性能方面:随机访问 , 通过索引值遍历> for-each遍历 > Iterator迭代器遍历。

1.4、remove的一个问题

代码一

public class Test{
	public static void main(String[] args) {
		
		List list=new ArrayList();
		list.add(1);
		list.add(2);
		list.add(3);
		updateList(list);//调用静态方法
		System.out.println(list);//[1, 2]		
	}
	public static void updateList(List list) {
		list.remove(2); //删除的是索引2位置上的元素
	}
}

代码二

public class Test{
	public static void main(String[] args) {
		
		List list=new ArrayList();
		list.add(1);
		list.add(2);
		list.add(3);
		updateList(list);
		System.out.println(list);//[1, 3]
		
		
	}
	public static void updateList(List list) {
		list.remove(new Integer(2)); //如果要删除2这个数,则应该把它装成对象,如下
	}
}

总结

  • 第一种情况下调用的是List中的remove方法Object remove(int index),所以这种情况下是删掉索引值为2的元素
  • 第二种情况下调用的是Collection中的remove方法,删除的必须是个对象,集传入参数必须是类的对象,所以如果要删除2这个数,则应该把它装成包装类的对象
  • 注意:从List集合中移除元素的操作,也会导致被移除的元素以后的所有元素的向左移动一个位置。
  • 因此考察的点list里面装的是其实是对象,就算是数字进行add操作,会进行自动装箱操作

2、ArrayList实现类

2.1、ArrayList实现类概述

ArrayList 类位于 java.util 包 ,ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。ArrayList 继承了 AbstractList ,并实现了 List 接口,间接实现了Collection接口。除了实现 List 接口里面的方法外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

作为List接口的主要实现类;查询快,增删慢,线程不安全,效率高;底层使用Object[] elementData数组结构存储,其操作基本上是对数组的操作,该集合可以存储重复元素

注意,此实现类不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一 个线程从结构上修改了列表,那么它必须保持外部同步。 相对比的,Vector 是线程安全的,其中涉及线程安全的方法皆被同步操作了。

查看源码

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

继承树如下:

image.png

通过结构图可以看出,ArrayList继承自AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。

  • 继承AbstractList,实现了List接口:它是一个动态数组,提供了相关添加,删除,修改和遍历等功能;
  • 实现RandomAccess接口:提供随机访问的能力;RandmoAccess是Java中用来被List接口实现,为List提供快速访问功能的。在ArrayList中,可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • 实现Cloneable接口:覆盖clone()方法,可以被克隆;
  • 实现Serializable接口:支持序列化和反序列化,可以通过序列化传输数据。

ArrayList的特性:

  • 增删慢,查询快(底层数据结构是数组);
  • 效率高,线程不安全的(非同步);
  • 擅长随机访问(实现了RandomAccess接口);

2.2、ArrayList源码剖析

2.2.1、jdk7情况下

概述

每个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造 ArrayList 时指定其容量。在添加大量元素前,应用程序也可以使用 ensureCapacity 判断扩容操作来增加 ArrayList 实例的容量,这可以减少递增式再分配的数量。

从下面代码出发进行源码的简析:

ArrayList list = new ArrayList(); 
list.add(123);
list.add(11);

底层使用数组实现:

private transient Object[] elementData;

ArrayList的构造方法如下

ArrayList 提供了三种方式的构造器,分别可以构造一个默认初始容量为 10 的空列表、构造一个指定初始容量的空列表以及构造一个包含指定 collection 的元素的列表,这些元素按照 该 collection 的迭代器返回它们的顺序排列的。

//指定容量为initialCapacity大小的ArrayList
public ArrayList(int initialCapacity) {
    super()
        if (initialCapacity < 0) {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    this.elementData = new Object[initialCapacity];//初始化容量为10的数组
}

//指定容量为10的ArrayList
public ArrayList() {
    this(10) //调用带参的构造器,如上
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

add方法源码如下:从上至下

// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
    //1、调用ensureCapacityInternal方法确认一下集合容量是否够
    //size:动态数组的初始化大小,表示已经添加了几个,初始的话就是0
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e; ///3、往elementData数组里面添加数据,然后size+1
    return true;
}

//2、判断数据容量,若数组已满,则扩容
private void ensureCapacityInternal(int minCapacity) {
    modCount++;
    //如果minCapacity大于elementData数组初始长度10,就扩容
    //显然我们初次添加,minCapacity=size + 1=1 < 10,所以不会扩容
    if (minCapacity - elementData.length > 0) {
        grow(minCapacity)//扩容,并传入参数为 minCapacity=size + 1
    }
}

//4、扩容操作
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;//oldCapacity:扩容前数组的容量:10
    int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity:默认情况下扩容为原来的1.5倍
    if (newCapacity - minCapacity < 0)//如果扩容后还是小于所需的容量minCapacity
        newCapacity = minCapacity;//就直接把我们所需的容量minCapacity赋给newCapacity
    if (newCapacity - MAX_ARRAY_SIZE > 0)//如果扩容后大于内部规定的最大值MAX_ARRAY_SIZE
        newCapacity = hugeCapacity(minCapacity);//则取最大值Integer.MAX_VALUE赋给newCapacity,否则抛出内存溢出异常
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);//将原有数组中的数据复制到新的容量为newCapacity的数组中
}

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
@Native public static final int MAX_VALUE = 0x7fffffff;

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

总结:

  • ArrayList list = new ArrayList() :当我们通过空参构造器初始化创建一个ArrayList集合的的时候,会通过ArrayList的有参构造器在底层创建长度是10的Object[]数组elementData
  • 然后执行add方法的时候,首先会调用 ensureCapacityInternal(size + 1) 方法确认一下当前为10的集合容量是否够,即这里的扩容要求的判断条件是minCapacity - elementData.length > 0
    • 如果够的话,即数组长度为10可以满足要求,则往elementData数组里面添加数据,即elementData[size++] = e;,然后size+1,size属性在刚开始添加数据的时候为初始值0,
    • 如果ArrayList容量不足,ArrayList会动态扩容大小 ,即minCapacity=size+1大于elementData数组初始长度10,则调用grow(int minCapacity)方法自动扩容,默认情况下,扩容为原来的容量的1.5倍【newCapacity = oldCapacity + (oldCapacity >> 1)】,同时需要将原有数组中的所有数据复制到新的容量为newCapacity的数组中
  • 建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity),这样就能将elementData 数组提前初始化为我们想要的capacity容量【this.elementData = new Object[initialCapacity];】,然后就避免中间环节很多的扩容步骤,就一步自动扩容成想要的更大的容量了。

举例:

ArrayList list = new ArrayList(); //底层创建了长度是10的Object[]数组elementData
list.add(123);//添加元素,elementData[0] = new Integer(123);
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。

Fail-Fast 机制:

ArrayList 也采用了快速失败的机制,通过记录 modCount 参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为 的风险。

2.2.2、jdk 8中ArrayList的变化

还是从下面代码出发进行源码的简析:

ArrayList list = new ArrayList(); 
list.add(123);
list.add(11);

ArrayList的源码如下

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    private static final long serialVersionUID = 8683452581122892189L;

   //默认初始化容量
    private static final int DEFAULT_CAPACITY = 10;
//空数组常量
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //默认的空数组常量
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    transient Object[] elementData; // 底层结构,数据存储数据
    private int size; //数组中包含元素的个数

//构造器1,指定容量为initialCapacity大小的ArrayList
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);
    }
}

//构造器2,指定容量为10的ArrayList
public ArrayList() {
	////底层Object[] elementData初始化为{}.并没有创建长度为10的数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    //private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
}

//构造器3
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;
    }
}

从构造方法中我们可以看见,默认情况下,elementData 是一个大小为 0 的空数组,但是当我们使用有参构造器指定了初始大小的时候,elementData 的初始大小就变成了我们所指定的初始大小了。

add方法源码如下:从上至下

//与jdk7没有任何变化
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

//判断容量
private void ensureCapacityInternal(int minCapacity) {
    //还是先判断能否扩容
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //private static final int DEFAULT_CAPACITY = 10;
        //minCapacity初始化为10
        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);
}

ArrayList 的 add 方法也很好理解,在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组中最后一个元素的后面。在 ensureCapacityInternal 方法中, 我们可以看见,如果当 elementData 为空数组时,它会使用默认的大小去扩容。所以 说,通过无参构造方法来创建 ArrayList 时,它的大小其实是为 0 的,只有在使用到 add方法的时候,才会去创建一个大小为 10 的数组。 第一个 add 方法的复杂度为 O(1),虽然有时候会涉及到扩容的操作,但是扩容的次 数是非常少的,所以这一部分的时间可以忽略不计。如果使用的是带指定下标的 add 方法,则复杂度为 O(n),因为涉及到对数组中元素的移动,这一操作是非常耗时的。

grow 方法是在数组进行扩容的时候用到的,从中我们可以看见,ArrayList 每次扩容都是扩 1.5 倍,然后调用 Arrays 类的 copyOf 方法,把元素重新拷贝到一个新的数组 中去。

get方法

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg
                                            (index));
}
E elementData(int index) {
    return (E) elementData[index];
}

因为 ArrayList 是采用数组结构来存储的,所以它的 get 方法非常简单,先是判断一下有没有越界,之后就可以直接通过数组下标来获取元素了,所以 get 的时间复杂度是 O(1)。

总结:

  • ArrayList list = new ArrayList() :当我们通过空参构造器初始化创建一个ArrayList集合的的时候,底层会直接将数组 Object[] elementData初始化为{},并没有创建长度为10的数组
  • 然后执行add方法的时候,首先会调用 ensureCapacityInternal(size + 1) 方法确认一下当前集合容量是否够,这里是不满足要求 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的前提下才能扩容
    • 如果满足要求,则在第一次调用add()时,底层才创建了一个长度10的数组elementData,并将数据添加到elementData
    • 如果ArrayList容量不足,ArrayList会动态扩容大小 ,则最终还是调用grow(int minCapacity)方法自动扩容,默认情况下,扩容为原来的容量的1.5倍【newCapacity = oldCapacity + (oldCapacity >> 1)】,同时需要将原有数组中的所有数据复制到新的容量为newCapacity的数组中
  • 建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity),这样就能将elementData 数组提前初始化为我们想要的capacity容量【this.elementData = new Object[initialCapacity];】,然后就避免中间环节很多的扩容步骤,就一步自动扩容成想要的更大的容量了。

举例:

ArrayList list = new ArrayList(); //底层Object[] elementData初始化为{}.并没有创建长度为10的数组
list.add(123);//添加元素,第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData中,elementData[0] = new Integer(123);
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。

小结:

  • jdk7中的ArrayList的对象的创建类似于单例的饿汉式
  • 而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
  • ArrayList 创建时的大小为 0;当加入第一个元素时,进行第一次扩容时,默认容 量大小为 10。
  • ArrayList 每次扩容都以当前数组大小的 1.5 倍去扩容。

3、LinkedList实现类

3.1、LinkedList实现类概述

链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,每一个节点里存放数据域以及前后节点的指针。

链表可分为单向链表和双向链表

一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。

img

双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点 和后继结点(如果有前驱/后继的话)。另外,双向链表还有一个 first 指针,指向头节点,和 last 指针,指向尾节点

img

LinkedList简介

Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。LinkedList 类位于 java.util 包中 ,LinkedList对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储,效率高,可以存储重复元素,允许插入null值,但是是线程不安全的。

LinkedList自定义了操作头部和尾部元素的方法,可以当做“栈”使用,同时又是Deque的实现类,所以有具有双端队列的特性。所以LinkedList既可以当做“栈”,也可以当做双端队列

以下情况使用 ArrayList :

  • 频繁访问列表中的某一个元素。
  • 只需要在列表末尾进行添加和删除元素操作。

以下情况使用 LinkedList :

  • 你需要通过循环迭代来访问列表中的某些元素。
  • 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。

继承体系:

image.png

总结:

  • LinkedList 继承AbstractList,实现了List接口:它是一个动态链表,提供了相关添加,删除,修改和遍历等功能;
  • LinkedList 实现了 Queue 接口,可作为队列使用
  • LinkedList 实现了 List 接口,可进行列表的相关操作。
  • LinkedList 实现了 Deque 接口,可作为队列使用。
  • LinkedList 实现了 Cloneable 接口,覆盖方法clone(),可以被克隆;
  • LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输数据。

3.2、LinkedList 新增方法

void addFirst(Object obj) // 使用 addFirst() 在头部添加元素
void addLast(Object obj) // 使用 addLast() 在尾部添加元素
Object getFirst() // 使用 getFirst() 获取头部元素
Object getLast() // 使用 getLast() 获取尾部元素
Object removeFirst() // 使用 removeFirst() 移除头部元素
Object removeLast() // 使用 removeFirst() 移除尾部元素

实例

public class RunoobTest {
    public static void main(String[] args) {
        //泛型,固定集合内部元素类型
        LinkedList<String> sites = new LinkedList<String>(); 
        sites.add("Google");
        sites.add("Runoob");
        sites.add("Taobao");
        sites.add("Weibo");
        System.out.println("集合初始元素为:");
        System.out.println(sites);

        // 使用 addFirst() 在头部添加元素
        sites.addFirst("Wiki");
        System.out.println("在头部添加元素后,集合元素为:");
        System.out.println(sites);

        // 使用 addLast() 在尾部添加元素
        sites.addLast("Wiki");
        System.out.println("在尾部添加元素后,集合元素为:");
        System.out.println(sites);

        // 使用 removeFirst() 移除头部元素
        sites.removeFirst();
        System.out.println("移除头部元素后,集合元素为:");
        System.out.println(sites);

        // 使用 removeLast() 移除尾部元素
        sites.removeLast();
        System.out.println("移除尾部元素后,集合元素为:");
        System.out.println(sites);

        // 使用 getFirst() 获取头部元素
        System.out.println("头部元素为:");
        System.out.println(sites.getFirst());

        // 使用 getLast() 获取尾部元素
        System.out.println("尾部元素为:");
        System.out.println(sites.getLast());
    }
}

结果:

集合初始元素为:
[Google, Runoob, Taobao, Weibo]
在头部添加元素后,集合元素为:
[Wiki, Google, Runoob, Taobao, Weibo]
在尾部添加元素后,集合元素为:
[Wiki, Google, Runoob, Taobao, Weibo, Wiki]
移除头部元素后,集合元素为:
[Google, Runoob, Taobao, Weibo, Wiki]
移除尾部元素后,集合元素为:
[Google, Runoob, Taobao, Weibo]
头部元素为:
Google
尾部元素为:
Weibo

3.3、LinkedList的源码分析(重点)

从下面代码出发进行源码的简析:

LinkedList list = new LinkedList(); 
list.add(123);

LinkedList的源码如下

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

	//属性
    transient int size = 0;//size:双向链表节点的个数;
    transient Node<E> first;//first变量指向双向链表的头节点保存的数据
    transient Node<E> last;//last变量指向双向链表的尾节点保存的数据

	//默认构造方法
    public LinkedList() {
    }
	
	//保存指定集合元素的构造方法
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }
 ...
}

Node内部类

体现了LinkedList的双向链表的说法,从上面源码中可以看出,firstlast是Node的实体实例,Node是LinkedList内部的私有静态类,Node表示链表每个节点的结构,包括一个数据域 item,一个后置指针 next,一个前置指针 prev。

//节点类,创建Node对象
private static class Node<E> {
    
    //节点的属性
    E item; //data域,保存当前节点的值;即往集合中添加的数据
    Node<E> next; //指针域,next指针指向当前元素item的下一个节点
    Node<E> prev; //指针域,prev指针指向当前元素item的上一个节点

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

add方法

对于链表这种数据结构来说,添加元素的操作无非就是在表头/表尾插入元素,又或 者在指定位置插入元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进 行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表, 所以复杂度为 O(n)。

//添加元素
public boolean add(E e) {
    linkLast(e);
    return true;
}

//在链表尾部添加一个元素
void linkLast(E e) {
    final Node<E> l = last;//初始的时候,集合没有数据,所以l=last=null,
    //将数据封装到Node中,创建了Node对象。
    final Node<E> newNode = new Node<>(l, e, null);
    //并将尾指针指向该新添加的节点
    last = newNode;
    //如果原来有尾节点,则更新原来节点的后继指针
    if (l == null)
        first = newNode; //否则将头指针指向该新节点
    else
        l.next = newNode;
    size++;
    modCount++;
}

当向表尾插入一个节点时,很显然当前节点的后继一定为 null,而前驱结点是 last 指针指向的节点,然后还要修改 last 指针指向新的尾节点。此外,还要修改原来尾节点的后继指针,使它指向新的尾节点,图解如下:

image.png

补充:

当向表头插入一个节点时,很显然当前节点的前驱一定为 null,而后继结点是 first 指针指向的节点,当然还要修改 first 指针指向新的头节点。除此之外,原来的头节点变成了第二个节点,所以还要修改原来头节点的前驱指针,使它指向表头节点,图解如下:

image.png

当向指定节点之前插入一个节点时,当前节点的后继为指定节点,而前驱结点为指 定节点的前驱节点。此外,还要修改前驱节点的后继为当前节点,以及后继节点的前驱为当前节点

image.png

总结:

LinkedList list = new LinkedList(); //内部声明了Node类型的first和last属性,默认值为null

list.add(123);//将123封装到Node中,创建了Node对象。

3.4、总结

参考www.cnblogs.com/lingq/p/127…

LinkedList与ArrayList的区别

  • 底层数据结构不同。ArrayList底层数据结构是数组。LinkedList 的底层结构是一个带头/尾指针的双向链表,可以快速的对头/尾节点 进行操作。
  • 性能不同。ArrayList增删慢,查询快。LinkedLIst增删快,查询慢(底层数据结构不同);
  • 内存不同。LinkedList比ArrayList更占内存(因为LinkedList每个节点存储两个引用,一个指向前元素,一个指向后元素)。

总结

  • LinkedList是基于链表结构的集合。内部定义了一个私有的Node实体实例,Node是双向链表节点对应数据的数据结构,其定义了三个属性:item(保存该节点的值)、prev(指向前一个元素的节点)、next(指向后一个元素的节点);
  • LinkedList不存在容量不足的问题;
  • LinkedList克隆时,是将全部元素克隆到新的LinkedLIst对象中;
  • LinkedList实现了Serializable接口,当写入输出流时,先写入“容量”,再依次写入“每一个节点的值”,当读取输入流时,先读取“容量”,再依次读取“每个节点的元素”;
  • LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法,提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false)。

4、Vector实现类

参考www.cnblogs.com/lingq/p/127…

4.1、Vector实现类概述

VectorArrayList类似,作为List接口的古老实现类。底层使用Object[] elementData数组结构存储,可以存储重复元素,特性即功能与ArrayList类似,也具有增删慢,查询快的特点,不同的是,Vector是同步的,线程安全的,所以在多线程环境下,使用VectorArrayList更合适。

在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时, 使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。

Vector的特性:

  • 增删慢,查询快;
  • 效率低,线程安全(与ArrayList相反);
  • 擅长随机访问,通过索引值遍历;

Vector构造方法

image.png

Vector的遍历方式

Vector一共有4种遍历方式:Iterator迭代器遍历、通过索引值的随机方法、for-each遍历和Enumeration遍历。

Iterator迭代器遍历、通过索引值的随机方法、for-each遍历和ArrayList类似不再赘述,这里讲一下Enumeration遍历。

Integer value = null;
Enumeration enu = vector.elements();
while (enu.hasMoreElements()) {
    value = (Integer)enu.nextElement();
}

遍历Vector时,在性能方面:随机访问,通过索引值遍历 > for-each遍历 > Enumeration遍历 > Iterator迭代器遍历

所以,无论是ArrayList还是Vector遍历,推荐使用随机访问,通过索引值遍历 。

ArrayList与Vector的区别

  1. 线程安全。ArrayList是线程不安全的,Vector是线程安全的;
  2. 扩容不同。ArrayList默认扩容是原始容量的1.5倍,Vector默认扩容是原始容量 * 2;
  3. 遍历方式不同,ArrayList有3种,Vector有4种,多的一种是Enumeration遍历;

4.2、Vector的源码分析(重点)

从下面代码出发进行源码的简析:

Vector list = new Vector(); 
list.add(123);

Vector的源码如下

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

    protected Object[] elementData;
    protected int elementCount;//动态数组的大小,同ArrayList的size
    protected int capacityIncrement;//扩容数组大小的增长系数
    //在创建Vector时如指定了capacityIncrement的系数,则在Vector需要扩容时,扩容的大小总是capacityIncrement的系数大小

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -2767605614048989439L;

    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

    public Vector() {
        this(10); //底层都创建了长度为10的数组
    }

    public Vector(Collection<? extends E> c) {
        elementData = c.toArray();
        elementCount = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
    }

Vector很多方法都跟 ArrayList 一样,只是多加了个 synchronized 来保证线程安全罢了。Vector 比 ArrayList 多了一个属性:

protected int capacityIncrement;//扩容数组大小的增长系数

这个属性是在扩容的时候用到的,它表示每次扩容只扩 capacityIncrement 个空间就 足够了。该属性可以通过构造方法给它赋值。

从构造方法中,我们可以看出 Vector 的默认大小也是 10,而且它在初始化的时候就已经创建了数组了,这点跟 ArrayList 不一样。

add方法

//添加元素
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

grow 扩容方法:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 
                                      0) ? capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

从 grow 方法中我们可以发现,newCapacity 默认情况下是两倍的 oldCapacity,而当 指定了 capacityIncrement 的值之后,newCapacity 变成了 oldCapacity+capacityIncrement。

总结:

jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍

4.3、总结

  • Vector实际上是通过一个数组去保存数据的。当我们构造Vecotr时;若使用默认构造函数,则Vector的默认容量大小是10
  • 当Vector容量不足以容纳全部元素时,Vector的容量会增加。若容量增加系数 >0,则将容量的值增加“容量增加系数”;否则,将容量大小增加一倍。
  • Vector的克隆函数,即是将全部元素克隆到一个数组中;
  • Vector默认扩容是原始容量 * 2
  • Vector 每次扩容都以当前数组大小的 2 倍去扩容。当指定了 capacityIncrement 之 后,每次扩容仅在原先基础上增加 capacityIncrement 个单位空间。
  • ArrayList 和 Vector 的 add、get、size 方法的复杂度都为 O(1),remove 方法的复 杂度为 O(n)。
  • ArrayList 是非线程安全的,Vector 是线程安全的

5、Stack实现类

参考www.cnblogs.com/lingq/p/127…

5.1、Stack实现类概述

Stack是继承自Vector,所以它的底层数据结构也是动态数组。实现了一个 “先进后出(FILO, First In Last Out)”的栈结构。由于继承了Vector,所以具有了Vector的相关特性,这里不再赘述。

源码

public class Stack<E> extends Vector<E> {

    public Stack() {
    }

    public E push(E item) {
        addElement(item);

        return item;
    }


    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }

    public boolean empty() {
        return size() == 0;
    }

    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    private static final long serialVersionUID = 1224463164541339165L;
}

常用的API方法

image.png

总结

  • Stack实际上也是通过数组去实现的。
    • 执行push时(即,将元素推入栈中),是通过将元素追加的数组的末尾中。
    • 执行peek时(即,取出栈顶元素,不执行删除),是返回数组末尾的元素。
    • 执行pop时(即,取出栈顶元素,并将该元素从栈中删除),是取出数组末尾的元素,然后将该元素从数组中删除。
  • Stack继承于Vector,意味着Vector拥有的属性和功能,Stack都拥有。

6、List总结

image.png

重点:

image.png