泛型集合接口的一个重要优势就是你可以只实现一次算法。比如,考虑一个找出序列中最大值的算法。以前,人们使用循环实现。
if (a.length == 0) throws new NoSuchElementException();
T largest = a[0];
for(int i = 0;i != a.length;i++)
if(largest.compareTo(a[i]) < 0)
largest = a[i];
同时,为了找到一个ArrayList中的最大值,你需要稍微改动一下代码
if (v.size() == 0) throws new NoSuchElementException();
T largest = v.get(0);
for(int i = 1;i < v.size();i++)
if(largest.compareTo(v.get(i)) < 0)
largest = v.get(i);
那么找到一个链表中的最大值呢?你不应该随机读取链表,你需要使用迭代器,因此你需要修改你的代码。
if(l.isEmpty()) throws new NoSuchElementException();
Iterator<T> iter = l.iterator();
T largest = iter.next();
while(iter.hasNext())
{
T next = iter.next();
if(largest.compareTo(next) < 0)
largest = next;
}
这些循环看起来很难受,而且容易出错。如果集合类中只有一个元素怎么办?如果一个是空的怎么办呢?你也不想每次都调试这段代码,而且需要实现一堆函数。
static <T extends Comparable> T max(T[] a);
static <T extends Comparable> T max(ArrayList<T> a);
static <T extends Comparable> T max(LinkedList<T> a);
这就是集合类接口出现的原因,想想我们为了实现这个算法,需要哪些接口?你需要随机获取函数set和get吗?你不需要。在链表算法中,我们只用到了迭代器。因此,你可以对任何实现了Collection接口的类实现同一个算法。
public static <T extends Comparable> max(Collections<T> a)
{
if(a.isEmpty()) throws new NoSuchElementException;
Iterator<T> iter = c.iterator();
T largest = iter.next();
while(iter.hasNext()
{
T next = iter.next();
if(largest.compareTo(next) < 0)
largest = next;
}
return largest;
}
你可以通过这个函数,找到链表,列表或者数组的最大值。
这就是接口的强大之处,实际上,C++的标准模板库里面实现了很多有用的算法,都是使用泛型技术实现的,Java中没有那么多,但是一些基本的都有:排序,二分搜索,和一些实用算法。
排序和重排
以前人们需要自己编写排序算法,现在多数编程语言自带排序函数,Java也不例外。
sort方法可以用于所有实现了List接口的类。
List<String> staff = new LinkedList<String>();
// fill collection.
Collections.sort(staff);
这个方法需要集合类中存储的元素实现了Comparable接口,如果没有实现,你需要将一个Comparator对象作为第二个参数传入函数中,我们在之前已经讲过了类似的问题。比如下面的排序方法。
Comparator<Item> itemComparator = new
Comparator<Item>()
{
public int compare(Item a,Item b)
{
return a.partNumber - b.partNumber;
}
}
Collections.sort(items,itemComparator);
如果你想降序排序,你可以直接使用Collections.reverseOrder()方法,他会返回一个降序比较器。
Collections.sort(staff,Collections.reverseOrder());
你可能会问,Java里面是怎么排序的?如果你看过算法书,那你应该了解基本的排序算法,但是有些算法需要随机读取数值,而某些List不支持或不推荐随机读取。你可以在算法书中找到这些。Java排序算法先将数据复制到一个数组中,然后使用一种混合排序算法的变体对数组进行排序,最后把值复制回来。
这种混合排序算法比其他语言实现的快速排序算法慢一些,但是它有一个优点,它是稳定的,不会调换元素间的位置。这在大多数情况下都是需要的。比如你先按名字排序了,然后按薪酬排序,但是在第二次排序时,如果排序算法不稳定,那么第一次排序的结果将不会被保留。
那么什么类型的对象可以排序呢?根据官方文档,你的对象应该可修改,但不必可变容。他们的含义是:
- 如果对象支持set方法,那么它是可修改的。
- 如果变量支持add和remove操作,那么它是可变容的。
shuffle方法执行与排序相反的操作,它将数组随机打乱。
ArrayList<Card> cards = ...;
Collections.shuffle(cards);
如果你的对象没有实现RandomAccess接口,那么shuffle方法会先把值复制到数组,然后再打乱,然后复制回来。
二分查找
为了找到一个值,你通常需要遍历一个数组。但是如果数组是有序的,你可以使用二分查找。Collections中的binarySearch方法实现了这种查找方式。向函数中传入的参数包括一个实现了List接口的对象,需要查找的值,如果你的对象是按照自己的比较方式排序的,那么你应该将Comparator对象传入。
i = Collections.binarySearch(c,element);
i = Collections.binarySearch(c,element,comparator);
如果返回值为正数,那么他就是这个元素所在的下表,如果为负数,那么他是这个元素应该插入的位置
insertionPoint = -i - 1;
之所以不是-i,是因为当i为0时会产生歧义。因此,你应该这样插入
if(i < 0)
c.add(-i - 1,element);
之后数组仍然是有序的。
最后,二分查找需要随机获取能力,因此,如果你将链表对象传入二分查找函数,那么你失去了所有优势,他会退化为线性查找。
简单算法
Collections类实现了一些简单的算法,无论是查找最大值还是复制,还是填充值,你可以直接使用他们。
static <T extends Comparable<? super T> min(Collection<T> c);
static <T extends Comparable<? super T> max(Collection<T> c);
static <T> min(Collection<T> elements,Comparator<? super T> c);
static <T> max(Collection<T> elements,Comparator<? super T> c);
static <T> void copy(List<? super T> to, List<T> from);
static <T> void fill(List<? super T> l, T value);
static <T> void addAll(Collection<? super T> c, T... values);
static <T> void replaceAll(List<T> l, T oldValue,T newValue);
static int indexOfSubList(List<?> l, List<?> s);
static int lastIndexOfSubList(List<?> l,List<?> s);
static void swap(List<?> l, int i, int j );
static void reverse(List<?> l);
static void rotate(List<?> l, int d); // 将数组循环移位d次。
static int frequency(Collection<?> c, Object c); // 统计c出现的次数。
static boolean disjoint(Collection<?> c1, Collection<?> c2); // 如果两个数组没有公共元素,返回true,否则返回false。
编写自己的算法
如果你需要编写自己的算法,你也应该使用接口,而不是直接使用具体类的函数。
传统集合类
下面的介绍自Java存在以来就存在的集合类,主要包括:Hashtable,Properties,Stack和Vector,以及BitSet类。
Hashtable
Hashtable和HashMap的功能基本一致,主要区别是,Hashtable是同步的,如果你不需要同步功能,你就应该使用HashMap。
Enumerations
Enumeration接口的用法和迭代器一样,主要有两个方法,hasMoreElements和nextElements,他们就是对Iterator接口的hasNext和next方法的模拟。
属性映射
属性映射是映射的特殊形式。他有三个特征。
- 键值都是字符串
- 可以保存到文件,从文件读取。
- 默认使用二级表
Java平台实现了叫做Properties的属性映射。
Properties映射通常用在存储映射。
Stack
Stack类模拟栈结构,只能在顶部删除(pop)或添加(push)元素。
BitSet
BitSet类用于存储位序列(事实上他不是数学意义上的集合(Set),说成序列更合适)。如果用于存储为序列,它通常比ArrayList更高效。你可以对每一位置零或置一。
结语
现在我们结束了Java类框架的介绍,你可以发现,Java有很多方便的集合类可供使用。在最后一章,我们将会介绍并发编程。