Java核心技术基础知识学习之Java集合(二)


七、Java集合

7.4 List 集合

List 集合代表一个元素有序、可重复的集合,集合中每个元素都有对应的顺序索引。List 集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List 集合默认按元素的添加顺序设置元素的索引。

7.4.1 List 接口和 ListIterator 接口

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

  • void add(int index, Object element):将元素 element 插入到 List 集合的 index 处(即 index 处的元素即为 element);
  • boolean addAll(int index, Collection c):将集合 c 所包含的所有元素都插入到 List 集合的 index 处;
  • Object get(int index):返回集合 index 索引处的元素;
  • int indexOf(Object o):返回对象 o 在 List 集合中第一次出现的位置索引;
  • int lastIndexOf(Object o):返回对象 o 在 List 集合中最后一次出现的位置索引;
  • Object remove(int index):删除并返回 index 索引处的元素;
  • Object set(int index, Object element):将 index 索引处的元素替换成 element 对象,返回被替换的旧元素;
  • List subList(int fromIndex, int toIndex):返回从索引 fromIndex(包含)到 toIndex(不包含)处所有集合组成的子集合;
  • void replaceAll(UnaryOperator operator):根据 operator 指定的计算规则重新设置 List 集合的所有元素;
  • void sort(Comparator c):根据 Comparator 参数对 List 集合的元素排序;
import java.util.ArrayList;
import java.util.List;

public class ListTest {
    public static void main(String[] args) {
        List books = new ArrayList();
        books.add(new String("Java核心技术"));
        books.add(new String("Java编程思想"));
        books.add(new String("JVM虚拟机"));
        System.out.println(books);
        //将新字符串插入到第二个位置
        books.add(1, new String("Java性能调优指南"));
        //for循环迭代访问books集合
        for (int i = 0; i < books.size(); i++) {
            System.out.println(books.get(i));
        }
        //删除第三个元素
        books.remove(2);
        //判断指定元素在List集合中的位置
        System.out.println(books.indexOf(new String("Java性能调优指南")));
        //将第二个元素替换成新的字符串对象
        books.set(1, new String("Effective Java"));
        //将books集合中第二个是元素到第三个元素截取成子集合
        System.out.println(books.subList(1, 2));
    }
}

注意到,程序试图返回新字符串对象在 List 集合中的位置,实际上 List 集合中并未包含该字符串对象。因为 List 集合添加字符串对象时,添加的是通过 new 关键字创建的新字符串对象,显然两者不是同一对象,但 List 的indexOf方法依然可以返回 1。List 判断两个对象相等的标准是通过 equals() 方法比较返回 true 即可。

当调用 List 的 set(int index, Object element) 方法来改变 List 集合指定索引处的元素时,指定的索引必须是 List 集合的有效索引

Java 8 为 List 集合增加了sort()replaceAll()两个常用的默认方法,其中 sort() 方法需要一个 Comparator 对象来控制元素排序,程序可使用 Lambda 表达式作为参数;而replaceAll()方法则需要一个 unaryOperator 来替换所有集合元素,UnaryOperator 也是一个函数式接口,因此程序也可以使用 Lambda 表达式作为参数。

import java.util.ArrayList;
import java.util.List;

public class ListTest2 {
    public static void main(String[] args) {
        List books = new ArrayList();
        books.add(new String("Java核心技术"));
        books.add(new String("Java编程思想"));
        books.add(new String("JVM虚拟机"));
        //使用目标类型为Comparator的Lambda表达式对List集合排序
        books.sort((o1, o2) -> ((String) o1).length() - ((String) o2).length());
        System.out.println(books);
        //使用目标类型为UnaryOperator的Lambda表达式来替换集合中所有元素
        //该Lambda表达式控制使用每个字符串长度作为新的集合元素
        books.replaceAll(ele -> ((String)ele).length());
        System.out.println(books);//输出[6, 8, 8]
    }
}

List 集合进行排序,传给sort()方法的 Lambda 表达式指定的排序规则:字符串长度越长,字符串越大,因此 List 集合中的字符串会按由短到长的顺序排列。另外replaceAll()方法的 Lambda 表达式指定了替换集合元素的规则:直接用集合元素(字符串)的长度作为新的集合元素,执行该方法后集合元素被替换字符串长度。
与 Set 只提供一个iterator()方法不同,List 还额外提供了一个listIterator()方法,该方法返回一个 ListIterator 对象,ListIterator 接口继承了 Iterator 接口,提供专门操作 List 的方法。ListIterator 接口在 Iterator 接口基础上增加如下方法:

  • boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素;
  • Object previous():返回该迭代器的上一个元素;
  • void add(Object o):在指定位置插入一个元素;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class ListIteratorTest {
    public static void main(String[] args) {
        String[] books = {"Java核心技术", "Java编程思想", "JVM虚拟机"};
        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());
        }
    }
}

