【Java编程思想】11.持有对象

如果一个程序只包含固定数量的且生命周期都是已知的对象,那么这是一个非常简单的程序。

Java 类库中提供一套容器类,来存储比较复杂的一组对象。其中有 ListSetQueueMap 等。这些类也被称为集合类,Java 的类库中使用 Collection 这个名字指代该类库的一个特殊子集(其实 Java 中大部分容器类都实现了 Collection 接口)。


11.1 泛型和类型安全的容器

在 Java SE5 之前的容器,编译器是允许向容器中插入不正确的类型的。因此在获取容器中对象时,一旦转型就会抛出一个异常。
一个类如果没有显式地声明继承自哪个类,那么他就自动地继承自 Object,因此对于容器来说,添加不同的类,无论是编译器还是运行期都不会有问题,问题在于使用容器中存储的对象时,会引发意想不到的错误。
因此,Java SE5引入了泛型的支持(15章有详解)。通过使用泛型,可以在编译期防止将错误类型的对象放置到容器中。

List<Apple> list = new ArrayList();

在定义泛型之后,从容器中取出对象时,容器会直接转成泛型对应的类型。同时向上转型也可以作用在泛型上。


11.2 基本概念

Java 容器类库划分为两种:

  1. Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List 必须按照插入的顺序保存元素,Set 不能有重复元素,Queue 按照派对规则来确定对象产生的顺序(通常与其元素被插入的顺序相同)。
  2. Map。一组成对的键值对对象,允许使用键来查找值。映射表允许我们使用另一个对象来查找某个对象,它也被称作关联数组,因为他将某些对象与另外一些对象关联在了一起;或者被称为字典,因为你可以使用键对象来查找值对象

使用容器的时候,可能有些情况不需要使用向上转型的方式,例如 LinkedList 具有 List 接口中未包含的方法;TreeMap 具有 Map 中未包含的方法,因此如果要使用这些方法,就不能将他们向上转型为接口。

所有的 Collection 都可以用 foreach 语法遍历。


11.3 添加一组元素

添加一组元素有多种方法:

  • Arrays.asList() 方法,接收一个数组或者是逗号分隔的元素列表(使用可变参数),并将其转换为一个 List 对象。
  • Collections.addAll() 方法,接收一个 Collection 对象,以及一个数组或是用逗号分隔的列表,并将元素添加到 Collection 中。

两者的使用都有限制,Arrays.asList() 方法的输出在底层的表示是数组,因此不能调整尺寸。而 Collections.addAll() 方法只能接受另一个 Collection 对象作为参数。

像如下这种初始化,可以告诉编译器,由 Arrays.asList() 方法产生的 List 类型(实际的目标类型)。这种成为显式类型参数说明

List<Snow> snow4 = Arrays.<Snow>asList(new Light(), new Heavy());

11.4 容器的打印

数组的打印必须借助 Arrays.toString() 来表示(或者遍历打印);但是容器的打印可以直接使用 print(默认就能生成可读性很好的结果)。

  • Collection 在每个“槽”中只能保存一个元素
    • List 以特定的顺序保存一组元素。
    • Set 元素不能重复。
    • Queue 只允许在容器的一“端”插入对象,并从另一“端”移出对象。
  • Map 在每个“槽”内保存两个对象,即和与之相关联的

关于各种容器的实现类型特点:

  • ArrayList:按插入顺序保存元素,性能高
  • LinkedList:按插入顺序保存元素,性能略低于 ArrayList
  • HashSet:最快的获取元素方式
  • TreeSet:按照比较结果升序保存对象,注重存储顺序
  • LinkedHashSet:按照添加顺序保存对象
  • HashMap:提供最快的查找技术,没有特定顺序
  • TreeMap:敖钊比较结果的升序保存键
  • LinkedHashMap:按照插入顺序保存键

11.5 List

  • ArrayList:擅长于随机访问元素,在 List 的中间插入和移出元素时较慢。
  • LinkedList:通过代价较低的在 List 中间进行的插入和删除操作,提供优化的顺序访问。另外其特性集更大。

与数组不同,List 允许在他被创建之后添加元素、移除元素或者自我调整尺寸,是一种可修改的序列。

关于 List 的方法:

  • contains():确定对象是否在列表中
  • remove():从列表中移出元素
  • indexOf():获取对象在列表中的索引编号
  • subList():从较大的列表中创建出一个片段
  • containsAll():判断片段是否包含于列表
  • retainAll():求列表交集
  • removeAll():移出指定的全部元素
  • replace():在指定索引处,用指定参数替换该位置的元素
  • addAll():在初始列表中插入新的列表
  • isEmpty():校验列表是否为空
  • clear():清空列表
  • toArray():列表转数组

