持有对象

第11章 持有对象

11.1 泛型和类型安全的容器

通过使用泛型,可以在编译期防止将错误类型的对象放置到容器中,在将元素从容器中取出时,容器知道保持类型,并在调用get()时自动执行转型。

当指定了某个类型作为泛型参数时,我们可以将其导出类型放入该容器中,编译器会自动对其进行向上转型。

11.2 基本概念

java容器类库的用途是保存对象,并根据容器中每个槽保存的元素个数不同,将其划分为两个不同的概念:

  • Collection: 一个独立元素的序列,这些元素都服从一条或多条规则。

    (1)List必须按照插入的顺序保存元素。

    (2)Set不能有重复元素。

    (3)Queue按照排队规则来确定对象产生的顺序。

  • Map: 一组成对的键值对对象,允许你使用建来查找值。映射表允许我们使用另一个对象来查找某个对象,它也被称为关联数组,或字典。

编写的大部分代码都是在与接口打交道,并且唯一需要知道所使用的精确类型的地方就是在创建的时候:

List<Apple> apples = new ArrayList<Apple>();

使用接口的目的在于如果你决定去修改你的实现,你所需的只是再创建处修改它:

List<Apple> apples = new LinkedList<Apple>();

因此,你应该创建一个具体类的对象,将其转型为接口,然后在其余的代码中都是用这个接口。

若要使用某些类的额外功能,例如,LinkedList具有在List接口中未包含的额外方法,TreeMap和Map也是如此。则不能将它们向上转型为更通用的接口。

Collection接口概括了序列的概念:一种存放一组对象的方式。

11.3 添加一组元素

在java.util包中的Arrays和Collections类中都有很多实用的方法,可以在一个Collectio中添加一组数据:

  • Arrays.asList():接收一个数组或一个用逗号分割的元素列表,并将其转换成一个List对象。但其底层表示的是数组,因此不能调整尺寸。若试图用add()或delete()方法在这种列表中添加或删除元素,就可能会引发去改变数组尺寸的尝试,获得运行时异常Unsupported Operation。

  • Collections.addAll():接收一个Collection对象,以及一个数组或一个用逗号分割的列表,将元素添加到Collection中。

  • Collection的构造器:接收另一个Collection,用它来将自身初始化。

public class AddingGroups {
    public static void main(String[] args) {
        Collection<Integer> collection = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4, 5));
        Integer[] moreInts = { 6, 7, 8, 9, 10 };
        collection.addAll(Arrays.asList(moreInts));
        Collections.addAll(collection, 11, 12, 13, 14, 15);
        Collections.addAll(collection, moreInts);
        List<Integer> list = Arrays.asList(16, 17, 18, 19, 20);
        list.set(1, 99);
        //        list.add(100);     java.lang.UnsupportedOperationException
        System.out.println(collection);
        System.out.println(list);
    }
}

使用Arrays.asList创建List时,根据List的泛型类型无法添加其泛型子类,需要使用显示类型参数说明

class Snow{}
class Powder extends Snow{}
class Light extends Powder{}
class Heavy extends Powder{}
class Crusty extends Snow{}
class Slush extends Snow{}

public class AsListInference {
    public static void main(String[] args) {
        List<Snow> snow1 = Arrays.asList(new Crusty(),new Slush(),new Powder());
        
//      cannot convert from List<Powder> to List<Snow>
//      List<Snow> snow2 = Arrays.asList(new Light(),new Heavy());
        
        List<Snow> snow3 = Arrays.<Snow>asList(new Light(),new Heavy());
    }
}

11.4 容器的打印

public class PrintingContainers {
    static Collection fill(Collection<String> collection) {
        collection.add("rat");
        collection.add("cat");
        collection.add("dog");
        collection.add("dog");
        return collection;
    }

    static Map fill(Map<String, String> map) {
        map.put("rat", "Fuzzy");
        map.put("cat", "Rags");
        map.put("dog", "Bosco");
        map.put("dog", "Spot");
        return map;
    }