7.4.2 ArrayList 和 Vector 类

ArrayList 和 Vector 作为 List 类的典型实现,完全支持前面介绍的 List 接口的全部功能。ArrayList 和 Vector 类都是基于数组实现的 List 类,所以 ArrayList 和 Vector 类封装了一个动态的、允许再分配的 Object[] 数组。
ArrayList 或 Vector 对象使用 initialCapacity 参数来设置该数组的长度,当向 ArrayList 或 Vector 中添加元素超过该数组的长度时,他们的 intialCapacity 会自动增加。如果向 ArrayList 或 Vector 集合添加大量元素时,可使用 ensureCapacity(int minCapacity) 方法一次性增加 intialCapacity。这样可以减少重分配的次数,从而提高性能。如果创建空的 ArrayList 或 Vector 集合时不指定 initialCapacity 参数,则 Object[] 数组的长度默认为10。除此之外,ArrayList 和 Vector 还提供了如下两个方法来重新分配数组:

  • void ensureCapacity(int minCapacity):将 ArrayList 或 Vector 集合的 Object[] 数组长度增加大于或等于 minCapacity 值;
  • void trimToSize():调整 ArrayList 或 Vector 集合的 Object[] 数组长度为当前元素的个数。调用该方法可减少 ArrayList 或 Vector 集合对象占用的存储空间;

ArrayList 和 Vector 的显著区别是:ArrayList 是线程不安全的,当多个线程访问同一个 ArrayList 集合时,如果有超过一个线程修改了 ArrayList 集合,则程序必须手动保证该集合的同步性。 Vector 集合则是线程安全的,无须程序保证该集合的同步性,也因此性能比 ArrayList 的性能要低。实际上,即使需要保证 List 集合线程安全,也同样不推荐使用 Vector 实现类。后面会介绍 Collections 工具类,它可以将 ArrayList 变成线程安全的。
Vector 还提供了 Stack 子类,用于模拟栈数据结构,继承 Vector 同样也是线程安全性能较差,因此也应该尽量少用。如果需要使用栈,可以考虑使用 ArrayDeque。ArrayDeque 实现了 List 和 Deque 接口,底层是基于数据的实现,因此性能很好。

7.4.3 固定长度的 List

Arrays 工具类中提供了asList(Object... a)方法,该方法可以将一个数组或指定个数的对象转换成一个 List 集合,这个 List 集合既不是 ArrayList 实现类的实例,也不是 Vector 实现类的实例,而是 Arrays 的内部类 ArrayList 的实例
Arrays.ArrayList 是一个固定长度的 List 集合,程序只能遍历访问该集合的元素,不可增加、删除该集合里的元素。

import java.util.Arrays;
import java.util.List;

public class FixedSizeLst {
    public static void main(String[] args) {
        List fixedList = Arrays.asList("Java编程思想", "Java虚拟机");
        //获取fixedList的实现类,将输出Arrays$ArrayList
        System.out.println(fixedList.getClass());
        //使用方法引用遍历集合元素
        fixedList.forEach(System.out::println);
        //试图增加或删除元素都会引发UnsupportedOperationException
        //fixedList.add("Java核心技术");
        //fixedList.remove("Java编程思想");
    }
}

7.5 Queue 集合

Queue 用于模拟队列这种数据结构,定义了如下几个方法:

  • void add(Object e):将指定元素加入此队列的尾部;
  • Object element():获取队列的头部元素,但是不删除元素;
  • boolean offer(Object e):将指定元素加入此队列的尾部,当使用有容量限制的队列时,此方法通常比 add(Object e) 方法更好;
  • Object peek():获取队列头部的元素,但是不删除该元素,如果此队列为空,则返回 null;
  • Object poll():获取队列头部的元素,并删除该元素,如果队列为空,则返回null;
  • Object remove():获取队列头部的元素,并删除该元素;

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

7.5.1 PriorityQueue 实现类

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

import java.util.PriorityQueue;

public class PriorityQueueTest {
    public static void main(String[] args) {
        PriorityQueue pq = new PriorityQueue();
        pq.offer(6);
        pq.offer(-3);
        pq.add(1);
        pq.offer(2);
        pq.offer(5);
        //输出pq队列,并不是按照元素的加入顺序排列
        System.out.println(pq);
        int size = pq.size();
        for (int i = 0; i < size; i++) {
            System.out.println(pq.poll());
        }
    }
}