11.6 迭代器

迭代器(也是一种设计模式)是一个对象那个,它的工作室遍历并选择序列中的对象,使用者不需要关心该序列的底层结构。

Java 中有迭代器 Iterator,只能单向移动,Iterator 只能用来:

  1. 使用方法 iterator() 要求容器返回一个 IteratorIterator 将准备好返回序列的第一个元素。
  2. 使用 next() 获得序列中的下一个元素。
  3. 使用 hasNext() 检查序列中是否还有元素。
  4. 使用 remove() 将迭代器新近返回的元素删除。

使用 Iterator 时不需要关心容器中的元素数量,只需要向前遍历列表即可。
Iterator 可以移出有 next() 产生的最后一个元素,这意味着在调用 remove() 之前需要先调用 next()

ListIterator 是加强版的 Iterator 子类型,只能用于 List 类的访问

  • 区别于 IteratorListIterator 是可以双向移动的
  • 还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引
  • 可以使用 set() 方法替换它访问过的最后一个元素
  • 可以通过调用 listIterator() 方法产生一个指向 List 开始处的 ListIterator
  • 还可以通过调用 listIterator(n) 方法创建一个一开始就指向列表索引为 n 的元素处的 ListIterator

11.7 LinkedList

LinkedList 在执行插入和删除时效率更高,随机访问操作方面要逊色一些。
除此之外,LinkedList 还添加了可以使其用作栈、队列和双端队列的方法。

  • getFirst()/element():返回列表头,列表为空是抛出 NoSuchElementException
  • peek():返回列表头,列表为空返回 null。
  • removeFirst()/remove():移出并返回列表头,列表为空是抛出 NoSuchElementException
  • poll():移出并返回列表头,列表为空返回 null。
  • addFirst()/add()/addLst():将某元素插入到列表尾部。
  • removeLast():移出并返回列表最后一个元素。

LinkedListQueue 的一个实现。对比 Queue 接口,可以发现 QueueLinkedList 的基础上添加了 element()offer()peek()poll()remove()方法。


11.8 Stack

通常是指”后进先出”(LIFO)的容器。有时栈也被称为叠加栈,因为最后压栈的元素最先出栈。
LinkedList 具有能够直接实现一个栈的所有功能和方法,因此可以直接将 LinkedList 作为栈使用。

public class Stack<T> {
    private LinkedList<T> storage = new LinkedList<T>();
    public void push(T v) { storage.addFirst(v); }
    public T peek() { return storage.getFirst(); }
    public T pop() { return storage.removeFirst(); }
    public boolean empty() { return storage.isEmpty(); }
    public String toString() { return storage.toString(); }
}

11.9 Set

Set 是基于对象的值来确定归属性的,具有与 Collection 完全一样的接口(实际上 Set 就是 Collection,只是行为不同--一种继承与多态的典型应用,表现不同的行为)。

  • HashSet 使用了散列(更多17章介绍),因此存储元素没有任何规律。
  • TreeSet 将元素存储在红黑树数据结构中,输出结果是排序的。默认按照字典序排序(A-Z,a-z),如果想按照字母序排序(Aa-Zz),可以指定 new TreeSet<String>(String.CASE_INSENSITIVE_ORDER)
  • LinkedHashSet 使用了散列,但是也用了链表结构来维护元素的插入顺序。

11.10 Map

get(key) 方法会返回与键关联的值,如果键不在容器中,则返回 null。
containKey()containValue() 可以查看键和值是否包含在 Map 中。

Map 实际上就是讲对象映射到其他对象上的工具。


11.11 Queue

队列是一个典型的先进先出(FIFO)的容器。从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。
队列常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径-->例如并发编程中,安全的将对象从一个任务传输给另一个任务。

LinkedListQueue 的一个实现,可以将其向上转型为 Queue
Queue 接口窄化了对 LinkedList 的方法的访问权限,以使得只有恰当的方法才可以使用,因此能够访问的 LinkedList 的方法会变少。

队列规则是指在给定一组队列中的元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该是等待时间最长的元素。
优先级队列声明下一个弹出元素时最需要的元素(具有最高的优先级)。PriorityQueue 提供了这种实现。当在 PriorityQueue 上调用 offer() 方法插入一个对象时,这个对象会在队列中被排序(通常这类队列的实现,会在插入时排序-维护一个堆-但是他们也可能在移出时选择最重要的元素)。默认的排序会使用对象在队列中的自然顺序,但是可以通过提供自己的 Comparator 来修改这个顺序。 这样能保证在对 PriorityQueue 使用 peek()poll()remove() 方法时,会先处理队列中优先级最高的元素。


