javacore - chapter9 - 集合- Collection根集合的概念

9.1## 集合框架
9.1.1## 将集合的接口与实现分离

Java集合类库将接口interface和实现implementation分离。首先看一下队列queue是如何实现分离的。
队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数,当需要收集对象的时候,按照“先进先出”的原则进行检索时候就可以使用队列。

队列的实现通常有两种方式:一种是使用循环数组(循环数组可以使用CircleArrayQueue),一种是使用链表(链表队列可以直接使用LinkedList)。

当在程序中使用了队列的时候,一旦构建了集合就不需要知道究竟使用了那种实现,因此,只有在构建集合对象的时候,使用具体的类才有意义,可以使用接口类型存放集合的引用。

Queue<Customer> expressLane = new CircleArrayQueue<>(100);
expressLane.add(new Cunstomer("harry"));

利用这种方式,一旦改变了想法,可以轻松地使用另一种不同的实现,只需要对程序的一个地方作出修改,即调用构造器的地方。如果觉得LinkedListQueue是一个更好的选择,就可以将代码修改为:

Queue<Customer> expressLane = new LinkedListQueue<>();
expressLane.add(new Customer("Harry"));

具体选择那种实现方式,接口本省并不能说明那种实现的效率如何。循环数组要比链表更加高效,因此很多人优先选择循环数组。然而,通常这样做也要付出一定的代价。
循环数组是一个有界集合,即容量有限。如果程序中要手机的对象数量没有上限,最好使用链表来实现。
在研究API文档时候,会发现另外一组以Abstract开头的类,例如,AbstractQueue这些类是为类库实现者而设计的,如果想要实现自己的队列类,会发现扩展AbstractQueue类要比实现Queue接口中所有的方法容易的多。

9.1.2 Collection接口

在Java类库中,集合类的基本接口是Collection接口。这个接口有两个基本方法,

public interface Collection<E>{
	boolean add(E element);
	Iterator<E> iterator();
}

除了这两个方法之外,还有几个方法,稍后看。
add方法用于想集合中添加元素,如果添加元素确实改变了集合就返回true,否则返回false。如果,试图向集合当中添加一个对象,这个对象在集合中已经存在,这个添加请求就是没有实效,因为集合中不定允许有重复的对象。

iterator方法用于返回一个实现了Iterator接口的对象,可以使用这个迭代器对象依次访问集合中的元素。

9.1.3 迭代器

Iterator接口包含了4个方法:

boolean hasNext();

如果被迭代的集合中有下一个元素,就返回true,否则返回false。换句话说,如果没有下一个元素,将会返回false,而不是会抛出一个异常。

E next();

调用next()方法将会返回集合中下一个元素,如果现在已经迭代到了集合当中的最后一个元素,则将会抛出NoSuchElementException,所有在使用这个next()之前一般要结合length或者size的判断再使用,确保迭代时候是下一位是有元素存在的。

 default void remove() {
        throw new UnsupportedOperationException("remove");
    }

通过迭代循环来移除集合当中的所有元素。这个方法只能每次调用一次。如果在迭代过程中以除调用此方法之外的任何方式修改了基础集合,则迭代器的行为是不确定的,所以如果要修改元素,就不要用迭代的方式进行修改。

default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }

对每个剩余元素执行给定的操作,直到所有元素都已处理或该操作引发异常。 如果指定了迭代顺序,则按迭代顺序执行操作。 操作引发的异常将传递给调用者。

通过反复调用next()方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next方法将会抛出一个NoSuchElementException。因此,需要在调用hasNext方法。如果地带器对象还有多个供访问的元素,这个方法就会返回true,如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true时候反复调用next方法,例如:

Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while (iter.hasNext()){
 String element = iter.next();
 do sth with element
 }

用“for each”循环可以更加简练地表达相同的循环操作:

for (String element : C){
do something with element
}

编译器简单地将“for each”循环翻译为带有迭代器的循环。“for each”循环可以与任何实现了Iterator接口的对象一起工作,这个接口只包含一个抽象方法:

public interface Iterable<E>{
Iterator<E> iterator();
...
}

Collection接口扩展了Iterator接口。因此,对于标准库中的任何集合都可以使用“for each”循环。
在JavaSE 8 中,甚至不用写循环,可以调用forEachRemaining方法并提供一个lambda表达式(它会处理一个元素),将对迭代器的每一个元素调用这个lambda表达式,知道在没有元素为止。

iterator.forEachRemaining(element -> do something with element);

元素被访问的顺序取决于集合类型,如果对ArrayList进行迭代,迭代器将索引从0开始,每迭代一次,索引值加1.然而,如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。虽然可以确定在迭代访问过程中能够遍历到集合中的所有元素,但是确无法预知元素被访问的次序。这对于计算总和或统计符合某个条件的元素个数这类与顺序无关的操作来说,并不是什么问题。