    public static void main(String[] args) {
        System.out.println(fill(new ArrayList<String>()));
        System.out.println(fill(new LinkedList<String>()));
        System.out.println(fill(new HashSet<String>()));
        System.out.println(fill(new TreeSet<String>()));
        System.out.println(fill(new LinkedHashSet<String>()));
        System.out.println(fill(new HashMap<String, String>()));
        System.out.println(fill(new TreeMap<String, String>()));
        System.out.println(fill(new LinkedHashMap<String, String>()));
    }
}

Java容器类库中的两种主要类型,它们的区别在于容器每个槽保存的元素个数:

  • Collection在每个槽中只能保存一个元素,此容器包括:

    1. List:它以特定的顺序保存一组元素。ArrayList和LinkedList都是List类型,两者的不同之处不仅在于执行某些类型的操作时的性能,而且LinkedList包含的操作也多余ArrayList。

    2. Set:元素不能重复。HashSet、TreeSet和LinkedHashSet都是Set类型,输出显示不同,Set实现存储元素的方式也不同。

      (1) HashSet:使用的是相当复杂的方式来存储元素的,这种方式是最快的获取元素方式,因此存储的顺序看起来并无实际意义(通常你只会关心某事物是否是某个Set的成员,而不会关心它在Set中的顺序)。

      (2) TreeSet:按照比较结果的升序保存对象。

      (3) LinkedHashSet:按照被添加的顺序保存对象。

    3. Queue:只允许从容器的一端插入对象,并从另外一端移除对象。

  • Map在每个槽内保存了两个对象,即键和与之相关联的值。使得你可以用键来查找对象,就像一个简单的数据库。对于每一个键,Map只接受存储一次。并且Map会自动地调整尺寸。键和值在Map中的保存顺序并不是它们的插入顺序。Map的实现方式也有三种:

    1. HashMap:提供了最快的查找技术,没有按照任何明显的顺序来保存其元素。

    2. TreeMap:按照比较结果的升序来保存键。

    3. LinkedHashMap:按照插入顺序保存键,同时还保留了HashMap的查询速度。

11.5 List

List:将元素维护在特定的序列中,允许被创建后添加元素,移除元素,或自我调整尺寸。即:一种可修改的序列。 其主要有两种子类型:

  • ArrayList:擅长于随机访问元素,但是在List的中间插入和移除元素时较慢。
  • LinkedList:插入和删除操作代价较低,并提供了优化的顺序访问。但在随机访问方面相对较慢。

List常用方法:

  • contains():传入对象引用,确定某个对象是否在列表中。
  • remove():传入对象引用,移除此对象。
  • indexOf():传入对象引用,发现该对象再List中所处位置的索引编号。
  • subList():从较大的列表中创建出一个片段。
  • retainAll():有效的交集操作。所产生的行为依赖于equals()方法。
  • removeAll():它将从List中移除在参数List中的所有元素,行为也基于equals()方法。
  • set():它的功能是在指定索引处(第一个参数),用第二个参数替换整个位置的元素。
  • addAll():重载方法,在指定索引处插入新的列表。
  • isEmpty():是否为空。
  • clear():清空。
  • toArray():将任意Collection转换为一个数组。
  • 当需要对对象引用进行判断时,都会用到equals()方法。List的行为根据equals()的行为而有所变化。

11.6 迭代器

如果原本是对List编码,但后来发现Set需要相同的功能。如何使代码变得通用,它们只是使用容器,不知道或不关心容器的类型。

迭代器则可以达成此目的:迭代器是一个对象,它的工作是遍历并选择序列中的对象,而客户端程序员不必知道或关心该序列底层的结构。

Java中的Iterator只能单向移动,并拥有以下方法:

  • iterator():要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。
  • next():获得序列中的下一个元素。
  • hasNext():检查序列中是否还有元素。
  • remove():将迭代器返回的元素删除。移除由next()产生的最后一个元素,这意味着在调用remove()之前必须先调用next()。

下例中的方法接收任何实现了Iterator接口的容器对象,进而遍历它们:

