前言:暑期应该开始了,因为小区对面的小学这两天早上都没有像以往那样一到七八点钟就人声喧闹、车水马龙。
前两篇文章介绍了Collection框架的主要接口和常用类,例如List、Set、Queue,和ArrayList、HashSet、LinkedList等等。根据核心框架图,相信我们都已经对Collection这个JavaSE中最常用API之一有一个较为全面的认识。
这个学习过程,还可以推及到其他常用开源框架和公司项目的学习和熟悉上面。借助开发工具或说明文档,先是对项目整体有一个宏观的认识,再根据这个认识逐一熟悉各个核心模块。(如果对关于Collection的两篇文章感兴趣的话,可以在文章的最末尾点击链接阅读。)
1.5 遍历对象的Iterator
Collection,顾名思义,就是收集,在JavaSE当中起到了收集对象的作用。
收集完对象之后,有一个非常普遍的需求,就是要遍历所收集的对象。学生报名之后,老师有浏览都有哪些学生已经注册的需要;客户下了订单之后, 商家有看看哪些商品被购买的需要;病人挂号之后,医院工作人员有了解门诊工作量的需要。
1.5.1 遍历对象遇到的问题
如果要写一个forEach()方法,可以显示List收集的所有对象,也许你会这么写:
1 private static void forEach(List list) { 2 int size = list.size(); 3 for(int i = 0; i < size; i++) { 4 System.out.println(list.get(i)); 5 } 6 }
这个方法适用于所有实现List接口的对象,如ArrayList、LinkedList等。如果要让你写个forEach()方法显示Set收集的所有对象,你该怎么写呢?在查看过Set的API说明文档后,发现有个toArray()方法,可以将Set收集的对象转为Object[]返回,所以你会这么写:
1 private static void forEach(Set set) { 2 for(Object o : set.toArray()) { 3 System.out.println(o); 4 } 5 }
这个方法适用于所有操作Set接口的对象,如HashSet、TreeSet等。如果现在要让你再写一个forEach()方法,可以显示Queue收集的对象,也许你会这么写:
1 private static void forEach(Queue queue) { 2 while(queue.peek() != null) { 3 System.out.println(queue.poll()); 4 } 5 }
表面上看来好像是正确的,不过Queue的poll()方法会取出对象,当你显示完Queue中所有对象,Queue也空了。这并不是我们想要的结果,怎么办呢?
1.5.2 使用Iterator
事实上,无论是List、Set还是Queue,都会有个Queue,都会有个iterator()方法,这个方法在JDK1.4之前,是定义在Collection接口中,而它们都继承自Collection,所以也都拥有iterator()的行为。
iterator()方法会返回java.util.Iterator接口的操作对象,这个对象包括了Collection收集的所有对象,你可以使用Iterator的hasNext()看看有无下一个对象,若有的话,再使用next()取得下一个对象。因此,无论List、Set、Queue还是任何Collection,都可以使用以下的forEach()方法来显示所收集的对象:
1 private static void forEach(Collection collection) { 2 Iterator iterator = collection.iterator(); 3 while(iterator.hasNext()) { 4 System.out.println(iterator.next()); 5 } 6 }
在JDK5之后,原先定义在Collection中的iterator()方法,提升至新的java.util.Iterable父接口,因此在JDK5之后,可以使用以下forEach()方法显示收集的所有对象:
1 private static void forEach(Iterable iterable) { 2 Iterator iterator = iterable.iterator(); 3 while(iterator.hasNext()) { 4 System.out.println(iterator.next()); 5 } 6 }
接下来,我们可以写一个比较完整的demo,看看是否能正确使用forEach()方法:
1 import java.util.*; 2 3 /** 4 * Iterator实验用例 5 */ 6 public class ForEach { 7 private static void forEach(Iterable iterable) { 8 Iterator iterator = iterable.iterator(); 9 while(iterator.hasNext()) { 10 System.out.println(iterator.next()); 11 } 12 } 13 14 public static void main(String[] args) { 15 List list = Arrays.asList("Tim", "Jack", "Jane"); 16 forEach(list); 17 forEach(new HashSet(list)); 18 forEach(new ArrayDeque(list)); 19 } 20 }
运行之后,我们可以得到以下结果:
1.5.3 Iterator小结
遍历显示所收集的所有对象,这是使用Celletion框架时频繁会遇到的需求。在了解List、Set、Queue如何显示收集对象之后,我们意外地发现可以通过使用定义在上层接口的iterator()方法,达到正确、安全地满足这一需求的目的。
无论是学习Colletion等JavaSE的API框架,还是熟悉开源或公司项目,在熟悉各个模块的同时,也要有抽象的理解能力来宏观地理解项目。在满足一些普遍常用需求时,可以找到更通用、复用程度更高的解决方案。如果能做到这一点,就不会犯新手常犯的“只见树木不见森林”的错误。
1.6 收集对象后的排序
在收集对象之后,对对象进行排序是常用的动作。
我的上家是做ERP系统的,常常有这样的需求:几家供应商提供的报价单价格不一样,系统用户希望可以在按照报价从低到高查看商品的报价;生产物料紧缺时,他们希望根据物料需求日期从近到远查看;同一个生产件,不同的生产配方和工艺流程产生的生产成本是不一样的,他们希望能按照成本从低到高查看生产明细。以上的这些需求,都要求系统对所收集的不同对象进行排序
1.6.1 Collection自带的排序算法
java.util.Collections提供有sort()方法,用来满足对对象进行排序的需求。由于必须有索引才能进行排序,因此Collections的sort()方法接受List实现对象。例如以下这段demo:
1 import java.util.*; 2 3 /** 4 * Collections的sort()方法实验用例 5 */ 6 public class Sort { 7 public static void main(String[] args) { 8 List numbers = Arrays.asList(10, 3, 4, 21, 9); 9 Collections.sort(numbers); 10 System.out.println(numbers); 11 } 12 }
执行结果我们可以看到已经排好序的一串数字:
可是,如果我们需要排序的对象稍微复杂一点点,会出现什么样的情况呢?
1 import java.util.*; 2 3 /** 4 * Collections的sort()方法实验用例2 5 */ 6 class Account { 7 private String name; 8 private int balance; 9 10 Account (String name, int balance) { 11 this.name = name; 12 this.balance = balance; 13 } 14 15 @Override 16 public String toString() { 17 return String.format("Account(%s, %d)", name, balance); 18 } 19 } 20 21 public class Sort { 22 public static void main(String[] args) { 23 List accounts = Arrays.asList( 24 new Account("Tim", 100), 25 new Account("Tom", 1300), 26 new Account("Jack", 5) 27 ); 28 Collections.sort(accounts); 29 System.out.println(accounts); 30 } 31 }
运行结果出现了抛出ClassCastException报错,到底是怎么回事呢?
1.6.2 实现Comparable
要说原因,是因为你根本没告诉Collections的sort()方法,到底要根据Account的name还是balance进行排序。用一句时下流行的话说:“我有什么办法,我也很绝望啊。”
Collections的sort()方法要求被排序的对象必须实现java.lang.Comparable接口,这个接口有个compareTo()方法必须返回大于0、等于0或小于0的数。这有什么用呢?我们直接来看下面这个针对账户余额排序的demo就了解了:
1 import java.util.*; 2 3 /** 4 * Collections的sort()方法实验用例3 5 */ 6 class Account implements Comparable{ 7 private String name; 8 private int balance; 9 10 Account (String name, int balance) { 11 this.name = name; 12 this.balance = balance; 13 } 14 15 @Override 16 public String toString() { 17 return String.format("Account(%s, %d)", name, balance); 18 } 19 20 @Override 21 public int compareTo(Object o) { 22 Account other = (Account) o; 23 return this.balance - other.balance; 24 } 25 } 26 27 public class Sort { 28 public static void main(String[] args) { 29 List accounts = Arrays.asList( 30 new Account("Tim", 100), 31 new Account("Tom", 1300), 32 new Account("Jack", 5) 33 ); 34 Collections.sort(accounts); 35 System.out.println(accounts); 36 } 37 }
Collections的sort()方法在取得a对象与b对象进行比较时,会先将对象Cast为Comparable(也因为这样,如果对象没实现这个接口,就会抛出ClassCastException),然后调用a.compareTo(b),如果a对象顺序上小于b对象,必须返回小于0的值;如果顺序上相等则返回0;如果顺序上a大于b,则要返回大于0的值。因此,上面的demo输出结果会是按照余额从小到大排列:
1.6.3 实现Comparator
如果你有用过Collections对所收集的String对象排序,你应该会知道JavaSE是按照A、B、C的字母表来排序的。可是,如果今天突然有个需求,要让排序结果反过来呢?
首先,String已经实现了Comparable接口,我们很难进行修改。另外,由于String声明为final,我们也没有办法通过继承的方式重新定义compareTo()方法。不过幸好,JavaSE给这种情况留下了备用的解决方案。
Collections的sort()方法有另一个重载版本,可以接受java.util.Comparator接口的操作对象。如果使用这个版本,排序方式将根据Comparator的compare()方法的定义来决定。例如下面这个demo:
1 import java.util.*; 2 3 /** 4 * Collections的sort()方法实验用例4 5 */ 6 class StringComparator implements Comparator { 7 @Override 8 public int compare(Object o1, Object o2) { 9 String str1 = (String) o1; 10 String str2 = (String) o2; 11 return -str1.compareTo(str2); 12 } 13 } 14 15 public class Sort { 16 public static void main(String[] args) { 17 List words = Arrays.asList("B", "C", "A", "X", "Z", "Y"); 18 Collections.sort(words, new StringComparator()); 19 System.out.println(words); 20 } 21 }
结果如下,符合我们之前的期望:
1.6.4 Sort()小结
在Java的规范中,与顺序有关的行为,通常要不就是对象本身是Comparable,即实现了Comparable接口,要不就是另行制定Comparator对象告知如何排序。
这就是深入学习API的好处,可以介绍遇到麻烦的次数。另外,无论你的工作语言是Java还是PHP、C#,熟悉语言规范是必不可少的内功,是衡量一个程序员实力的硬指标。
相关文章推荐:
JavaSE中Collection集合框架学习笔记(1)——具有索引的List
JavaSE中Collection集合框架学习笔记(2)——拒绝重复内容的Set和支持队列操作的Queue
如果你喜欢我的文章,可以扫描关注我的个人公众号“李文业的思考笔记”。
不定期地会推送我的原创思考文章。