到目前为止,foreach语法主要用于数组,但是它也可以应用于任何Collection对象。你实际上已经看到过很多使用ArrayList时用到它的示例,下面是一个更通用的证明:
package p10; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; public class ForEachCollections { public static void main(String[] args) { Collection cs = new LinkedList<>(); Collections.addAll(cs,"Take the long way home".split(" ")); for(String s:cs){ System.out.print("'" + s + "' "); } /** * 'Take' 'the' 'long' 'way' 'home' */ } } 复制代码
由于cs是一个Collection,所以这段代码展示了能够与foreach一起工作是所有Collection对象的特性。
之所以能够工作,是因为Java SE5引入了新的被称为Iterable的接口,该接口包含一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。因此如果你创建了任何实现Iterable的类,都可以将它用于foreach语句中:
package p10; import java.util.Iterator; public class IterableClass implements Iterable { protected String[] words = ("And that is how " + "we know the Earth to be banana-shaped.").split(" "); @Override public Iterator iterator() { return new Iterator() { private int cursor = 0; @Override public boolean hasNext() { return cursor < words.length; } @Override public String next() { return words[cursor++]; } }; } public static void main(String[] args) { for(String s:new IterableClass()){ System.out.print(s + " "); } /** * And that is how we know the Earth to be banana-shaped. */ } } 复制代码
iterator()方法返回的是实现了Iterator的匿名内部类的实例,该匿名内部类可以遍历数组中的所有单词。在main()中,你可以看到IterableClass确实可以用于foreach语句中。
在Java SE5中,大量的类都是Iterable类型,主要包括所有的Collection类(但是不包括各种Map)。
foreach语句可以用于数组或其他任何Iterable,但是这并不意味着数组肯定也是一个Iterable,'而任何自动包装也不会自动发生:
package p10; import java.util.Arrays; public class ArrayIsNotIterable { static void test(Iterable ib){ for(T t:ib){ System.out.print(t + " "); } } public static void main(String[] args) { test(Arrays.asList(1,2,3)); String[] strings = {"A","B","C"}; // An array works in foreach, but it's not Iterable // !test(strings) // You must explicitly convert it to an Iterable test(Arrays.asList(strings)); /** * 1 2 3 A B C */ } } 复制代码
尝试把数组当做一个Iterable参数传递会导致失败。这说明不存在任何从数组到Iterable的自动转换,你必须手动执行这种转换。
适配器方法惯用法
如果现有一个Iterable类,你想要添加一种或多种在foreach语句中使用这个类的方法,应该怎么做呢?例如,假设你希望可以选择以向前的方向或是向后的方向迭代一个单词列表。如果直接继承这个类,并覆盖iterator()你只能替换现有的方法,而不能实现选择。 一种解决方案是所谓适配器方法的惯用法。“适配器”部分来自于设计模式,因为你必须提供特定接口以满足foreach语句。当你有一个接口并需要另一个接口时,编写适配器就可以解决问题。这里,我希望在默认的前向迭代器的基础上,添加产生反向迭代器的能力,因此我不能使用覆盖,而是添加了一个能够产生Iterable对象的方法,该对象可以用于foreach语句。正如你所见,这使得我们可以提供多种使用foreach的方式:
package p10; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; public class AdapterMethodIdiom{ public static void main(String[] args) { ReversibleArrayList ral = new ReversibleArrayList<>(Arrays.asList("To be or not to be".split(" "))); 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 + " "); } /** * To be or not to be * be to not or be To */ } } class ReversibleArrayList extends ArrayList { public ReversibleArrayList(Collection c){ super(c); } public Iterable reversed(){ return new Iterable() { @Override public Iterator iterator() { return new Iterator(){ int cursor = size() - 1; @Override public boolean hasNext() { return cursor > -1; } @Override public T next() { return get(cursor--); } }; } }; }; } 复制代码
如果直接将ral对象置于foreach语句中,将得到(默认的)前向迭代器。但是如果在该对象上调用reversed()方法,就会产生不同的行为。 通过使用这种方式,我可以在IterableClass.java示例中添加两种适配器方法:
package p10; import java.util.*; public class MultiIterableClass extends IterableClass{ public Iterable reversed(){ return new Iterable() { @Override public Iterator iterator() { return new Iterator() { int cursor = words.length - 1; @Override public boolean hasNext() { return cursor > -1; } @Override public String next() { return words[cursor--]; } }; } }; } public Iterable randomized(){ return new Iterable() { @Override public Iterator iterator() { List shuffled = new ArrayList<>(Arrays.asList(words)); Collections.shuffle(shuffled,new Random(47)); return shuffled.iterator(); } }; } public static void main(String[] args) { MultiIterableClass mic = new MultiIterableClass(); for(String s:mic.reversed()){ System.out.print(s + " "); } System.out.println(); for(String s:mic.randomized()){ System.out.print(s + " "); } System.out.println(); for(String s:mic){ System.out.print(s + " "); } /** * banana-shaped. be to Earth the know we how is that And * is banana-shaped. Earth that how the be And we know to * And that is how we know the Earth to be banana-shaped. */ } } 复制代码
注意,第二个方法random()没有创建它自己的Iterator,而是直接返回被打乱的List中的Iterator。 从输出中可以看到,Collection.shuffe()方法没有影响到原来的数组,而只是打乱了shuffied中的引用。之所以这样,只是因为randomized()方法用一个ArrayList将Arrays.asList()方法的结果包装了起来。如果这个由Arrays.asList()方法产生的List被直接打乱,那么它就会修改底层的数组,就像下面这样:
package p10; import java.util.*; public class ModifyingArraysAsList { public static void main(String[] args) { Random random = new Random(47); Integer[] ia = {1,2,3,4,5,6,7,8,9,10}; List list1 = new ArrayList<>(Arrays.asList(ia)); System.out.println("Before shuffling: " + list1); Collections.shuffle(list1,random); System.out.println("After shuffling: " + list1); System.out.println("array: " + Arrays.toString(ia)); List list2 = Arrays.asList(ia); System.out.println("Before shuffling: " + list2); Collections.shuffle(list2,random); 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] */ } } 复制代码
在种情况中,Arrays.asList()的输出被传递给了ArrayList()的构造器,这将创建一个引用ia的元素的ArrayList,因此打乱这些引用不会修改该数组。但是,如果直接使用Arrays.asList(ia)的结果,这种打乱就会修改ia的顺序。意识到Arrays.asList()产生的List对象会使用底层数组作为其物理实现是很重要的。只要你执行的操作会修改这个List,并且你不想原来的数组被修改,那么你就应该在另一个容器中创建一个副本。