Java集合类库中的迭代器和其他类库中的迭代器在概念上有重要的区别。在传统的集合类库中,例如,C++的标准模板库,迭代器是根据数组索引来建模的。如果给定这样一个迭代器,就可以查看指定位置上的元素,就像知道数组索引i就可以查看数组元素a[i]一样,不需要查找元素,就可以将迭代器向前移动一个位置。这与不需要执行查看操作就可以通过i++将数组索引向前 移动一样。但是,Java迭代器并不是这样操作的,查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用next,而在调用next时候,迭代器就越下标一个元素,并返回刚刚越过的那个元素的引用。

这里还有一个有用的推论,可以将Iterator.next与InputStream.read看作为等效的从数据流中读取一个子节,就会自动地“消耗”掉这个子节。下一次调用read将会消耗返回输入的下一个子节。用同样的方式,可以反复地调用next就可以读取集合中所有的元素。

Iterator接口的remove方法将会删除上次调用next方法时候返回的元素,在大多数情况下,在决定删除某个元素之前应该先看依稀爱这个元素是很具有实际意义的。然而,如果想要删除指定位置上的元素,任然需要越过这个元素。例如,下面是如何删除字符串集合中第一个元素的方法:

Iterator<String> it = c.iterator();
it.next();
it.remove();

更重要的是,对next方法和remove方法的调用将具有互相依赖性。如果调用remove方法之前没有调用next将是不合法的。如果这样做,将会抛出一个IllegalStateExcetpion异常:
如果想要删除两个相邻的元素,不能直接这样调用:

it.remove();
it.remove();

相反地,必须先调用next越过要删除的元素:

it.remove();
it.next();
it.remove();

9.1.4 泛型实用方法

由于Collection与Iterator都是泛型接口,可以编写操作任何集合类的实用方法。例如,下面是一个检测任意集合是否包含指定元素的泛型方法:

public static<E> boolean contains(Collection<E> c, Object obj){
	for(E element : c)
		if(element.equals(obj))
		return true;
	return false;
}	

Java类库的设计者认为,这些实用方法中的某些方法非常有用,应该将它们提供给用户实用,这样,类库的使用者就不必自己重新构建这些方法了。contains就是这样一个实用的方法。
事实上,Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法。下面可以看具体的文章javautil包下的collection集合学习
当然,如果实现Collection接口的每一个类都要提供如此多的理性方法将是一件很烦人的事情,为了能够让实现者更容易地实现这个接口,Java类库提供了一个类AbstractCollection它将基础方法size和iterator抽象化了,但是在此提供了例行方法,例如:

public abstract class AbstractCollection<E> implements Collection<E> {
...
	public abstract Iterator<E> iterator();

	public boolean contains(Obj obj){
		for(E element : this) // calls iterator()
			if(element.equals(obj))
				return true;
			return false;
			}
			...
		}		

此时,一个具体的集合类可以扩展AbstractCollection类了。现在要由具体的集合类提供iterator方法,而contains方法已经有AbstractCollection提供了。然而,如果子类有更加有效的方式实现contains方法,也可以由子类提供,就这点而言,没有什么限制。

对于JavaSE8,这些方法确实是有些过时了。如果这些方法是Collection接口中的默认方法会更好。但是实际上并不是这样,还有一个比较有用的方法:

default boolean removeIf(Predicate<? super E> filter)
// 这个方法用于删除满足某个条件的元素

在这里插入图片描述
集合有两个基本接口:Collection和Map。我们已经看到,可以用以下方法在集合中存入元素:

boolean add(E element)

不过,由于映射包含键值对,所以要用put方法来插入:
v put(K key, V value)
要从集合中读取元素,可以用迭代器访问元素。不过,从映射中读取值则要使用get方法;
v get(K key)
List是一个有序集合(ordered collection)。元素会增加到容器中的特定位置,可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。后一种方法称为随机访问(random access)。因为这样可以按照顺序访问元素。与之不同,使用迭代器访问的时候,必须按照顺序访问元素。

List接口定义了多个用于随机访问的方法:

void add(int index, E element)
void remove(int index)
E get(int index)
E set(int index)

ListIterator接口是Iterator接口的一个子接口,它定义了一个方法用于在迭代器位置前面增加一个元素:

void add(E element)

坦率地讲,集合框架的这个方面的涉及并不好,实际中有两种有序集合,其性能开销有很大的差异。由于数组支持的有序集合可以快速地随机访问,因此适合使用List方法并提供一个整数索引来访问。与之不同,链表尽管也可能是有序的,但是随机访问很慢,所以最好使用迭代器来遍历。如果原先提供两个接口就会比较容易一些。

为了避免对链表完成随机访问,JavaSE1.4引入了一个标记接口RandomAccess这个接口不包含任何方法,不过可以用它来测试一个特定的集合是否支持高效的随机访问。
set接口等同于Collection接口,不过期方法有更加严谨的定义,集合Set的add方法不允许有重复元素添加,要适当的定义集的equals方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素的顺序要相等,hashCode方法的定义要保证相同元素的两个集会得到相同的散列码。
既然方法签名都一样,为什么要建立一个单独的接口呢?从概念上讲,并不是所有的集合都是集,建立一个Set接口可以让程序员编写只接受集的方法。
SortedSet和SortedMap接口将会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法。
最后JavaSe1.6引入了接口NavigableSet和NavigableMap,其中包含了一些用于搜索和遍历有序集合和映射的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值