第十一章 持有对象
如果一个程序只包含固定数量的且其生命周期都是已知的对象,那么这是一个非常简单的程序
- 通常,程序总是根据运行时才知道的某些条件去创建新对象,在此之前,不会知道所需对象的数量,甚至不知道确切的类型。
- java 中有多种方式保存对象,比如数组,但是数组尺寸固定,过于受限。
- java 提供了一套完整的容器来保存对象,基本的类型包括 List 、 Set 、 Queue 、 Map 。这些对象也被称为集合类,由于 java 类库中使用了 Collection 这个名字来指代该类库的一个特殊子集,所以用 “ 容器 ” 来称呼他们。
- java 容器都可以自动的调整自己的尺寸。因此,与数组不同,在编程时,可以将任意数量的对象放置在容器中,而不用担心容器应该设置为多大。
1 . 泛型和类型安全的容器
- 最基本最可靠的容器 ArrayList , 可以把 ArrayList 当做 “ 可以自动扩充自身尺寸的数组 ” 。
使用 ArrayList 的方法 :
- 使用 add() 插入对象
- 使用 get() 访问这些对象
- 使用 size() 查看有多少元素添加了进来
- 如果一个类没有显示的声明继承自哪个类,那么它自动的继承自 Object 。
- 使用预定义的泛型 : ArrayList< xxx > , 其中尖括号括起来的是 类型参数( 可以有多个 ) , 它指定了这个容器实例可以保存的类型。通过使用泛型,可以在 编译期 防止将错误类型的对象放置到容器中。
- 当你使用泛型之后,在将元素从 List 中取出的时候,类型转换也不是必须的了。因为 List 知道他保存的是什么类型,因此会在调用 get() 时替你执行转型。
- 如果不需要使用每个元素的索引,可以使用 foreach 语法来选择 List 中的每个元素。
- 当你指定了某个类型作为泛型参数的时候,你并不仅限于只能将该确切类型的对象放置到容器中,向上转型也可以像作用于其他类型一样作用于泛型。可以将一个类的子类添加到被指定为这个类的对象的容器中。
2 . 基本概念
java 容器类类库的用途是 " 保存对象 " ,并将其划分为两个不同的概念
Collection , 一个独立元素的序列,这些元素都服从一条或多条规则。
- List 必须按照插入的顺序保存元素。
- Set 不能有重复的元素。
- Queue 按照排队规则来确定对象产生的顺序 ( 通常与他们被插入的顺序相同 )
Map , 一组成对的 “ 键值对 ” 对象,允许你使用键来查找值。
- ArrayList 允许你使用数字来查找值,因此在某种意义上讲,它将数字与对象关联在了一起 。
- 映射表 允许我们使用另一个对象来查找某个对象,他也被称为 “ 关联数组 ” , 因为他将某些对象与另外一些对象关联在了一起;或者被称为 “ 字典 ” , 因为你可以使用 键对象 来查找 值对象 , 就像在字典中使用单词来定义一样, Map 是强大的编程工具。
- 可以这样创建一个 List
java
Listapples = new ArrayList () ; ArrayList 已经被向上转型为了 List ,使用接口的目的在于如果你决定去修改你的实现,你所需的只是在创建出修改它。像下面这样:
List<Apple> apples = new LinkedList<Apple>
- 因此,应该创建一个具体类的对象,将其转型为对应的接口,然后再其余的代码中使用这个接口。
- 但是这种方式并非总是有用的,因为有些类具有额外的功能,, LinkedList 具有 List 接口中未包含的方法, TreeMap 也具有 Map 中未包含的方法,如果要使用这些方法,就不能将他们向上转型成更通用的接口。
- Collection 接口包含了序列的概念 -------- 一种存放一组对象的方式
java
Collectionc = new ArrayList () ; - 任何继承自 Collection 的类的对象都可以正常工作,但是 ArrayList 是最基本的序列类型。
add() 还是表示将一个新元素放置到 Collection 中,在 Set 中,只有元素不存在的情况下才会被添加,在 ArrayList 中,或者在任何类的 List 时 , add() 总是表示 “ 把它放进去 ” ,因为 List 不关心是否存在重复。 - 所有的 Collection 都支持 foreach 语法遍历。
3 . 添加一组元素
- 可以再一个 Collection 中添加一组元素。 ArrayList。asList() 方法接受一个数组或者是一个用逗号分割的元素列表(使用可变参数),并将其转换为一个 List 对象。 Collections.addAll() 方法接受一个 Collection 对象,以及一个数组或者一个用逗号分隔列表,将元素添加到 Collection 中。
4 . 容器的打印
必须使用 Arrays.toString() 来产生数组的可打印表示,但是打印容器却无需任何帮助。通过代码来分析:
package ElevenTest; import java.util.*; public class FourTest { static Collection fill(Collection<String> collection){ collection.add("rat") ; collection.add("cat"); collection.add("dog"); collection.add("dog") ; return collection ; } static Map fill(Map<String,String> map){ map.put("rat","Fuzzy"); map.put("cat","Rags") ; map.put("dog","Bosco") ; map.put("dog","Spot"); return map ; } public static void main(String[] args) { System.out.println(fill(new ArrayList<String>())); System.out.println(fill(new LinkedList<String>())); System.out.println(fill(new HashSet<String>())); System.out.println(fill(new TreeSet<String>())); System.out.println(fill(new LinkedHashSet<String>())); System.out.println(fill(new HashMap<String, String>())); System.out.println(fill(new TreeMap<String, String>())); System.out.println(fill(new LinkedHashMap<String,String>())); } } //output [rat, cat, dog, dog] [rat, cat, dog, dog] [rat, cat, dog] [cat, dog, rat] [rat, cat, dog] {rat=Fuzzy, cat=Rags, dog=Spot} {cat=Rags, dog=Spot, rat=Fuzzy} {rat=Fuzzy, cat=Rags, dog=Spot}
- 上面展示的是 java 容器类库 的两种主要类型,他们的区别在于容器中每个 “ 槽 ” 保存的元素个数。
Collection 在每个槽中只能保存一个元素,这类容器包括 :
- List ,它以特定的顺序保存一组元素。
- Set , 元素不能重复。
- Queue ,只允许在容器的一端插入对象,并从另一端移除对象(上面例子没有展示)
- Map 在每个槽内都保存了两个对象,即 键 与之相关的 值。
- 查看输出可以发现,默认的打印行为( toString() 方法 ) 就可以生成可读性很好的结果。 Collection 打印出来的内容用方括号扩住,每个元素由逗号分开,Map 使用大括号扩住,键与值由等号联系(键在等号左边,值在等号右边)
- 第一个 fill() 方法可以作用于所有类型的 Collection ,这些类型都实现了用来添加新元素的 add() 方法。
- ArrayList 和 LinkedList 都是 List 类型, LinkedList 包含的 操作比 ArrayList 要多。
- HashSet 、TreeSet 、 LinkedHashSet 都是 Set 类型,输出结果显示在 Set 中,每个相同的项,只保存一次。
- Map( 关联数组 ) 使得你可以用键来查找对象,就像一个简单的数据库。对于每一个键,Map 只接受存储一次。 Map.put(key,value)方法将增加一个值, map.get( key ) 方法将产生于这个键相关的值。
- HashMap 、HashSet 提供了最快的查找技术,没有按照任何明显的顺序来保存元素。 TreeMap 按照比较结果的升序保存键, LinkedHashMap 按照插入顺序来保存键,同时保留了 HashMap 的查找速度。
5 . List
- List 承诺可以将元素维护在特定的序列中,List 接口在 Collection 的基础上添加了大量的方法,使得可以再 List 的中间插入和删除元素。
- 基本的 ArrayList ,它长于随机访问元素,但是在 List 中间 插入和 移除元素的时候比较慢
- LinkedList ,通过代价较低的在 List 中间进行插入和删除操作,提供了优化的顺序访问。但是在随机访问方面相对较慢,但是特性相对于 ArrayList 更大。
- 与数组不同,List 允许在创建他之后添加元素,移除元素,或者自我调整尺寸。它是一种可以修改的序列。 可以使用 contains() 方法来确定某个对象是否在列表中,
- 优化是一个很棘手的问题,最好的策略就是置之不顾,直到你发现需要担心它了。
- 对于List ,有一个重载的 addAll() 方法可以使我们在 初始 List 的中间插入新的列表,而不仅仅只能用 Collection 的 addAll() 方法将其追加到表尾。
6 . 迭代器
任何容器类,都必须有某种方式可以插入元素,并将他们再次取回。毕竟,持有事物时容器最基本的工作。对于 List ,add() 是插入元素的方法之一,而 get() 是取出元素的方法之一
迭代器 ( 也是一种设计模式 ) 可以在不重写代码的情况下应用于不同类型的容器。 迭代器是一个对象,他的工作是遍历并选择序列中的对象,而客户端程序员,不需要知道或者关心这个序列底层的结构。此外,迭代器经常被称为 轻量级对象 , 创建它的代价小。所以经常可以看到一些迭代器有一些奇怪的限制。例如 java 的 Iterator 只能单向移动,这个 Iterator 只能用来:
- 使用方法 iterator() 要求容器返回一个 Iterator ,Iterator 将准备返回序列的第一个元素。
- 使用 next() 获得序列中的下一个元素。
- 使用 hasNext() 检查序列中是否还有元素。
- 使用 remove() 将迭代器新折返回的元素删除。
- 有了迭代器就不必再为容器中的容器中的元素数量操心了,那是由 hasNext() 和 next() 应该担心的事情,
- 当然,如果只是打算向前遍历 List ,使用 foreach 更加简单。
- Iterator 还可以移除由 next() 产生的最后一个元素,这意味着在调用 remove() 之前,必须调用 next() .
- 接受对象容器并传递他,从而在每个对象上都执行操作。
- Iterator 真正的威力:能够将遍历序列的操作与序列底层的结构分离。所以有时候说,迭代器统一了对容器的访问方式。
6.1 ListIterator
- ListIterator 是一个更加强大的 Iterator 的子类型,他只能用于各种 List 类的访问,尽管 Iterator 只能单向移动,但是 ListIterator 可以双向移动,它还可以产生迭代器在列表中指向的当前位置的前一个和后一个元素索引,并且可以使用 set() 方法替换它访问过的最后一个元素。可以调用 ListIterator 产生一个指向 List 开始处的 ListIterator ,并且还可以通过调用 ListIterator(n) 方法创建一个一开始就指向列表索引为 n 的元素处的 ListIterator 。
7 . LinkedList
- LinkedList 也像 ArrayList 一样实现了基本的 List 接口,但是它执行某些操作(插入和删除)比 ArrayList 更加高效,但是在随机访问操作要逊色一些。
- LinkedList 还添加了可以使其用作 栈 、队列、双端队列的方法。
8 . Stack
“ 栈 ” 通常是指 “ 后进先出 ” ( LIFO ) 的容器。有时候也被称为 叠加栈,因为最后 “ 压入 ” 栈的元素,第一个 “ 弹出 ” 栈。
- LinkedList 具有能够直接实现栈的所有功能的方法,因此可以直接将 LinkedList 作为栈来使用。
9. Set
- set 不保存重复的元素,set中做常被使用的是测试归属性,你可以很容易的查询某个对象是否在某个 Set 中,因此,查找就成了 Set 中最重要的操作,因此你通常都会选择一个 HashSet 的实现,他专门对快速查找进行了优化。
Set 具有 和 Collection 完全一样的接口,因此没有任何额外的功能,不像之前的 List ,实际上 Set 就是 Collection ,只是行为不同 ( 多态 ),Set 是基于对象的值来确定归属性的。
Set<Integer> inset = new HashSet<Integer>() ;
- 输出的结果没有顺序,因为是出于速度考虑的, HashSet 使用了散列。
HashSet 所维护的顺序和 TreeSet 或者 LinkedHashSet 都不同,因为他们的实现具有不同的元素存储方式。 TreeSet 将元素存储在 红-黑 数据结构中。 如果相对结果进行排序,一种方式是 TreeSet 来代替 HashSet
SortedSet<Integer> inset = new TreeSet<Integer>() ;
- 可以使用 contains 来测试 Set 的归属性。 能够产生每个元素的唯一列表是相当有用的功能。
10. Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏。检查 Random的随机性:
Random rand = new Random(47) ; Map<Integer,Integer> m = new HashMap<Integer, Integer>() ; for(int i = 0 ;i< 10000 ; i++){ int r = rand.nextInt(20); Integer freg = m.get(r) ; m.put(r , freg == null ? 1 : freg +1); }
其中,自动包装机制会自动将生成的 int 包装成 HashMap 可以使用的 Integer 类型。
- Map与数组和其他的 Collection 一样,可以很容易的扩展到多维,可以很容易的将容器组合成强大的数据结构。
11. Queue
- 队列是一个典型的先进先出 ( FIFO ) 的容器,从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。队列常被当做一种可靠地将对象从程序的某个区域传到另一个区域的途径。
- LinkedList 提供了方法以支持队列的行为,并且他实现了 Queue 接口, 因此 LinkedList可以用作 Queue 的一种实现,通过将 LinkedList 向上转型为 Queue
- offer() 方法是与 Queue 相关的方法之一,它在允许的情况下,讲一个元素插入到队尾,或者返回 false。
- peek() 和 element() 都将在不移除的情况下返回对头。
- poll() 和 remove() 方法将移除并返回对头。
11.1 PriorityQueue
- 先进先出描述了最典型的队列规则。所谓队列规则,是指在给定一组队列中的元素情况下,确定下一个弹出队列元素的规则。先进先出声明的应该是下一个应该是等待时间最长的元素,
- 优先级队列声明的是下一个弹出的应该是最需要的元素( 具有最高的优先级 )
- 当你在 PriorityQueue 上调用 offer() 方法插入一个对象的时候,这个对象会在队列中被排序,默认的排序将使用对象在队列中的自然排序。但是你可以通过提供自己的 Comparator 来修改这个顺序, PriorityQueue 可以确保当你调用 peek() , poll() , remove() 方法时,获取的元素将是队列中优先级最高的元素。
- Integer 、 String 、Character 可以与 PriorityQueue 一起工作,因为这些类已经建立了自然排序。
12. Collection 和 Iterator
- Collection 是秒速所有序列容器的共性的根借口。使用接口描述的一个理由是它可以使我们创建更通用的接口。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多的对象类型。如果编写的方法接受一个 Collection ,那么这个方法就可以应用于所有实现了 Collection 的类。
- Collection 接口 和 Iterator 接口
13. Foreach 与 迭代器
- foreach 语法主要用于数组,但是他也可以应用于任何 Collection 对象。
- Interable 接口,这个接口包含一个能够产生 Iterator 的 iterator() 方法,并且 Interable 接口被 foreach 用来在序列中移动,如果你创建了人已实现 Interable 接口的类,都可以使用 foreach 语句,
- 适配器方法的设计模式。但你有一个接口但是需要另一个接口的时候,编写适配器可以解决问题。
- 容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所持有的包装器类型之间的双向转换。
由本人从 Thinking in java ( java 编程思想 ) 整理而来