Java核心技术 集合

1.Java集合框架

将集合的接口与实现分离

与现代的数据结构类库的常见情况一样,Java集合类库也将接口与实现分离。

队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中的元素个数。当需要收集对象,并按照“先进先出”的规则检索对象时就应该使用队列。

// 标准库中接口的简化形式
public interface Queue<E> {
    void add(E element);
    E remove();
    int size();
}

队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表。

每一个实现都可以通过一个实现了Queue接口的类表示。

如果需要一个循环队列,就可以使用ArrayDeque类。如果需要一个链表队列,就直接使用LinkedList类,这个类实现了Queue接口。

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

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

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

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

接口本身并不能说明哪种实现的效率究竟如何。循环数组要比链表更高效,然而通常这样做也需要付出一定的代价。循环数组是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。

API文档中,会有一组以Abstract开头的类,这些类是为类库实现者而设计的。如果想要实现自己的队列类,会发现扩展AbstractQueue类要比实现Queue接口中的所有方法轻松得多。

 

Collection接口

集合类的基本接口是Collection接口,这个接口有两个基本方法:

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

除了这两个方法外,还有几个方法。

add方法用于向集合中添加元素。如果集合确实改变了就返回true。如果集合没有发生变化就返回false。

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

 

迭代器

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
    void forEachRemaining(Consumer<? super E> action);
}

通过反复调用next方法,可以逐个访问集合中的每个元素。但是如果到达了集合的末尾,next方法将抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法。如果迭代器还有多个供访问的元素,就返回true。

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

用for each循环可以更加简练地表示同样的循环操作,编译器简单地将for each循环翻译为带有迭代器的循环。for each循环可以与任何实现了Iterable接口的对象一起工作,这个接口只包含一个抽象方法:

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

Collection接口扩展了Iterable接口。因此标准库中的任何集合都可以使用for each循环。

在Java SE 8中,可以调用forEachRemaining方法并提供lambda表达式。将对迭代器的每一个元素调用这个lambda表达式,直到没有元素为止。

iterator.forEachRemaining(element -> ...);

元素访问的顺序取决于集合类型。ArrayList从索引0开始每迭代一次,索引值加0。HashSet会按照某种元素的次序出现。

Java迭代器位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。

Iterator接口的remove方法将会删除上次调用next方法时返回的元素。如果要删除指定位置的元素,仍然需要越过这个元素。

next方法和remove方法调用具有相互依赖性。如果调用remove之前没有调用next将是不合法的。如果这样做将抛出一个IllegalStateException。

 

泛型实用方法

由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。

例如检测任意集合是否包含指定元素的泛型方法:

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

如果实现Collection接口的每个类都要提供如此多的例行方法。为了能够让实现者更容易实现接口,Java类库提供一个类AbstractCollection,它将基础方法size和iterator抽象化,但是在此提供了例行方法。

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

    public boolean contains(Object o) {
        Iterator<E> it = iterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return true;
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return true;
        }
        return false;
    }
}

具体的集合类可以扩展AbstractCollection类。由具体的集合类提供iterator方法,而contains方法已有AbstractCollection超类提供了。子类也可以自己提供contains方法。

Java SE 8中,Collection添加了许多与流相关的默认方法:

default boolean removeIf(Predicate< ? super E> filter)

 

集合框架柱的接口

集合有两个基本接口:Collection和Map。List是一个有序集合(ordered collection)。元素会增加到容器中的特定位置。两种方式访问元素:使用迭代器访问或者使用一个整数索引来访问。后一种方法称为随机访问(random access),因为可以按任意顺序访问元素:

void add(int index, E element)

void remove(int index)

E get(int index)

E set(int index, E element)

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

void add(E element)

由数组支持的有序集合可以快速地随机访问,因此适合使用List方法并提供一个整数索引来访问。链表尽管也是有序的,但是随机访问很慢,所以最好使用迭代器来遍历。

 

Set接口等同于Collection接口,不过其方法的行为有更严谨的定义。集(set)的add方法不允许增加重复元素。要适当地定义集的equals方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。hashCode方法的定义要包含相同元素的两个集会得到相同的散列码。

 

2.具体的集合