11.12 Collection 和 Iterator

Collection 是描述所有序列容器的共性的根接口。java.util.AbstractCollection 类提供了 Collection 的默认实现,可以创建 AbstarctCollection 的子类型,其中不会有不必要的代码重复。
实现了 Collection 意味着需要提供 iterator() 方法。
(这章没太看懂,回头再看一遍)


11.13 Foreach 与迭代器

foreach 是基于 Iterator 接口完成实现的,Iterator 接口被 foreach 用来在序列中移动。任何实现了 Iterator 接口的类,都可以用于 foreach 语句。

System.getenv() 方法可以返回一个 MapSystem.getenv().entrySet() 可以产生一个有 Map.Entry 的元素构成的 Set,并且这个 Set 是一个 Iterable,因此它可以用于 foreach 循环。

foreach 语句可以用于数组或其他任何实现了 Iterable 的类,但是并不意味这数组肯定也是一个 Iterable,而且任何自动包装都不会自动发生。


在类实现 Iterable 接口时,如果想要添加多种在 foreach 语句中使用这个类的方法(在 foreach 中的条件中使用类的方法),有一种方案是适配器模式。-->在实现的基础上,增加一个返回 Iterable 对象的方法,则该方法就可以用于 foreach 语句。例如

// 添加一个反向的迭代器
class ReversibleArrayList<T> extends ArrayList<T> {
    public ReversibleArrayList(Collection<T> c) { super(c); }
    public Iterable<T> reversed() {
        return new Iterable<T>() {
            public Iterator<T> iterator() {
                return new Iterator<T>() {
                    int current = size() - 1;
                    @Override
                    public boolean hasNext() { return current > -1; }
                    @Override
                    public T next() { return get(current--); }
                    @Override
                    public void remove() { // Not implemented
                        throw new UnsupportedOperationException();
                    }};
            }};
    }
}
// 调用
for(String s : ral.reversed()) {
    System.out.print(s + " ");
}

使用 Collection.shuffle() 时,可以看到包装与否对于结果的影响。

Integer[] ia = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
List<Integer> list1 = new ArrayList<>(Arrays.asList(ia));
System.out.println("Before shuffling: " + list1);
Collections.shuffle(list1, rand);
System.out.println("After shuffling: " + list1);
System.out.println("array: " + Arrays.toString(ia));

List<Integer> list2 = Arrays.asList(ia);
System.out.println("Before shuffling: " + list2);
Collections.shuffle(list2, rand);
System.out.println("After shuffling: " + list2);
System.out.println("array: " + Arrays.toString(ia));

输出:

Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After shuffling: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9]
array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After shuffling: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
array: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]

从结果中可以看到,Collection.shuffle() 方法没有影响到原来的数组-->只是打乱了列表中的引用。
如果用 ArrayListArrays.asList(ia) 方法产生的结果包装起来,那么只会打乱引用;而不包装的时候,就会直接修改低层的数组。因此 Java 中数组和 List 的本质是不同的。


11.14 总结

Java 提供的持有对象的方式:

  1. 数组:数组保存明确类型的对象,查询对象的时候不需要对结果做类型转换。数组可以是多维的,也可以保存基本类型数据。但是数组的容量,在生成后就不能改变了。
  2. Collection/Map:Collection 保存单一的元素,Map 保存关联的键值对。在使用了泛型指定了容器存放的对象类型后,查询对象时便也不需要进行类型转换。CollectionMap 的尺寸是动态的,不能持有基本类型(但是自动包装机制-装箱-会将存入容器的基本类型进行数据转换)。
  3. List:数组和 List 都是排好序的容器,但是 List 能够自动扩充容量。大量随机访问使用 ArrayList,插入删除元素使用 LinkedList
  4. Queue:各种 Queue 以及栈的行为,由 LinkedList 提供支持。
  5. Map:Map 是一种将对象与对象相关联的设计。快速访问使用 HashMap,保持的排序状态使用 TreeMap,但是速度略慢,保持元素插入顺序以及快速访问能力使用 LinkedHashMap
  6. Set:Set 不接收重复元素。需要快速查询使用 HashSet,保持元素排序状态使用 TreeSet,保持元素插入顺序使用 LinkedHashSet
  7. 有一部分的容器已经过时不应该使用:VectorHashtableStack

下面是 Java 容器的简图:

  • 黑框:常用容器
  • 点线框:接口
  • 实线框:普通的(具体的)类
  • 空心箭头点线:一个特定的类实现了接口
  • 实心箭头:某个类可以生成箭头所指向类的对象

猜你喜欢

转载自www.cnblogs.com/chentnt/p/9791903.html