接口实现分离的设计不仅受益于层次分明,也体现在接口编程上。对于我们所要实现的算法,只实现一次便可通用于整个集合框架,最大可能地减少代码量、测试量及错误率(一个系统假如处处遍布挑战性的代码,我认为这不是个好系统)。因此仅需写好关键性代码,将其实现为能够接收任何实现了Collection 接口的对象,我们便可以忽略其细节轻松使用了。下面我们逐一介绍一些java框架所应用的算法。
排序与混排:Collections 类中的sort 方法可以对实现了List 接口的集合进行排序。
List<String> staff = new LinkedListoO;
fill collection
Collections,sort(staff);
这个方法假定列表元素实现了Comparable 接口。如果想采用其他方式对列表进行排序,可以使用List 接口的sort 方法并传入一个Comparator 对象。可以如下按工资对一个员工列表排序:
staff _sort(Comparator.comparingDouble(Employee::getSalary));
如果想按照降序对列表进行排序, 可以使用一种非常方便的静态方法Collections.reverse-Order()。 这个方法将返回一个比较器, 比较器则返回b.compareTo⑻。例如,
staff.sort(Comparator.reverseOrder() 这个方法将根据元素类型的compareTo 方法给定排序顺序, 按照逆序对列表staff 进行排序。同样,
staff.sort(Comparator.comparingDouble(Employee::getSalary).reversedO)
将按工资逆序排序。
人们可能会对sort 方法所采用的排序手段感到好奇。通常,在翻阅有关算法书籍中的排序算法时, 会发觉介绍的都是有关数组的排序算法, 而且使用的是随机访问方式。但是,对列表进行随机访问的效率很低。实际上, 可以使用归并排序对列表进行高效的排序。然而,Java 程序设计语言并不是这样实现的。它直接将所有元素转人一个数组, 对数组进行排序,然后,再将排序后的序列复制回列表。
集合类库中使用的排序算法比快速排序要慢一些, 快速排序是通用排序算法的传统选择。但是, 归并排序有一个主要的优点:稳定, 即不需要交换相同的元素。为什么要关注相同元素的顺序呢? 下面是一种常见的情况。假设有一个已经按照姓名排列的员工列表。现在,要按照工资再进行排序。如果两个雇员的工资相等发生什么情况呢? 如果采用稳定的排序算法, 将会保留按名字排列的顺序。换句话说, 排序的结果将会产生这样一个列表, 首先按照工资排序, 工资相同者再按照姓名排序。因为集合不需要实现所有的“ 可选” 方法,因此, 所有接受集合参数的方法必须描述什么时候可以安全地将集合传递给算法。例如, 显然不能将unmodifiableList 列表传递给排序算法。可以传递什么类型的列表呢? 根据文档说明,列表必须是可修改的, 但不必是可以改变大小的。
下面是有关的术语定义:
•如果列表支持set 方法,则是可修改的。
•如果列表支持add 和remove 方法, 则是可改变大小的。
Collections 类有一个算法shuffle, 其功能与排序刚好相反, 即随机地混排列表中元素的顺序。例如:
ArrayList<Card> cards = . . .;
Collections.shuffle(cards) ;
如果提供的列表没有实现RandomAccess 接口,shuffle 方法将元素复制到数组中, 然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。
二分查找:要想在数组中査找一个对象, 通常要依次访问数组中的每个元素,直到找到匹配的元素为止。然而, 如果数组是有序的, 就可以直接査看位于数组中间的元素, 看一看是否大于要查找的元素。如果是, 用同样的方法在数组的前半部分继续查找; 否则, 用同样的方法在数组的后半部分继续查找。这样就可以将查找范围缩减一半。一直用这种方式査找下去。例
如, 如果数组中有1024 个元素, 可以在10 次比较后定位所匹配的元素(或者可以确认在数组中不存在这样的元素),而使用线性查找, 如果元素存在,平均需要512 次比较; 如果元素不存在, 需要1024 次比较才可以确认。Collections 类的binarySearch 方法实现了这个算法。注意, 集合必须是排好序的, 否则算法将返回错误的答案。要想查找某个元素, 必须提供集合(这个集合要实现List 接口, 下面还要更加详细地介绍这个问题)以及要查找的元素。如果集合没有采用Comparable 接口的compareTo 方法进行排序, 就还要提供一个比较器对象。
i = Collections.binarySearch(c, element) ;
i = Collections.binarySearch(c, element, comparator);
如果binarySearch 方法返回的数值大于等于0, 则表示匹配对象的索引。也就是说,c.get(i) 等于在这个比较顺序下的element。如果返回负值, 则表示没有匹配的兀素。但是,可以利用返回值计算应该将element 插人到集合的哪个位置, 以保持集合的有序性。插人的位置是
insertionPoint = -i - 1;
这并不是简单的-i, 因为0 值是不确定的。也就是说, 下面这个操作:
if (i < 0)
c.add(-i - 1, element) ;
将把元素插人到正确的位置上。
只有采用随机访问,二分査找才有意义。如果必须利用迭代方式一次次地遍历链表的一半元素来找到中间位置的元素,二分査找就完全失去了优势。因此, 如果为binarySearch 算法提供一个链表, 它将自动地变为线性查找。
批操作:很多操作会“ 成批” 复制或删除元素。以下调用
colll.removeAll (coll2);
将从colli 中删除)112 中出现的所有元素。与之相反,
colli.retainAll(coll2);
会从colli 中删除所有未在C0112 中出现的元素。下面是一个典型的应用。
假设希望找出两个集的交集( intersection),也就是两个集中共有的元素。首先,建立一个新集来存放结果:
Set<String> result = new HashSeto(a);
在这里, 我们利用了一个事实:每一个集合都有这样一个构造器,其参数是包含初始值的另一个集合。
现在来使用retainAll 方法:
result.retainAll(b);
这会保留恰好也在b 中出现的所有元素。这样就构成了交集, 而无需编写循环。可以把这个思路更进一步,对视图应用一个批操作。例如, 假设有一个映射, 将员工ID映射到员工对象, 而且建立了一个将不再聘用的所有员工的ID。
MajxString, Employee〉staffMap = . . .;
Set<String> terainatedlDs = . . .;
直接建立一个键集,并删除终止聘用关系的所有员工的ID。
staffMap.keySet().removeAll (terminatedIDs);
由于键集是映射的一个视图,所以键和相关联的员工名会自动从映射中删除。通过使用一个子范围视图,可以把批操作限制在子列表和子集上。例如, 假设希望把一个列表的前10 个元素增加到另一个容器,可以建立一个子列表选出前10 个元素:
relocated.addAll (staff.subList(0, 10)):
这个子范围还可以完成更改操作。
staff.subList®, 10).dearO;
集合和数组的转换:由于Java 平台API 的大部分内容都是在集合框架创建之前设计的, 所以, 有时候需要在传统的数组和比较现代的集合之间进行转换。
如果需要把一个数组转换为集合,Arrays.asList 包装器可以达到这个目的。例如:
String□ values = . .
HashSet<String> staff = new HashSeto(Arrays.asList(values));
从集合得到数组会更困难一些。当然,可以使用toArray 方法:
Object □ values = staff.toArray0;
不过,这样做的结果是一个对象数组。尽管你知道集合中包含一个特定类型的对象,但
不能使用强制类型转换:
StringQ values = (StringQ) staff.toArray0;// Error!
toArray 方法返回的数组是一个Object[] 数组, 不能改变它的类型。实际上, 必须使用
toArray 方法的一个变体形式,提供一个所需类型而且长度为0 的数组。这样一来, 返回的数
组就会创建为相同的数组类型:
String[] values = staff .toArray(new StringtO]) ;
如果愿意,可以构造一个指定大小的数组:
staff.toArray(new String[staff•si ze()]) ;
在这种情况下,不会创建新数组。
值得一提的是: 你可能奇怪为什么不能直接将一个Class 对象(如String.class) 传递到toArray 方法。原因是这个方法有“ 双重职责”, 不仅要填充一个已有的数组(如果它足够长),还要创建一个新数组。
那么,我们java集合框架的学习就到这里了,正如我们所学习那样,java集合框架为我们提供了大量的集合类以适应程序设计的需要,只要我们对它稍微深入了解,对其有个整体把握,同时知道各个的的优缺点及使用场景,便能熟练运用,为程序设计带来极大的效率。