集合类型描述
ArrayList一种可以动态增长和缩减的索引序列
LinkedList一种可以在任意位置进行高效地插入和删除操作的有序序列
ArrayDeque一种用循环数组实现的双端队列
HashSet一种没有重复元素的无序集合
TreeSet一种有序集
EnumSet一种包含枚举类型值的集
LinkedHashSet一种可以记住元素插入次序的集
PriorityQueue一种允许高效删除最小元素的集合
HashMap一种存储键/值关联的数据结构
TreeMap一种键值有序排列的映射表
EnumMap一种键值属于枚举类型的映射表
LinkedHashMap一种可以记住键/值项添加次序的映射表
WeakHashMap一种其值无用武之地后可以被垃圾回收器回收的映射表
IdentityHashMap一种用==而不是用equals比较键值的映射表

链表

数组和数组列表都有一个重大缺陷。从数组的中间位置删除一个元素要付出很大的代价,原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。在数组中间的位置插入一个元素也是如此。

数组在连续的存储位置上存放对象引用。而链表(linked list)将每个对象存放在独立的结点中。每个结点还存放着序列中下一个结点的引用。在Java中,所有链表实际上都是双向链接的(doubly linked),即每个结点还存放着指向前驱结点的引用。

从链表中删除元素,只需要更新被删除元素附近的链接。

List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Iterator<String> iter = staff.iterator();
String first = iter.next();
String second = iter.next();
iter.remove();

LinkedList.add方法将对象添加到链表的尾部。但是常常需要将元素添加到链表中间,由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。集(set)的元素完全无序。因此在Iterator接口中没有add方法。相反地,集合类库提供了子接口ListIterator,其中包含add方法:

interface ListIterator<E> extends Iterator<E> {
    void add(E element);
    ...
}

与Collection.add不同,这个方法不返回boolean类型的值,它假定添加操作总会改变链表。

ListIterator接口,可以反向遍历链表:

E previous()

boolean hasPrevious()

List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
ListIterator<String> iter = staff.listIterator();
iter.next();
iter.add("Juliet");
staff.forEach(System.out::println);

如果多次调用add方法,将按照提供的次序把元素添加到链表中,它们被依次添加到迭代器当前位置之前。当用一个刚刚返回的Iterator,并且指向链表表头的迭代器调用add时,新添加的元素将变成列表的新表头。

如果链表有n个元素,有n+1个位置可以添加元素。

set方法用一个新元素取代调用next或previous方法返回的上一个元素。

如果在某个迭代器修改集合时,另一个迭代器对它进行遍历,会出现混乱的情况,会抛出一个ConcurrentModificationException。

Collection接口中声明了很多用于对链表操作的有用方法。其中大部分都是LinkedList类的超类AbstractCollection实现(toString,contains)。

链表不支持快速地随机访问。鉴于这个原因,在程序需要采用整数索引访问元素时,通常不选用链表。尽管LinkedList还是提供一个get方法访问某个特定元素,但这个方法并不是太效率。

绝不要使用下面这种让人费解的方式来遍历链表,效率极低,每次查找一个元素都要从列表头部开始搜索,LinkedList对象根本不作任何缓存位置信息的操作。

for (int i = 0; i < list.size(); i++) {
    list.get(i);  // LinkedList
}

get方法做了微小的优化,如果索引值大于size()/2就从列表尾端开始搜索元素。

 

列表迭代器接口可以告之当前索引。因为Java迭代器是在两个元素之间,所以可以产生两个索引值:nextIndex方法返回下一次调用next方法时返回元素的整数索引;previousIndex返回下一次调用previous方法时返回元素的整数索引。两个索引差值为1。这两个方法的效率非常高,因为迭代器保持着当前位置的计数器。

如果有一个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素前面的位置。调用next与调用list.get(n)会产生同一个元素,只是获得这个迭代器的效率比较低。

使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素所付出的代价。如果列表只有少数几个元素,完全可以使用ArrayList。避免使用整数索引表示链表中位置的所有方法。如果需要对集合进行随机访问,就使用数组或ArrayList,而不要使用链表。

public class LinkedListTest {
    public static void main(String[] args) {
        List<String> a = new LinkedList<>();
        a.add("Amy");
        a.add("Carl");
        a.add("Erica");

        List<String> b = new LinkedList<>();
        b.add("Bob");
        b.add("Doug");
        b.add("Frances");
        b.add("Gloria");

        ListIterator<String> aIter = a.listIterator();
        Iterator<String> bIter = b.iterator();

        while (bIter.hasNext()) {
            if(aIter.hasNext()) {
                aIter.next();
            }
            aIter.add(bIter.next());
        }

        System.out.println(a);

        bIter = b.iterator();
        while (bIter.hasNext()) {
            bIter.next();
            if(bIter.hasNext()) {
                bIter.next();
                bIter.remove();
            }
        }

        System.out.println(b);

        a.removeAll(b);

        System.out.println(a);
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值