List集合

List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。

List接口和ListIterator接口

List作为Collection接口的子接口,当然可以使用Collection接口里的全部方法。而且由于List是有序的集合,因此List集合里增加了一些根据索引来操作集合元素的方法。

将元素element插入到List集合的index处

void add(int index, E element);

将集合c所包含的所有元素都插入到List集合的index处

boolean addAll(int index, Collection<? extends E> c);

返回集合index索引处的元素

E get(int index);

返回对象o在List集合中第一次出现的位置索引

int indexOf(Object o);

返回对象o在List集合中最后一次出现的位置索引

int lastIndexOf(Object o);

删除并返回index索引处的元素

E remove(int index);

将index索引处的元素替换成element对象,返回新元素

E set(int index, E element);

返回从索引fromIndex(包含)到索引toIndex(不包含)处所有集合元素组成的子集合

List<E> subList(int fromIndex, int toIndex);
public class ListTest {

    public static void main(String[] args) {
        List books = new ArrayList<>();
        //向books中添加三个元素
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂Android讲义"));
        System.out.println(books);
        //将新字符串对象插入在第二个位置
        books.add(1, new String("疯狂Ajax讲义"));
        for (int i=0; i < books.size(); i++){
            System.out.println(books.get(i));
        }

        //删除第三个元素
        books.remove(2);
        System.out.println(books);
        //判断指定元素在List集合中的位置;输出1表明位于第二位
        System.out.println(books.indexOf(new String("疯狂Ajax讲义")));
        //将第二个元素替换成新的字符串对象
        books.set(1, new String("疯狂Java讲义"));
        System.out.println(books);
        //将books集合的第二个元素(包括)到第三个元数(不包括)截取成子集合
        System.out.println(books.subList(1, 2));


    }

}
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
轻量级Java EE企业应用实战
疯狂Ajax讲义
疯狂Java讲义
疯狂Android讲义
[轻量级Java EE企业应用实战, 疯狂Ajax讲义, 疯狂Android讲义]
1
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
[疯狂Java讲义]

与Set只提供了一个iterator()方法不同,List还额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法。ListIterator接口在Iterator接口基础上增加了如下方法。

返回该迭代器关联的集合是否还有上一个元素

boolean hasPrevious();

返回该迭代器的上一个元素

E previous();

在指定位置插入一个元素

void add(E e);

拿ListIterator与普通的Iterator进行对比,不难发现ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator还可以通过add方法向List集合中添加元素(Iterator只能删除元素).

public class ListIteratorTest {

    public static void main(String[] args) {
        String[] books = {"疯狂Java讲义","轻量级Java EE企业应用实战"};

        List bookList = new ArrayList<>();
        for (int i=0; i<books.length; i++){
            bookList.add(books[i]);
        }

        ListIterator lit = bookList.listIterator();
        while (lit.hasNext()){
            System.out.println(lit.next());
            lit.add("-----------分割符-------------");
        }
        System.out.println("============下面开始反向迭代=============");
        while (lit.hasPrevious()){
            System.out.println(lit.previous());
        }



    }

}
疯狂Java讲义
轻量级Java EE企业应用实战
============下面开始反向迭代=============
-----------分割符-------------
轻量级Java EE企业应用实战
-----------分割符-------------
疯狂Java讲义

从上面程序中可以看出,使用ListIterator迭代List集合时,开始也需要采用正向迭代,即先使用next()方法进行迭代,在迭代过程中可以使用add()方法向上一次迭代元素的后面添加一个新元素。

ArrayList和Vector实现类

ArrayList和Vector类都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数组。ArrayList或Vector对象使用initialCapacity参数来设置该数组的长度,当向ArrayList或Vector中添加元素超出该数组的长度时,它们的initialCapacity会自动增加。

对于通常的编程场景,我们无须关心ArrayList和Vector的initialCapacity。但如果向ArrayList或Vector集合中添加大量元素时,可使用ensureCapacity(int minCapacity)方法一次性地增加initialCapacity。这可以减少重分配的次数,从而提高性能。

如果开始就知道ArrayList或Vector集合需要保存多少个元素,则可以在创建它们时就指定initialCapacity大小。如果创建空的ArrayList或Vector集合时不指定initialCapacity参数,则Object[]数组的长度默认为10.

除此之外,ArrayList和Vector还提供了如下两个方法来重新分配Object[]数组。

将ArrayList或Vector集合的Object[]数组长度增加minCapacity

    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);
        }
    }

调整ArrayList或Vector集合的Object[]数组长度为当前元素的个数。程序可调用该方法来减少ArrayList或Vector集合对象占用的存储空间。

    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

Vector里有一些功能重复的方法,这些方法中方法名更短的方法属于后来新增的方法,方法名更长的方法则是Vector原有的方法。Java改写了Vector原有的方法,将其方法名缩短是为了简化编程。而ArrayList开始就作为List的主要实现类。实际上,Vector具有很有缺点,通常尽量少用Vector实现类。

