七、迭代器Iterators
1.为什么要有迭代器
在任何集合中,都必须有某种方式可以插入元素并再次获取它们。
如果从更高层次的角度考虑,会发现这里有个缺点:要使用集合,必须对集合的确切类型编程。
为了解决这个问题,迭代器这个概念被提了出来。
2.什么是迭代器
迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为轻量级对象(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java 的 Iterator 只能单向移动。这个 Iterator 只能用来:
使用 iterator() 方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。
使用 next() 方法获得序列中的下一个元素。
使用 hasNext() 方法检查序列中是否还有元素。
使用 remove() 方法将迭代器最近返回的那个元素删除。
3.如何使用迭代器
代码示例如下:
public static void display(Iterator<Pet> it) { while(it.hasNext()) { Pet p = it.next(); System.out.print(p.id() + ":" + p + " "); } System.out.println(); }
我们可以使用 Iterable 接口生成上一个示例的更简洁版本(该接口必须要实现iterator()方法,即提供一个Iterator迭代器)
public static void display(Iterator<Pet> it) { while(it.hasNext()) { Pet p = it.next(); System.out.print(p.id() + ":" + p + " "); } System.out.println(); }
4.ListIterator迭代器
ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且可以使用 set() 方法替换它访问过的最近一个元素。可以通过调用 listIterator() 方法来生成指向 List 开头的 ListIterator ,还可以通过调用 listIterator(n) 创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator 。
5.for-in和迭代器
for-in 语法主要用于数组,但它也适用于任何 Collection 对象。示例如下: for(Map.Entry entry: System.getenv().entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
for-in语法出现于Java5,其原理在于 Java 5 引入了一个名为 Iterable 的接口,该接口包含一个能够生成 Iterator 的 iterator() 方法。for-in 使用此 Iterable 接口来遍历序列。而在 Java 5 中,许多类都是 Iterable ,主要包括所有的 Collection 类(但不包括各种 Maps )。
所以for-in 语句适用于数组或其它任何 Iterable ,但这并不意味着数组肯定也是个 Iterable ,也不会发生任何自动装箱。
PS:这句话的意思是实现了Iterable 接口的类都可以使用for-in语法,但是这并不是说数组本身也实现了Iterable接口,数组能使用for-in语法应该要归功于编译器层次的改变。
适配器方法惯用法
如果现在有一个 Iterable 类,你想要添加一种或多种在 for-in 语句中使用这个类的方法,应该怎么做呢?例如,你希望可以选择正向还是反向遍历一个单词列表。如果直接继承这个类,并覆盖 iterator() 方法,则只能替换现有的方法,而不能实现遍历顺序的选择。
一种解决方案是所谓适配器方法(Adapter Method)的惯用法
而解决思路就是另写一个方法返回实现了Iterable接口的对象,实例如下
import java.util.*; class ReversibleArrayList<T> extends ArrayList<T> { 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; public boolean hasNext() { return current > -1; } public T next() { return get(current--); } public void remove() { // Not implemented throw new UnsupportedOperationException(); } }; } }; } } public class AdapterMethodIdiom { public static void main(String[] args) { ReversibleArrayList<String> ral = new ReversibleArrayList<String>( Arrays.asList("To be or not to be".split(" "))); // Grabs the ordinary iterator via iterator(): for(String s : ral) System.out.print(s + " "); System.out.println(); // Hand it the Iterable of your choice for(String s : ral.reversed()) System.out.print(s + " "); } } /* Output: To be or not to be be to not or be To */
八、集合的使用技巧
1.面向接口编程
在理想情况下,你编写的大部分代码都在与这些接口打交道,并且唯一需要指定所使用的精确类型的地方就是在创建的时候。因此,可以像下面这样创建一个 List :
List<Apple> apples = new ArrayList<>();
请注意, ArrayList 已经被向上转型为了 List ,这与之前示例中的处理方式正好相反。使用接口的目的是,如果想要改变具体实现,只需在创建时修改它就行了。因此,应该创建一个具体类的对象,将其向上转型为对应的接口,然后在其余代码中都是用这个接口。
2.创建集合时尽量写上合适的初始化大小
我们在平常使用过程中,创建对象时,便给ArrayList一个初始化的大小,代码如下:
List arrayList=new ArrayList<>(2);
在可预期的范围内,尽量避免扩容的发生。当然这并不是越大越好,因为过大的初始容量会造成内存的浪费。
3.使用Iterator来连接序列和消费该序列的方法
生成 Iterator 是将序列与消费该序列的方法连接在一起耦合度最小的方式,并且与实现 Collection 相比,它在序列类上所施加的约束也少得多。
4.添加元素组的几个常用方法
在 java.util 包中的 Arrays 和 Collections 类中都有很多实用的方法,可以在一个 Collection 中添加一组元素。
Arrays.asList()
Arrays.asList() 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 List 对象。
PS:这里看源码我们就能发现Arrays类内部自己实现了个ArrayList类,和util包中的ArrayList有所不同,它就是简单继承实现了AbstractList类,其并没有真正ArrayList的扩容能力,其内部只是根据这个数组去做了一个封装,所以如果改变了这个list的内容,那么输入的数组也会发生改变(如果输入的数组的话)。
这里我们还可以使用如下方法:
List<Snow> snow4 = Arrays.<Snow>asList( new Light(), new Heavy(), new Slush());
注意 Arrays.asList() 中间的“暗示”(即 ),告诉编译器 Arrays.asList() 生成的结果 List 类型是什么实际目标类型。这称为显式类型参数说明(explicit type argument specification)。
Collections.addAll()
Collections.addAll() 方法接受一个 Collection 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 Collection 中。
PS:和Arrays类类似,Collections类中只包含对集合进行操作或返回集合的静态方法。和我们常说的集合类无关,可以理解为简化集合类操作的工具类。就如jdk里注释所说的:
collection.addAll()
用法如下:
Collection<Integer> collection =new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); collection.addAll(Arrays.asList(moreInts));
5.集合的打印
PS:原文这部分内容打印部分讲的比较少,反而各个集合类型的区别占了很大篇幅,这是我认为不合理的地方,为了应和标题,我将补充一下我对于集合打印的理解
集合打印会自动调用集合的toString方法,原因是在调用println方法时实质上会调用String.valueOf(Object x)方法,而该方法实质上就是调用了对象的toString方法。
而我们打印的时候可以打印出类似数组的样式
所以我们有理由猜测集合类中一定重写了toString方法,而事实确实如此,可我们在具体诸如ArrayList、LinkedList类中并没有发现被重写的痕迹。原因是该方法已经在AbstractCollection类中被重写了。
不知道大家注意到没有,这里有一点很巧妙。他在重写toString方法时调用了相应的迭代器,从而使该方法可以在各个类的抽象类中就被重写而不用去管具体类的具体实现。
本章小结
Java 提供了许多保存对象的方法:
数组将数字索引与对象相关联。它保存类型明确的对象,因此在查找对象时不必对结果做类型转换。它可以是多维的,可以保存基本类型的数据。虽然可以在运行时创建数组,但是一旦创建数组,就无法更改数组的大小。
Collection 保存单一的元素,而 Map 包含相关联的键值对。使用 Java 泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 Collection 和各种 Map 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。
像数组一样, List 也将数字索引与对象相关联,因此,数组和 List 都是有序集合。
如果要执行大量的随机访问,则使用 ArrayList ,如果要经常从表中间插入或删除元素,则应该使用 LinkedList 。
队列和堆栈的行为是通过 LinkedList 提供的。
Map 是一种将对象(而非数字)与对象相关联的设计。 HashMap 专为快速访问而设计,而 TreeMap 保持键始终处于排序状态,所以没有 HashMap 快。 LinkedHashMap 按插入顺序保存其元素,但使用散列提供快速访问的能力。
Set 不接受重复元素。 HashSet 提供最快的查询速度,而 TreeSet 保持元素处于排序状态。 LinkedHashSet 按插入顺序保存其元素,但使用散列提供快速访问的能力。
不要在新代码中使用遗留类 Vector ,Hashtable 和 Stack 。