Java编程思想 学习笔记11

时间:2021-11-07 08:18:06

十一、持有对象 

通常,程序总是根据运行时才知道的某些条件去创建新对象。在此之前,不会知道所需对象的数量,甚至不知道确切的类型。

Java实用库还提供了一套相当完整的容器类来解决这个问题,其中基本的类型是List、Set、QueueMap

1.泛型和类型安全的容器   通过使用泛型,就可以在编译器防止将错误类型的对象放置到容器中。

2.基本概念

  Java容器类类库的用途是“保存对象”,并将其划分为两个不同的概念:

  1)Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照牌堆规则来确定对象产生的顺序(通常与被插入的顺序相同)。

  2)Map。一组成对的“键值对”对象,允许你使用键来查找值。

  尽管并非总是如此,但是在理想情况下,你编写的大部分代码都是与这些接口打交道,并且你唯一需要指定所使用的精确类型的地方就是在创建的时候。因此可以像下面一样创建一个List:  List<Apple> apples = new ArrayList<Apple>();

  ArrayList已经被向上转型为List。因此,你应该创建一个具体类的对象,将其转型为对应的接口,然后在其余的代码中都用这个接口。

  这种方式并非总能奏效,因为某些类具有额外的功能,例如,LinkedList具有在List接口中未包含的额外方法。如果你需要使用这些方法,就不能将它们向上转型为更通用的接口。

  Collection接口概括了序列的概念——一种存放一组对象的方式。所有的Collection都可以用foreach语法遍历。

3.添加一组元素

  在java.util包中的ArraysCollections类中都有很多实用方法,可以在一个Collection中添加一组元素。

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

  你也可以直接使用Arrays.asList()的输出,将其当作List,但是在这种情况下,其底层表示的是数组,因此不能调整尺寸。

  Map更加复杂,除了用另一个Map之外,Java标准类库没有提供其他任何自动初始化它们的方式。

4.容器的打印

  你必须使用Arrays.toString()来产生数组的可打印表示,但是打印容器无需任何帮助。

  有三种基本风格的MapHashMap、TreeMapLinkedMap。与HashSet一样,HashMap提供了最快的查找技术,也没有按照任何明显的顺序来保存其元素。TreeMap按照比较结果的升序保存键,而LinkedMap则按照插入顺序保存键,同时还保留了HashMap的查询速度。

5.List

  List承诺可以将元素维护在特定的序列中。有两种类型的List:

  1)基本的ArrayList,它擅长于随机访问元素,但是在List的中间插入和移除元素时较慢。

  2)LinkedList,它通过代价较低的在List中间进行插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList更大。

  当确定一个元素是否属于某个List,发现某个元素的索引,以及从某个List中移除一个元素时,都会用到equals()方法。

  subList()方法允许从较大的列表中创建出一个片段。retainAll()方法是一种有效的“交集”操作。请再次注意,所产生的行为依赖于equals()方法。removeAll()方法的行为也是基于equals()方法的,它从List中移除在参数List中的所有元素。

  toArray()方法是将任意的Collection转换为一个数组。这是一个重载方法,其无参数版本返回的是Object数组,但是如果你向这个重载版本传递目标类型的数据,那么它将产生指定类型的数据。

6.迭代器

  任何容器类,都必须有某种方式可以插入元素并将它们再次取回。毕竟,持有对象是容器最基本的工作。对于Listadd()是插入元素的方法之一,而get()是取出元素的方法之一。

  如果从更高层的角度思考,会发现这里有个缺点:要使用容器,必须对容器的确切类型编程。如何能不关心容器的类型而只面向容器编码以应用于不同类型的容器?

  迭代器(也是一种设计模式)的概念可以用于达成此目的。迭代器是一个对象,它的工作是遍历并选择序列中的对象,而客户端程序员不必知道或关心该序列底层的结构。此外,迭代器通常被称为轻量级对象:创建它的代价小。因此,经常可以见到对迭代器有些奇怪的限制;例如,Java的Iterator只能单向移动,这个Iterator只能用来:

  1)使用方法iterator()要求容器返回一个IteratorIterator将准备好返回序列的第一个元素。

  2)使用next()获得序列中的下一个元素。

  3)使用hasNext()检查序列中是否还有元素。

  4)使用remove()将迭代器新近返回的元素删除。

  Iterator还可以移除由next()产生的最后一个元素,这意味着在调用remove()之前必须先调用next()

  Iterator真正威力:能将遍历序列的操作于序列底层的结构分离。

  ①ListIerator

  ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问,它可以双向移动,还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个元素。你可以通过调用listIterator()方法产生一个指向List开始处的ListIterator,并且还可以通过调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator

7.LinkedList

  LinkedList也像ArrayList一样实现了基本的List接口,但是它执行某些操作(插入和删除)时要高效一些,但在随机访问数据方面却要逊色一些。

  LinkedList还添加了可以使其作为栈、队列或双端队列的方法。

8.Stack

  “栈”通常是“后进先出”(LIFO)的容器。

  LinkedList具有能够直接实现栈的所有功能的方法。

  实现栈的行为时,最好使用组合。因为如果使用了继承,会产生具有LinkedList的其他所有方法的类(Java1.0的设计者在创建java.util.Stack时,就犯了这个错误)。