public class CrossContainerIteration {
    public static void display(Iterator<Pet> it) {
        while (it.hasNext()) {
            Pet p = it.next();
            System.out.print(p.id() + ":" + p + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        ArrayList<Pet> pets = Pets.arrayList(8);
        LinkedList<Pet> petsLL = new LinkedList<Pet>(pets);
        HashSet<Pet> petsHS = new HashSet<Pet>(pets);
        TreeSet<Pet> petsTS = new TreeSet<Pet>(pets);
        display(pets.iterator());
        display(petsLL.iterator());
        display(petsHS.iterator());
        display(petsTS.iterator());
    }
}

display()方法不包含任何有关它所遍历的序列的类型信息,这也展示了Iterator的真正作用:能够将遍历序列的操作与序列底层的结构分离。

11.6.1 ListIterator

ListIterator 是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。尽管Iterator只能向后移动,但是ListIterator可以双向移动。

public class ListIteration {
    public static void main(String[] args) {
        ArrayList<Pet> pets = Pets.arrayList(8);
        ListIterator<Pet> listIterator = pets.listIterator();
        while (listIterator.hasNext()) {
            System.out.print(listIterator.next() + " "
                             + listIterator.nextIndex() + " "
                             + listIterator.previousIndex() + ", ");
        }
        System.out.println();
        while (listIterator.hasPrevious()) {
            System.out.print(listIterator.previous().id() + " ");
        }
        System.out.println();
        listIterator = pets.listIterator(3);
        while (listIterator.hasNext()) {
            listIterator.next();
            listIterator.set(Pets.randomPet());
        }
        System.out.println(pets);
    }
}

ListIterator 使用方式:

  • listIterator():产生一个指向开始处的ListIterator。
  • listIterator(n):产生一个指向索引为n的元素处的ListIterator。
  • previousIndex():产生迭代器在列表中的当前位置的前一个索引。
  • nextIndex():产生迭代器在列表中的当前位置的后一个索引。
  • hasPrevious():是否有前一个元素。
  • previous():获取前一个元素。
  • hasNext():是否有后一个元素。
  • next():获取后一个元素。

11.7 LinkedList

LinkedList像ArrayList一样实现了基本的List接口,但是它执行插入和移除时比ArrayList更高效,但在随机访问操作方面却要逊色一些。

LinkedList还添加了可以使其用作栈、队列或双端队列的方法。

11.8 Stack

栈:后进先出(LIFO)的容器。

LinkedList具有能够直接实现栈的所有功能的方法。

public class Stack<T> {
    private LinkedList<T> storage = new LinkedList<T>();
    public void push(T v) {
        storage.addFirst(v);
    }
    public T peek() {
        return storage.getFirst();
    }
    public T pop() {
        return storage.removeFirst();
    }
    public boolean empty() {
        return storage.isEmpty();
    }
    public String toString() {
        return storage.toString();
    }
}

类名之后的< T >告诉编译器这将是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,就是T。Stack是用LinkedList实现的,而LinkedList也被告知它将持有T类型对象。

11.9 Set

Set不保存重复的元素,它最常被用来判断某个对象是否在某个Set中。所以,查找成为了Set中最重要的操作,因此,HashSet是个最佳选择,它专门对快速查找进行了优化。

Set具有与Collection完全一样的接口,没有额外功能。实际上,Set就是Collection,只是行为不同。这也是继承和多态思想的典型应用:表现不同的行为。

Set的三种实现方式:

  • HashSet:输出顺序没有规律可寻,出于速度考虑,使用了散列函数。
  • TreeSet:将元素存储在红-黑树数据结构中。
  • LinkedHashSet:因为查询速度的原因也使用了散列,但看起来它使用了链表来维护元素的插入顺序。

如果想对结果排序,则是使用TreeSet来代替HashSet,并且可以向TreeSet的构造器中传入比较器(建立排列顺序的对象)。

public class UniqueWordsAlphabetic {
    public static void main(String[] args) {
        Set<String> words = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);

        words.addAll(Arrays.asList("Z", "door", "care", "tree",
            "X", "all", "before", "yes", "e", "from", "G", "Home", "Set"));

        System.out.println(words);
    }
}

/*  Output:
[all, before, care, door, e, from, G, Home, Set, tree, X, yes, Z]
*/

11.10 Map

将对象映射到其他对象的能力是一种解决编程问题的杀手锏。 例如,使用一个程序,用来检查Java的Random类的随机性,Map可以轻易地解决该问题:

public class Statistics {
    public static void main(String[] args) {
        Random random = new Random(47);
        Integer freq;
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        for (int i = 0; i < 10000; i++) {
            int x = random.nextInt(20);
            freq = map.get(x);
            map.put(x, freq != null ? freq + 1 : 1);
        }
        System.out.println(map);
    }
}

Map可以返回它的键的Set,它的值的Collection。即使用keySet()方法产生键的Set,然后使用foreach语句遍历该Map。

11.11 Queue

队列:一个典型的先进先出(FIFO)的容器。即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。

队列常被当作一种可靠的将对象从程序的某个区域传输到另一个区域的途径,在并发编程中特别重要。

LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此,LinkedList可以用作Queue的一种实现:

public class QueueDemo {
    public static void printQ(Queue queue) {
        while (queue.peek() != null) {
            System.out.print(queue.remove() + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<Integer>();
        Random random = new Random(47);
        for (int i = 0; i < 10; i++) {
            queue.offer(random.nextInt(i + 10));
        }
        printQ(queue);

        Queue<Character> qc = new LinkedList<Character>();
        for (Character c : "Brontodsurus".toCharArray()) {
            qc.offer(c);
        }
        printQ(qc);
    }
}

Queue常用方法:

  • offer():在允许的情况下,将一个元素插入到队尾,或者返回false。
  • peek():在不移除的情况下返回队头,在队列为空时返回null。
  • element():在不移除的情况下返回队头,在队列为空时会抛异常。
  • poll():移除并返回队头,在队列为空时返回null。
  • remove():移除并返回队头,在队列为空时会抛异常。

11.11.1 PriorityQueue

队列规则:在给定一组队列中的元素的情况下,确定下一个弹出队列的元素的规则。

  • 先进先出:下一个弹出元素应该是等待时间最长的元素。
  • 优先级队列:下一个弹出元素是具有最高的优先级的元素。

PriorityQueue调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序,但是可以通过提供Comparator(比较器)来修改这个顺序。

PriorityQueue允许重复,最小的值拥有最高的优先级(如果是String,空格也可以算作值,并优先级比字母高)

11.12 Collection和Iterator

Collection:描述所有序列容器的共性的根接口。 java.util.AbstractCollection类提供了Collection的默认实现。

使用接口描述的一个理由是:它可以使我们能够创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多的对象类型。 例如:我们编写的方法接收一个Collection,那么此方法就可以应用于任何实现了Collection的类。

使用迭代器来表示容器之前的共性,而实现Collection就意味着需要提供iterator()方法。如果直接实现Collection接口,就必须实现该接口中的所有方法。因此,我们可以继承AbstractCollection来简化代码:

public class CollectionSequence extends AbstractCollection<Pet> {
    private Pet[] pets = Pets.createArray(8);

    public int size() { return pets.length; }
    public Iterator<Pet> iterator() {
        return new Iterator<Pet>() {
            private int index = 0;

            public boolean hasNext() { return index < size(); }
            public void remove() { throw new UnsupportedOperationException(); }
            public Pet next() {
                if (hasNext())  
                        return pets[index++];
                return null;
            }
        };
    }

    public static void main(String[] args) {
        CollectionSequence c = new CollectionSequence();
        InterfaceVsIterator.display(c);
        InterfaceVsIterator.display(c.iterator());
    }
}

11.13 ForEach与迭代器

下面这段代码展示了,所有Collection对象都可以与foreach一起工作的特性:

public class ForEachCollections {
    public static void main(String[] args) {
        Collection<String> cs = new LinkedList<String>();
        Collections.addAll(cs, "Take the long way home".split(" "));
        for (String s : cs) {
            System.out.print("'" + s + "' ");
        }
    }
}

其工作原理是:JavaSE5 引入的Iterable的接口,该接口包含了一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。 因此,如果创建了任何实现了Iterable接口的类,都可以将它用于foreach语句中:

public class IterableClass implements Iterable<String> {
    protected String[] words = ("And that is how" +
                                "we konw the Earth to be banana-shaped").split(" ");

    public Iterator<String> iterator() {
        return new Iterator<String>() {
            private int index = 0;

            public boolean hasNext() { return index < words.length; }
            public String next() { return words[index++]; }
            public void remove() { throw new UnsupportedOperationException(); }
        };
    }

    public static void main(String[] args) {
        for (String s : new IterableClass()) {
            System.out.print(s + " ");
        }
    }
}

在Java SE5中,大量的类都是Iterable类型,主要包括所有的Collection类(不包括Map)。foreach可用作于数组,但是数组不是Iterable类型

11.13.1 适配器方法惯用法

假设你希望可以选择向前的方向或向后的方向迭代一个单词列表。如果继承一个Collection,并覆盖iterator()方法,只能替换现有方法,而不能实现选择。

当你有一个接口并需要另一个接口时,使用适配器设计模式则可以解决问题:

public class MultiIterableClass extends IterableClass {
    public Iterable<String> reversed() {
        return new Iterable<String>() {
            public Iterator<String> iterator() {
                return new Iterator<String>() {
                    int current = words.length - 1;
                    public boolean hasNext() { return current > -1; }
                    public String next() { return words[current--]; }
                    public void remove() {}
                };
            }
        };
    }

    public Iterable<String> randomized() {
        return new Iterable<String>() {
            public Iterator<String> iterator() {
                List<String> shuffeld = new ArrayList<String>(Arrays.asList(words));
                Collections.shuffle(shuffeld, new Random(66));
                return shuffeld.iterator();
            }
        };
    }
    
    public static void main(String[] args) {
        MultiIterableClass mic = new MultiIterableClass();
        for (String s : mic.reversed())
            System.out.print(s + " ");
        System.out.println();
        for (String s : mic.randomized()) 
            System.out.print(s + " ");
    }
}

11.14 总结

(1)数组将数字与对象联系起来,它保存类型明确的对象,查询对象时,不需要对结果做类型转换。它可以是多维的,可以保存最近基本的数据。但是,数组一旦生成,其容量就不能改变。

(2)Collection保存单一的元素,而Map保存相关联的键值对。有了Java的泛型,你就可以指定容器存放的对象类型,因此你就不会将错误类型的对象放置容器中,并且从容器中获取元素时,不必进行类型转换。各种Collection和Map都会自动调整尺寸。容器不能持有基本类型,但自动包装机制会自动转换。

(3)像数组一样,List也建立数字索引与对象的关联,因此,数组和List都是排好序的容器,List能自动扩充容量。

(4)大量的随机访问用ArrayList,经常从列表中插入或删除元素,则用LinkedList。

(5)各种Queue以及Stack的行为,LinkedList都支持。

(6)Map是一种将对象与对象关联的设计,HashMap设计用来快速访问;TreeMap保持键始终处于排序状态,所以没HashMap访问速度快;LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问能力。

(7)Set不接收重复元素。HashSet提供最快的查询速度;TreeSet保持元素处于排序状态;LinkedHashSet以插入顺序保存元素。

(8)新程序中不应使用过时的Vector、Hashtable和Stack。

public class ToDoList extends PriorityQueue{ static class ToDoItem implements Comparable { private char primary; private int secondary; private String item; public ToDoItem(char primary, int secondary, String item) { this.primary = primary; this.secondary = secondary; this.item = item; } public int compareTo(ToDoItem arg) { if(this.primary > arg.primary) return 1; if(this.primary == arg.primary) if(this.secondary > arg.secondary) return 1; else if(this.secondary == arg.secondary) return 0; return -1; } public String toString() { return Character.toString(primary) + secondary + ": " + item; } }

public void add(char primary, int secondary, String item) {
    super.add(new ToDoItem(primary, secondary, item));
}

public static void main(String[] args) {
    ToDoList toDoList = new ToDoList();
    toDoList.add('C', 4, "Empty trash");
    toDoList.add('A', 2, "Feed dog");
    toDoList.add('B', 7, "Feed bird");
    toDoList.add('C', 3, "Mow lawn");
    toDoList.add('A', 1, "Water lawn");
    toDoList.add('B', 1, "Feed cat");
    while(!toDoList.isEmpty()) 
        System.out.println(toDoList.remove());
}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值