ArrayList和Vector的显著区别是:ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性;但Vector集合则是线程安全的,无须程序保证该集合的同步性。因为Vector是线程安全的,所以Vector的性能比ArrayList的性能要低。实际上,即使需要保证List集合线程安全,也通用不推荐使用Vector实现类。后面会介绍一个Collections工具类,它可以将一个ArrayList变成线程安全的。

Vector还提供了一个Stack子类,它用于模拟“栈”这种数据结构,“栈”通常是指“后进先出”(LIFO)的容器。最后“push”进栈的元素,将最先被“pop”出栈。Stack类里提供了如下几个方法。

返回“栈”的第一个元素,但并不将该元素“pop”出栈

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

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

返回“栈”的第一个元素,并将该元素“pop”出栈

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

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

        return obj;
    }

将一个元素“push”进栈,最后一个进“栈”的元素总是位于“栈”顶

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

        return item;
    }
public class VectorTest {

    public static void main(String[] args) {
        Stack v = new Stack();
        //依次将3个元素push入栈
        v.push("疯狂Java讲义");
        v.push("轻量级Java EE企业应用实战");
        v.push("疯狂Android讲义");
        System.out.println(v);

        //访问第一个元素,但并不将其pop出栈
        System.out.println(v.peek());
        System.out.println(v);
        System.out.println(v.pop());
        System.out.println(v);

    }

}
[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
疯狂Android讲义
[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
疯狂Android讲义
[疯狂Java讲义, 轻量级Java EE企业应用实战]

需要指出的是,由于Stack继承了Vector,因此它也是一个非常古老的Java集合类,它是线程安全的,性能比较差,因此现在的程序中一般比较少使用Stack类。如果程序需要使用“栈”这种数据结构,则可以考虑使用LinkedList。

LinkedList也是List的实现类,它是一个基于链表实现的List类,对于顺序访问集合中的元素进行了优化,特别是插入、删除元素时速度非常快。LinkedList既实现了List接口,也实现了Deque接口,由于实现了Deque接口,因此可以作为栈来使用。

固定长度的List

Arrays工具类里提供了asList(T… a)方法,该方法可以把一个数组或指定个数的对象转换成一个List集合,这个List集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例。

Arrays.Arraylist是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素。

public class FixedSizeList {

    public static void main(String[] args) {
        List fixedList = Arrays.asList("疯狂Java讲义", "轻量级Java EE企业应用实战");
        //获取fixedList的实现类,将输出Arrays$ArrayList
        System.out.println(fixedList.getClass());

        for (int i=0; i<fixedList.size(); i++){
            System.out.println(fixedList.get(i));
        }

        //试图增加、删除元素都会引发异常
        fixedList.add("疯狂Android讲义");
        fixedList.remove("疯狂Java讲义");


    }

}
class java.util.Arrays$ArrayList
Exception in thread "main" java.lang.UnsupportedOperationException
疯狂Java讲义
轻量级Java EE企业应用实战
	at java.util.AbstractList.add(AbstractList.java:148)
	at java.util.AbstractList.add(AbstractList.java:108)
	at com.sunrise.eHealth.day0612.FixedSizeList.main(FixedSizeList.java:18)

Queue集合

Queue用于模拟队列这种数据结构,队列通常是指先进先出(FIFO)的容器。通常,队列不允许随机访问队列中的元素。

Queue接口中定义了如下几个方法。
将指定元素加入此队列的尾部

boolean add(E e);

获取队列头部的元素,但是不删除该元素

E element();

将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add方法更好

boolean offer(E e);

获取队列头部的元素,但是不删除该元素,如果此队列为空,则返回null

E peek();

获取队列头部的元素,并删除该元素。如果此队列为空,则返回null

E poll();

获取队列头部的元素,并删除该元素

E remove();

Queue接口有一个PriorityQueue实现类。除此之外,Queue还有一个Deque接口,Deque代表一个双端队列,双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可当成队列使用,也可当成栈使用。Java为Deque提供了ArrayDeque和LinkedList两个实现类。

PriorityQueue实现类

PriorityQueue是一个比较标准的队列实现类。之所以说它是比较标准的队列实现,而不是绝对标准的队列实现,是因为PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。因此当调用peek方法或者poll方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素。从这个意义上来看,PriorityQueue已经违反了队列的最基本规则:先进先出(FIFO)。

Deque接口和ArrayDeque实现类

Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。

将指定元素插入该双端队列的开头

void addFirst(E e);

将指定元素插入该双端队列的末尾

void addLast(E e);

返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素

Iterator<E> descendingIterator();

获取但不删除双端队列的第一个元素

E getFirst();

获取但不删除双端队列的最后一个元素

E getLast();

将指定元素插入该双端队列的开头

boolean offerFirst(E e);

将指定元素插入该双端队列的末尾

boolean offerLast(E e);

获取但不删除该双端队列的第一个元素;如果此双端队列为空,则返回null

E peekFirst();

获取但不删除该双端队列的最后一个元素;如果此双端队列为空,则返回null

E peekLast();

获取并删除该双端队列的第一个元素;如果此双端队列为空,则返回null

E pollFirst();

获取并删除该双端队列的最后一个元素;如果此双端队列为空,则返回null

E pollLast();

ArrayList和ArrayDeque两个集合类的实现机制基本相似,它们的底层都采用一个动态的,可重分配的Object[]数组来存储集合元素,当集合元素超出了该数组的容量时,系统会在底层重新分配一个Object[]数组来存储集合元素。

LinkedList实现类

LinkedList类是一个List接口的实现类,这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,因此它可以被当成双端队列来使用,自然也可以被当成栈来使用了。

public class LinkedListTest {

    public static void main(String[] args) {
        LinkedList books = new LinkedList();
        //将字符串元素加入队列的尾部
        books.offer("疯狂Java讲义");
        //将一个字符串元素加入栈的顶部
        books.push("轻量级Java EE企业应用实战");
        //将字符串元素添加到队列的头部(相当于栈的顶部)
        books.offerFirst("疯狂Android讲义");
        for (int i=0; i<books.size(); i++){
            System.out.println(books.get(i));
        }

        //访问但不删除栈顶的元素
        System.out.println(books.peekFirst());
        //访问但不删除队列的最后一个元素
        System.out.println(books.peekLast());
        //将栈顶的元素弹出栈
        System.out.println(books.pop());
        //下面输出将看到队列中第一个元素被删除
        System.out.println(books);

        //访问并删除队列的最后一个元素
        System.out.println(books.pollLast());
        //下面输出将看到队列中只剩下中间一个元素
        System.out.println(books);



    }

}

疯狂Android讲义
轻量级Java EE企业应用实战
疯狂Java讲义
疯狂Android讲义
疯狂Java讲义
疯狂Android讲义
[轻量级Java EE企业应用实战, 疯狂Java讲义]
疯狂Java讲义
[轻量级Java EE企业应用实战]

LinkedList与ArrayList、ArrayDeque的实现机制完全不同,ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能非常出色(只需要改变指针所指的地址即可)。

各种线性表的性能分析

Java提供的List就是一个线性表接口,而ArrayList、LinkedList又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。Queue代表了队列,Deque代表了双端队列(既可作为队列使用,也可作为栈使用)。

各种集合的性能对比

List集合_第1张图片
因为数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好。所有的内部以数组作为底层实现的集合在随机访问时性能较好;而内部以链表作为底层实现的集合在执行插入、删除操作时有很好的性能;进行迭代操作时,以链表作为底层实现的集合比以数组作为底层实现的集合性能好。

public class PerformanceTest {

    public static void main(String[] args) {
        //创建一个字符串数组
        String[] tst1 = new String[900000];
        //动态初始化数组元素
        for (int i=0; i<900000; i++){
            tst1[i] = String.valueOf(i);
        }
        ArrayList al = new ArrayList();
        //将所有的数组元素加入ArrayList集合中
        for (int i=0; i<tst1.length; i++){
            al.add(tst1[i]);
        }
        LinkedList ll = new LinkedList();
        //将所有的数组元素加入LinkedList集合中
        for (int i=0; i<tst1.length; i++){
            ll.add(tst1[i]);
        }

        //迭代访问ArrayList集合的所有元素,并输出迭代时间
        long start = System.currentTimeMillis();
        for (Iterator it = al.iterator(); it.hasNext();){
            it.next();
        }
        System.out.println("迭代ArrayList集合元素的时间:" + (System.currentTimeMillis() - start));

        //迭代访问LinkedList集合的所有元素,并输出迭代时间
        start = System.currentTimeMillis();
        for (Iterator it = ll.iterator(); it.hasNext();){
            it.next();
        }

        System.out.println("迭代LinkedList集合元素的时间:" + (System.currentTimeMillis() - start));


    }


}
迭代ArrayList集合元素的时间:31
迭代LinkedList集合元素的时间:16

由运行结果可知,迭代ArrayList集合的时间略大于迭代LinkedList集合的时间。
关于使用List集合有如下建议

  • 如果需要遍历List集合元素,对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素,这样性能更好;对于LinkedList集合,则应该采用迭代器(Iterator)来遍历集合元素
  • 如果需要经常执行插入、删除操作来改变List集合的大小,则应该使用LinkedList集合,而不是ArrayList。使用ArrayList、Vector集合需要经常重新分配内存数组的大小,其时间开销常常是使用LinkdeList的时间开销的几十倍,效果很差
  • 如果有多个线程需要同时访问List集合中的元素,可考虑使用Collections将集合包装成线程安全的集合

你可能感兴趣的:(Java基础)