直接输出 PriorityQueue 时,可能看到该队列里的元素并没有很好地按大小进行排序,但这只是由于toString()方法的返回值影响。事实上,程序多次调用 PriorityQueue 集合对象的 poll() 方法,即可看到元素从小到大的顺序“移出队列”。
PriorityQueue 不允许插入 null 元素,它还需要对队列元素进行排序,PriorityQueue 的元素有两种排序方法,对元素的要求与 TreeSet 元素的要求一致:

  • 自然排序:采用自然排序的 PriorityQueue 集合中的元素必须实现了 Comparable 接口,而且应该是同一个类的多个实例,否则会导致 ClassCastException 异常;
  • 定制排序:创建 PriorityQueue 队列时传入 Comparator 对象,该对象负责对队列中的所有元素进行排序,采用定制排序时不要求队列元素实现 Comparable 接口;

7.5.2 Deque 接口与 ArrayDeque 实现类

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

  • void addFirst():将指定元素插入该双端队列的开头;
  • void addLast():将指定元素插入该双端队列的末尾;
  • Iterator descendingIterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素;
  • Object getFirst():获取但不删除该双端队列的第一个元素;
  • Object getLast():获取但不删除该双端队列的最后一个元素;
  • boolean offerFirst(Object o):将指定元素插入该双端队列的开头;
  • boolean offerLast(Object o):将指定元素插入该双端队列末尾;
  • Object peekFirst():获取但不删除该双端队列的第一个元素,如果此双端队列为空,则返回 null;
  • Object peekLast():获取但不删除该双端队列的最后一个元素,如果此双端队列为空,则返回 null;
  • Object pollFirst():获取并删除该双端队列的第一个元素,如果此双端队列为空,则返回 null;
  • Object pollLast():获取并删除该双端队列的最后一个元素,如果此双端队列为空,则返回 null;
  • Object pop(栈方法):pop 出该双端队列所表示栈的栈顶元素,相当于 removeFirst();
  • void push(Object e)(栈方法):将一个元素 push 进该双端队列所表示的栈的栈顶,相当于 addFirst(e);
  • Object removeFirst():获取并删除该双端队列的第一个元素;
  • Object removeFirstOccurrence(Object o):删除该双端队列的第一次出现的元素 o;
  • Object removeLast():获取并删除该双端队列的最后一个元素;
  • Object removeLastOccurrence(Object o):删除该双端队列的最后一次出现的元素 o;

Deque 不仅可以当成双端队列来使用,而且可以被当成栈来使用,因为该类里还包含了 pop(出栈)、push(入栈)两个方法。

Queue 方法Deque 方法Stack 方法
add(e) / offer(e)addFirst(e) / offerLast(e)push(e)
remove() / poll()removeFirst() / pollFirst()pop()
element() / peek()getFirst() / peekFirst()peek()

Deque 接口提供了一个典型的实现类:ArrayDeque,它是基于数组实现的双端队列,创建 Deque 同样可以指定一个 numElements 参数,该参数用于指定 Object[] 数组的长度;如果不指定 numElements 参数,Deque 底层数组的长度为 16

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

ArrayDeque 作为栈使用:

import java.util.ArrayDeque;

public class ArrayDequeStack {
    public static void main(String[] args) {
        ArrayDeque stack = new ArrayDeque();
        //元素入栈
        stack.push("Java编程思想");
        stack.push("Java核心技术");
        stack.push("Java虚拟机");
        System.out.println(stack);
        //访问第一个元素,但并不出栈
        System.out.println(stack.peek());
        //元素出栈
        System.out.println(stack.pop());
        System.out.println(stack);
    }
}

ArrayDeque 作为队列使用:

import java.util.ArrayDeque;

public class ArrayDequeQueue {
    public static void main(String[] args) {
        ArrayDeque queue = new ArrayDeque();
        //依次将三个元素加入队列
        queue.offer("Java编程思想");
        queue.offer("Java核心技术");
        queue.offer("Java虚拟机");
        System.out.println(queue);
        //访问队列头部元素,但不出poll队列
        System.out.println(queue.peek());
        //poll出队列的第一个元素
        System.out.println(queue.poll());
        System.out.println(queue);
    }
}

7.5.3 LinkedList 实现类

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

import java.util.LinkedList;

public class LinkedListTest {
    public static void main(String[] args) {
        LinkedList books = new LinkedList();
        //将字符串元素加入队列的尾部
        books.offer("Java编程思想");
        //将字符串元素添加到栈的顶部
        books.push("Java核心技术");
        //将字符串元素添加到队列的头部(相当于栈的顶部)
        books.addFirst("Java虚拟机");
        //以List的方式(按索引的方式)遍历集合元素
        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.pollFirst());
        System.out.println(books);
        //访问并删除队列最后一个元素
        System.out.println(books.pollLast());
        System.out.println(books);
    }
}

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

对于所有的内部基于数组的集合实现,例如 ArrayList、ArrayDeque 等,使用随机访问的性能比使用 Iterator 迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。

7.5.4 各种线性表的性能分析

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

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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值