9.Set

  Set不保存重复的元素。Set中最常被使用的是测试归属性,你可以很容易地询问某个对象是否在某个Set中。(contains()方法)

  Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)Set是基于对象的值来确定归属的。

  出于速度原因的考虑,HashSet使用了散列。TreeSet将元素存储在红-黑树数据结构中,而HashSet使用的是散列函数。LinkedHashList因为查询速度的原因也使用了散列,但是它使用了链表来维护元素的插入顺序。

10.Map

  将对象映射到其他对象的能力是一种解决编程问题的杀手锏。

  Map可以返回它的键的Set,它的值的Collection,或者它的键值对的Set

11.Queue

  队列是一个典型的先进先出(FIFO)的容器。即从容器的一段放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。队列常被当作一种可靠的将对象从程序的某个区域传播到另一个区域的途径。队列在并发编程中特别重要,因为它们可以安全地将对象从一个任务传输给另一个任务。

  LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现。

  offer()方法在允许的情况下,将一个元素插入到队尾,或者返回falsepeek()element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,而element()会抛出NoSuchElementException异常。poll()remove()方法将移除并返回队头,但是poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。

  通过将LinkedList向上转型为QueueQueue接口窄化了对LinkedList的方法的访问权限,以使得只有恰当的方法才能使用。

  ①PriorityQueue 

  先进先出描述了最典型的队列规则。队列规则是指在给定一组队列中的元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该是等待时间最长的元素。

  优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。

  当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的顺序将使用对象在队列中的自然顺序(最小的值拥有最高的优先值),但是可以通过自己提供Comparator来修改这个顺序。

12.Collection和Iterator

  Collection是描述所有序列容器的共性的根接口。它可能会被认为是一个“附属接口”,即因为要表示其他若干个接口的共性而出现的接口。另外,java.util.AbstractCollection类提供了Collection的默认实现,使得你可以创建AbstractCollection的子类型,而其中没有不必要的代码重复。

  使用接口描述的一个理由是它可以使我们能够创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多的对象类型。因此,如果我编写的方法将接受一个Collection,那么该方法就可以应用于任何实现了Collection的类。但是在标准的C++类库中并没有其容器的任何公共基类——容器之间的所有共性都是通过迭代器达成的。在Java中,这两种方法绑定在了一起,因为实现Collection就意味着需要提供iterator()方法。

  当你要实现一个不是Collection的外部类时,由于让它实现Collection接口可能非常困难或麻烦,因此使用Iterator就会变得非常吸引人。

13.Foreach与迭代器

  foreach可以应用于任何Collection对象。之所以能够工作,是因为Java SE5引入了新的被称为Iterable的接口,该接口包含了一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。

  数组不是一个Iterable。

  ①适配器方法惯用法 

  如果现有一个Iterable类,你想要添加一种或多种在foreach语句中使用这个类的方法,应该怎么做呢?例如,你希望可以选择以向前的方向或是向后的方向迭代一个单词列表。如果直接继承这个类,并覆盖iterator()方法,你只能替换现有方法,而不能实现选择。

  一种解决方案时所谓适配器方法的惯用法。“适配器”部分来自于设计模式,因为你必须提供特定接口以满足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()-;
public boolean hasNext() { return current > -; }
public T next() { return get(current--); }
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};
}
} public class AdapterMethodIdiom {
public static void main(String[] args) {
ReversibleArrayList<String> ral =
new ReversibleArrayList<String>(
Arrays.asList("Thank you".split(" ")));
for(String s : ral)
System.out.print(s+" ");
System.out.println();
for(String s : ral.reversed())
System.out.print(s+" ");
}
}
/*Output
Thank you
you Thank
*/

  

14.总结 

  Java提供了大量持有对象的方式:

  1)数组将数字与对象联系起来。它保存类型明确的对象,查询对象时,不需要对结果进行类型转换。它可以是多维的,可以保存基本类型的数据。但是,数组一旦生成,其容量就不能改变。

  2)Collection保存单一的元素,而Map保存相关联的键值对。有了Java的泛型,你就可以指定容器中存放的对象类型,因此你就不会将错误类型的对象放置到容器中,并且在从容器中获取元素时也不必进行类型转换。各种Collection和各种Map都可以在你向其中添加更多的元素时,自动调整其尺寸。容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所持有的包装器类型之间的双向转换。

  3)像数组一样,List也建立数字索引与对象的关联,因此,数组和List都是排好序的容器。List能够自动扩充容量。

  4)如果要进行大量的随机访问,就使用ArrayList;如果经常从表之间插入或删除元素,则应该使用LinkedList

  5)各种Queue以及栈的行为,由LinkedList提供支持。

  6)Map是一种将对象(而非数字)与对象相关联的设计。HashMap设计用来快速访问;而TreeMap保持“键”始终处于排序状态,所以没有HashMap快。LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问的能力。

  7)Set不结实重复元素。HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态。LinkedHashSet以插入顺序保存元素。

  8)新程序中不应该使用过时的VectorHashTableStack

  以下是Java容器的简图,很详细

Java编程思想 学习笔记11