Java 编程思想笔记:Learn 8

第 11 章 持有对象

如果一个程序只包含固定数量的且生命周期都是已知的对象,那么这是一个非常简单的程序。

通常,程序总是根据运行时才知道的某些条件去创建新对象。在此之前,不会知道所需对象的数量,甚至不会知道确切的类型。为解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。所以,就不依靠创建命名的引用来持有每个对象:

MyType aReference;

Java 有多种方式保存对象(应该说是对象的引用)。例如前面曾经学过的数组,它是编译器支持的类型。数组是保存一组对象的最有效的方式,如果你想保存一组基本类型数据,也推荐使用这种方式。但是数组具有固定的尺寸,而在更一般的情况中,你在写程序时并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,因此数组尺寸固定这一限制显得过于受限了。

Java 使用库提供了一套相当完整的容器类来解决这个问题,其中基本的类型是 List/ Set/ Queue 和 Map。这个对象类型也称为 集合类,我称为 容器。

容器解决了存储不确定数量的问题。此外容器还有一些其他的特性,Set 对于每个值都保存一个对象,Map 允许你将某些对象与其他对象关联起来的关联数组,Java 容器类都可以自动地调整自己的尺寸。因此,与数组不同,可以将任意数组的对象放置到容器中,并且不需要担心容器设置的大小问题。

11.1 泛型和类型安全的容器

ArrayList 最大的一个特性是,允许向容器中插入不同的类型。比如可以把 Apple 和 Orange 都放置在容器中。正常情况下,Java会报告警告信息,因为这个示例没有使用泛型。但是可以使用@SuppressWarnings注解及其参数表示只有有关 “不受检查的异常” 的警告信息应该被抑制:

class Apple{
    private static long counter;
    private final long id = counter++;

    public long id(){
        return id;
    }
}

class Orange{}

public class ApplesAndOrangeWithoutGeneration {

    @SuppressWarnings("unchecked")
    public static void main(String[] args){
        ArrayList apples = new ArrayList();
        for(int i = 0; i < 3; i++){
            apples.add(new Apple());
        }
        apples.add(new Orange());
        for(int i = 0; i < apples.size(); i++){
            ((Apple) apples.get(i)).id();
            // Orange is detected only run
        }
    }
}

运行后会有如下提示:

Exception in thread "main" java.lang.ClassCastException: Orange cannot be cast to Apple
    at ApplesAndOrangeWithoutGeneration.main(ApplesAndOrangeWithoutGeneration.java:24)

这说明,ArrayList 允许添加不同类型的元素,直到运行时才会被检查。

@SuppressWarnings("unchecked") 这个可以消除编译器时的警告。、

Apple 和 Orange 类是有区别的,它们除了都是 Object 之外没有任何共性。因为 ArrayList 保存的是 Obejct,因此你不仅可以通过 ArrayList 的 add() 方法将 Apple 对象放进这个容器,还可以添加 Orange 对象,而且无论在编译期还是在运行期都不会有问题。当你在使用 ArrayList 的 get() 方法来取出你认为是 Apple 的对象时,你得到的只是 Object 引用,必须将其转型为 Apple, 因此,需要将整个表达式括起来,在调用 Apple 的 id() 方法之前,强制执行转型。否则,你就会得到语法错误。在运行时,当你试图将 Orange 对象转型为 Apple 时,就会得到一个错误。

通过使用泛型,就可以在编译期防止将错误类型放置到容器中。尖括号括起来的是类型参数, (类型参数可以是多个)。

public class ApplesAndOrangeWithoutGeneration {

    @SuppressWarnings("unchecked")
    public static void main(String[] args){
        ArrayList<Apple> apples = new ArrayList();
        for(int i = 0; i < 3; i++){
            apples.add(new Apple());
        }
        // apples.add(new Orange());
        for(int i = 0; i < apples.size(); i++){
            ((Apple) apples.get(i)).id();
            // Orange is detected only run
        }
    }
}

在将元素从 List 中取出时,类型转换也不再是必需的。因为 List 知道它保存是什么类型,因此它会在调用 get() 时替换你执行转换。这样,通过使用泛型,你不仅知道编译器将你放置到容器中的对象类型,而且在使用容器中的对象时,可以使用更加清晰的语法。

这个实例还表明,如果不需要使用每个元素的索引,你可以使用 foreach 语法来选择 List 中的每个元素。

当你指定了某个类型作为泛型参数时,也可是使用向上转型。

class GrannySmith extends Apple{}

class Gala extends Apple{}

class Fujj extends Apple{}

class Braeburn extends Apple{}

public class GenericsAndUpcasting {

    public static void main(String[] args){
        ArrayList<Apple> appleArrayList = new ArrayList<Apple>();
        appleArrayList.add(new GrannySmith());
        appleArrayList.add(new Gala());
        appleArrayList.add(new Fujj());
        appleArrayList.add(new Braeburn());
        for(Apple c : appleArrayList){
            System.out.println(c);
        }
    }
}

11.2 基本概念

Java 容器类类库的用途是,“保存对象”,并将其划分为两个不同的概念:

  1. Collection. 一个独立元素的序列,这些元素都服从一条或多条规则。List 必须按照插入的顺序保存元素,而Set 不能有重复元素。Queue 按照排队规则来确定对象产生的对象。

  2. Map。一组成对的 “键值对” 对象,允许你使用键来查找值。ArrayList 允许你使用数字来查找值,它将数字与对象关联在一起。映射表允许我们使用另一个对象来查找某个对象,它被称为关联数组。

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

在这个方法中,ArrayList 已经被向上转型为 List。一般情况下,应该创建一个具体类的对象,将其转型为对应的接口,然后在其余的代码中都使用这个接口。

但是这种方法也有弊端,因为某些类具有额外的功能,例如,LinkedList 具有在接口中未包含的额外方法

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

public class SimpleCollection {
    public static void main(String[] args){
        Collection<Integer> c = new ArrayList<Integer>();
        for(int i = 0; i < 10; i++){
            ((ArrayList<Integer>) c).add(i);
        }
        for(Integer i : c){
            System.out.println(i + ". ");
        }
    }
}

11.3 添加一组元素

在 java.util 包中的 Arrays 和 Collections 类中有很多实用方法,可以在一个 Collection 中添加一组元素。Arrays.asList() 方法接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),并将其转化为一个 List 对象。Collections.addAll() 方法接受一个 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};
        ((ArrayList<Integer>) collection).addAll(Arrays.asList(moreInts));
        Collections.addAll(collection, 11, 12, 13, 14);
        Collections.addAll(collection, moreInts);
        List<Integer> list = Arrays.asList(16,17,18,19);
        list.set(1, 99);
    }
}

Collection 的构造器可以接受另一个 Collection,用它将自身初始化,因此你可以使用 Arrays.List() 来为这个构造器产生输入。

但是声明一个空的 Collection ,然后再使用 Collection.addAll() 的方法运行速度要快很多。

也可以直接使用 Arrrays.asList() 的输出,将其当 做List, 但是在这种情况下,底层是数组,因此不能调整容量。

当使用 Arrays.asList() 方法时,当元素的类型是孙类型时,就会报错,这时需要指明类型。

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> snowList = Arrays.asList(new Crusty(), new Slush(), new Powder());

        List<Snow> snows = new ArrayList<Snow>();

        Collections.addAll(snows, new Light(), new Heavy());
        
        // 当添加的元素是 Snow 的孙类型时,需要在 Arrays.<Snow>asList 中声明类型,否则编译不能通过。
        List<Snow> snows1 = Arrays.<Snow>asList(new Light(), new Heavy());
    }
}

这种在 Arrrays.asList() 中间 插入一条 “线索“`,以告诉编译器对于由 Arrays.asList() 产生的List 类型,实际的目标类型应该是什么。这称为 “显示类型参数说明”。

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 TreeMap<String, String>()));
        System.out.println(fill(new LinkedHashMap<String, 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 在每个槽中只能保存一个元素。此类容器包括:List, 它以特定的顺序保存一组元素;Set,元素不能重复;Queue, 只允许在容器的一 “端” 插入对象,并从另外一端移除对象。Map 在每个槽内保存了两个对象,即键和与之相关联的值。

查看输出会发现,默认的打印行为,使用容器提供的 toString() 方法,打印的结果可读性很好。Collection 打印出来的内容用方括号扩住,每个元素由逗号分隔。

第一个 fill() 方法可以作用域所有类型的 Collection,这些类型都实现了用来添加新元素的 add() 方法。

ArrayList 和 LinkedList 都是 List 类型,从输出可以看出,它们都被按照被插入的顺序保存元素。

HashSet,TreeSet 和 LinkedHashSet 都是 Set 类型:

  • HashSet 获取元素的速度最快。
  • TreeSet 按照比较结果的升序保存对象
  • LinkedHashSet, 按照被添加的顺序保存对象

Map(也被称为关联数组)。HashMap / TreeMap / LinkedHashMap 都是Map。

  • HashMap,也提供了最快的查找技术
  • TreeMap,按照比较结果的升序保存键值
  • LinkedHashMap 则按照插入顺序保存键

11.5 List

List 是有序序列,List 接口在 Collection 的基础添加了大量的方法,使得可以在 List 的中间插入和移除元素。
有两种类型的 List:

  • 基本的 ArrayList, 随机访问元素速度快,但是在 List 的中间插入和移除元素时较慢
  • LinkedList,插入和删除操作比较快,但是随机访问比较慢。
    基本操作:
  • 查看元素是否存在,contain()
  • 移除一个对象,remove()
  • 查看一个对象的索引编号,indexOf()
  • 切片,subList()
  • 验证 List 是否包含某切片,containAll() ,在这个方法中类型并不重要。这一点,可以通过Collection.shuffle() 方法来验证。
  • retainAll(), 是求交集,保留在 copy 和 sub 中的元素
copy.retainAll(sub);
  • addAll(), 可以用来增加一个 sub list
  • isEmpty 和 clear,可以用来清空容器内的元素
  • toArray,可以把 Collection 转化为数组。

11.6 迭代器

任何容器类,都必须有某种方法可以插入元素并将它们再次取回。

迭代器是一个对象,也是一种设计模式,是一种通用的代码,可以用来迭代不同类型的容器。使用迭代器,程序员不必关系该序列底层的结构。迭代器通常被称为轻量级对象,创建它的代价小。Java 中的 Iterator 常有以下特性:

  • Iterator 只能单向移动
  • 使用方法 Iterator() 要求容器返回一个 Iterator,Iterator 将返回好序列的第一个元素
  • 使用 next() 获取序列中的下一个元素
  • 使用 hasNext() 检查序列中是否还有其他元素
  • 使用 remove() 将迭代器新近返回的元素删除
public class SimpleIteration{
  public static void main(String[] args){
      List<Integer> list = new ArrayList();
      list.addAll(Arrays.asList(1,2,3,4,4,5,6));
      Iterator<Integer> it = list.iteraotr();
      while(it.hasNext()){
          Integer inte = it.next();
          it.remove();
  }
  }
} 

可以组合使用 hasNextnext 来遍历迭代器的元素。如果只是遍历容器中的元素,而不进行修改的话,使用 foreach 更加简洁。

另外,如果想通过 Iterator 删除元素,在调用 remove 之前,一定要调用 next。

iterator 的真正威力在于:能够将遍历序列的操作与序列底层的结构分离。使用 foreach 语法时,我们往往还需要关注容器中元素的类型。

public class CrossContainerIteration {
    public static void display(Iterator<Integer> it){
        while (it.hasNext()){
            Integer i = it.next();
            System.out.println(i);
        }
        System.out.println();
    }
    public static void main(String[] args){
        ArrayList<Integer> arrayList = new ArrayList<Integer>(Arrays.asList(1,2,2,2,2,22,3,4,5,6,7,8,9,10));
        LinkedList<Integer> linkedList = new LinkedList<Integer>(arrayList);
        HashSet<Integer> hashSet = new HashSet<Integer>(arrayList);
        display(arrayList.iterator());
        display(linkedList.iterator());
        display(hashSet.iterator());
    }
}

11.6.1 ListIterator

ListIterator 是一个更加强大的 Iterator 的子类型,它只能用于各种 List 类的访问。

Iterator 只能向前移动,但是 ListIterator 可以双向移动。它还可以产生相对于迭代器在列表中指向的当前位置的前一个或后一个元素的索引,并且可以使用 set() 方法替换它访问过的最后一个元素。可以通过调用 listIterator() 方法产生一个指向 List 开始处的 ListIterator, 并且还可以通过调用 listIterator(n) 方法创建一个一开始就指向列表索引为 n 的元素处的 ListIterator.

public class ListIteration {
    public static void main(String[] args){
        List<Integer> list = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7));
        ListIterator<Integer> listIterator = list.listIterator();
        while (listIterator.hasNext()){
            System.out.println(listIterator.next() + ", " + listIterator.nextIndex() + ", " + listIterator.previousIndex());
        }
        System.out.println();
        while (listIterator.hasPrevious()){
            System.out.println(listIterator.previous());
        }
    }
}

ListIteration 实现了访问下一个元素 next(), 下一个元素的索引位置 nextIndex(), 之前的元素 previous() ,之前元素的索引位置 previousIndex()

11.7 LinkedList

LinkedList 也像 ArrayList 一样实现了基本的 List接口,但是 LinkedList 在执行插入和移除时比 ArrayList 更高效,在随机访问时效率要低。

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

这些方法中有些彼此之间只是名称有些诧异,或者只存在些差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。例如,getFirst() 和 element() 完全一样,它们都返回列表的头(第一个元素),而并不移除它,如果 List 为空,则抛出 NoSuchElementException. peek() 方法与这两个方式稍有差异,在列表为空时返回 null。

removeFirst() 与 remove() 也是完全一样的,他们移除并返回列表的头,而在列表为空时抛出 NoSuchElementException。poll() 稍有差异,它在列表为空时返回 null。

addFirst() 与 add() 和 addLast() 相同,它们都将某个元素插入到列表的尾部(端)部。

removeLast() 移除并返回列表的最后一个元素。

public class LinkedListFeatures {
    public static void main(String[] args){
        LinkedList<Integer> linkedList = new LinkedList<>(Arrays.asList(1,2,3,4,5,6));
        System.out.println(linkedList);
        // Only differs in empty-list behavior;
        System.out.println("linkedList.getFirst: " + linkedList.getFirst());
        System.out.println("linkedList.element: " + linkedList.element());
        System.out.println("linkedList.element: " + linkedList.peek());
        // Only differs in empty-list behavior
        System.out.println("linkedList.poll: "+ linkedList.poll());
        System.out.println(linkedList);
        linkedList.addFirst(778);
        System.out.println("after linkedList add first: "+ linkedList);
        linkedList.offer(990); // add the element at the tail of linkedList
        System.out.println("linkedList add the element at the tail of linkedList: " + linkedList);
        linkedList.addLast(999);
        System.out.println("linkedList add the element at the last of linkedList: " +  linkedList);
        System.out.println("linkedList remove last of linkedList: "+ linkedList.removeLast());
    }
}

11.8 Stack

"栈" 通常是指 “后进先出” (LIFO)的容器。有时栈也被称为 “叠加栈”,因为最后压入栈的元素,第一个弹出栈。经常用来类比栈的事物是装有弹簧的储放器中的自动参托盘,最后装入托盘的总是最先拿出来使用。

LinkedList 具有能够直接实现栈的所有功能的方法,因此可以直接将 LinkedList 作为栈使用。不过,有时一个真正的栈更能把事情讲清楚:

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

这里通过使用泛型,引入了在栈的类定义中最简单的可行示例。类名之后的 <T> 告诉编译器这将是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,就是 T。大体上,这个类是在声明 “我们在定义一个可以持有 T 类型对象的 Stack”。Stack 是用 LinkedList 实现的,而 LinkedList 也被告知它将持有 T 类型对象。注意,push() 接受的是T类型的对象,而 peek() 和 pop() 将返回 T 类型的对象。peek() 方法将提供栈顶元素,但是并不将其从栈顶移除,而 pop() 将移除并返回栈顶元素。

public class SetOperations {
    public static void main(String[] args){
        Set<String> set1 = new HashSet<>();
        Collections.addAll(set1, "a b c d e  f g  h u ".split(" "));
        set1.add("M");
        System.out.println("H: "+ set1.contains("H"));
        System.out.println("N: " + set1.contains("a"));
        System.out.println();
        Set<String> set2= new HashSet<>();
        Collections.addAll(set2, "H I J K L".split(" "));
        System.out.println("set2 in set1: " + set1.contains(set2));
        set1.removeAll(set2);
        System.out.println("set2 removed from set1: " + set1);
        Collections.addAll(set1, "X Y Z".split(" "));
        System.out.println("X Y Z added to sell: " + set1);
    }
}

11.10 Map

将对象映射到其他对象的能力是一种杀手锏。
如这有一个问题是测试 Java Random 的随机性,可以利用 Map键值对的特性来进行处理。

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

在 main() 中,自动包装机之将随机生成的 int 转化为 HashMap 可以使用的 Integer引用(不能使用基本类型的容器)。如果键不在容器中,get() 方法将返回 null...

介绍一下 map 中的 containsKey() / containsValue() 怎么用

public class PetMap {
    public static void main(String[] args){
        Map<String, String> petMap = new HashMap<>();
        petMap.put("My cat", "cat");
        petMap.put("My Dog", "Dog");
        petMap.put("My Hamster", "Hamster");
        System.out.println(petMap);
        String dog = petMap.get("My Dog");
        System.out.println(dog);
        System.out.println(petMap.containsKey("My Dog"));
        System.out.println(petMap.containsValue("Dog"));
    }
}

Map 与 数组和其他的 Collection 一样,可以扩展到多维。
两个都是静态的数据结构才能够互相调用。

public class MapOfList {
    // 两个都是静态的数据结构才能够互相调用。
    public static Map<String, List<? extends String>> petPeople = new HashMap<>();

    static {
        petPeople.put("Dawn", Arrays.asList("Molly", "Spot"));
        petPeople.put("Kate", Arrays.asList("Shackleton", "Margrett"));
        petPeople.put("Luke", Arrays.asList("Fizzy", "Freckly"));
    }

    public static void main(String[] args){
        // keySet 拿到 key 值
        System.out.println("People: " + petPeople.keySet());
        // values 拿到 字典值
        System.out.println("Pets: " + petPeople.values());
        for(String person : petPeople.keySet()){
            System.out.println(person + " has: ");
            for(String pet : petPeople.get(person)){
                System.out.println("  " + pet);
            }
        }
    }
}

11.11 Queue

队列 Queue 是一个典型的先进先出 FIFO 的容器。即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。队列常备当作一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中特别重要,因为它们可以安全地将对象从一个任务传输到另一个任务。

LinkedList 提供了方法以支持队列的行为,并且它实现了 Queue 接口,因此 LinkedList 可以用作 Queue 的一种实现。通过将 LinkedList 向上转型为 Queue,下面是 Queue 接口中与 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<>();
        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<>();
        for(char c: "Brontosauras".toCharArray()){
            qc.offer(c);
        }
        printQ(qc);
    }
}

offer() 方法是与 Queue 相关的方法之一,它在允许的情况下,将一个元素插入到队尾,或者返回 false. peek() 和 element() 方法都将在 不移除的情况下返回队头,但是 peek() 方法在队列为空时返回 null,而 element() 则会抛出 NoSuchElementException 异常。poll() 和 remove() 方法将移除并返回队头,但是 poll() 在队列为空时返回 null,而remove() 则会抛出 NoSuchElementException 异常。

自动包装机制会自动地将 nextInt() 方法的 int() 结果转换为 queue 所需的 Integer 对象,将 char c 转化为 qc 所需的 Character 对象。Queue 接口窄化了对 LinkedList 的方法的权限访问,以使得只有恰当的方法才可以使用,因此能够访问到的LinkedList 的方法会变少。

11.11.1 PriorityQueue

先进先出描述了最典型的队列规则。队列规则是指在给定一组队列中的元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明 的事下一个元素应该等待时间最长的元素。

优先队列声明下一个弹出元素是最需要的元素(具有最高的优先级),例如在飞机场,当飞机临近起飞时,这架飞机的乘客可以在办理登机手续时排到队头。如果构建了一个消息系统,某些消息比其他消息更重要,因而应该更快得到处理,那么它们何时得到处理就与它们何时到达无关。

当你在 PriorityQueue 上调用 offer() 方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序,但是你可以使用 Comparator 来修改这个顺序。PriorityQueue 可以确保当你调用 peek() / poll() / remove() 方法时,获取的元素将是队列中优先级最高的元素。

让 PriorityQueue 与 Integer / String / Character 这样的内置类型一起工作易如反掌。在下面的示例中,第一个值集与前一个示例中的随机值相同,因此你可以看到它们从 PriorityQueue 中弹出的顺序与前一个示例不同:

11.12 Collection 和 Iterator

Collection 是描述所有序列容器的共性的根接口,它可能会被认为是一个 “附属接口”,即因为要表示其他若干个接口的共性而出现的接口。另外,java.util.AbstractCollection 类提供了 Collection 的默认实现,使得你可以创建 AbstractCollection 的子类型,而其中没有不必要的代码重复。

使用接口描述的一个重要理由是它能够创建更通用的代码。针对接口而非具体实现来编写代码,我们可以应用于更多的对象类型。因此,如果我编写的方法将接受一个 Collection, 那么该方法就可以应用于任何实现了 Collection 的类 —— 这也就使得一个新类可以选择去实现 Collection 接口,以便我的方法可以使用它。在标准 C++ 类库中并没有其容器的任何的公共基类——容器之间所有共性都是通过迭代器达成的。在 Java 中,也遵循使用迭代器来表达容器之间的共性。又因为容器又有公共的基类 Collection, 这就会实现 Collection 就意味着需要提供 iterator() 方法:

public class InterfaceVsIterator {
    public static void display(Iterator<String> it){
        while (it.hasNext()){
            String s = it.next();
            System.out.println("s: " + s);
        }
        System.out.println();
    }

    public static void display(Collection<String> it){
        for(String s: it){
            System.out.println("s: "+ s);
        }
        System.out.println();
    }

    public static void main(String[] args){
        List<String> list = new ArrayList<>(Arrays.asList("a a a b c d e f g".split(" ")));
        Set<String> set = new HashSet<>(list);
        Map<String, String> map = new LinkedHashMap<>();
        String[] names = "jack lucy coco mock fake buger".split(" ");
        for(int i = 0; i< names.length; i++){
            map.put(names[i], list.get(i));
        }
        display(list);
        display(set);
        display(list.iterator());
        display(set.iterator());
        System.out.println(map);
        System.out.println(map.keySet());
        display(map.values());
        display(map.values().iterator());
    }
}

两个版本的 display() 方法都可以使用 Map 或 Collection的子类型来工作,而且 Collection 接口 和 Iterator 都可以将 display() 方法与底层容器的特定实现解耦。

在本例中,这两种方式都可以奏效。事实上,Collection 要更方便一点,因为它是 Iterable 类型,因此,在 display(Collection)实现中,可以使用 foreach 结构,从而使代码更清晰。

当你要实现一个不是 Collection的外部类时,由于让它实现 Collection 接口可能非常困难或麻烦,因此使用 Iterator 会变得非常吸引人。例如,如果我们通过继承一个持有 Pet 对象的类型来创建一个 Collection 实现,那么我们就必须实现 Collection 所有的方法。实现所有方法,可以通过继承 AbstractCollection 而实现,但是无论如何还要被强制实现 iterator() 和 size(),以便提供 AbstractCollection 没有实现,但是 AbstractCollection 中其他方法会使用到的方法:

public class CollectionSequence extends AbstractCollection<String> {
    private String[] strings = {};

    public int size(){
        return strings.length;
    }

    public Iterator<String> iterator(){
        return new Iterator<String>() {

            private int index = 0;

            @Override
            public boolean hasNext() {
                return index < strings.length;
            }

            @Override
            public String next() {
                return strings[index++];
            }
        };

    }

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

从这个例子可以看出,如果你实现 Collection, 就必须实现 iterator(), 并且只拿实现 iterator() 与继承 AbstractCollection 相比,花费的代价只是略微减少。但是如果类已经继承其他的类,那么就不能再继承 AbstractCollection 了。在这种情况下,要实现 Collection, 就必须实现该接口中的所有方法。此时,继承并提供创建迭代器的能力就容易很多:

class PetSequence{
    protected String[] strings = "ada daw dw deff ed ffasd fe dad wdwe fffsa".split(" ");
}

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

            @Override
            public boolean hasNext() {
                return index < strings.length;
            }

            @Override
            public String next() {
                return strings[index++];
            }
        };
    }

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

生成 Iterator 是将队列与消费者队列的方法连接在一起耦合度最小的方式。

11.13 Foreach 与 迭代器

collection 可以使用 foreach 迭代器。

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

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

public class IterableClass implements Iterable<String> {
    protected String[] words = "and that is how we know the Earth to be bnana".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 static void main(String[] args){
        for(String s: new IterableClass()){
            System.out.println(s + " ");
        }
    }
}

iterator() 方法返回的是实现了 iterator<String> 的匿名内部类的实例,该匿名内部类可以遍历数组中的所有单词。在main() 中,可以看到 IterableClass 确实饿可以用于 foreach 语句。

11.13.1 适配器方法惯用法

现在有一个 Iterable 类,想添加一种或多种在 foreach 语句中使用这个类的方法。

解决方案,适配器方法惯用法。

11.14 总结

Java提供了大量持有对象的方式:

  1. 数组将数字与对象联系起来. 它保存类型明确的对象,查询对象时,不需要对结果做类型转化.它可以多维或一维.数组一旦生成,容量就不能改变.

  2. Collection 保存单一的元素,而Map 可以保存键值对. 有了Java的泛型, 就可以指定容器中存放的对象类型.容器可以自动调整尺寸,容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所持有的包装类型之间的双向转化.

  3. 像数组一样,List 也建立起了数字索引与对象之间的关联.不同的是,List 能够自动扩容

  4. 如果进行大量的访问,使用 ArrayList;如果经常从表中插入或删除元素,LinkedList

  5. 各种 Queue 以及栈的行为,由 LinkedList提供支持

  6. Map 是一种将对象与对象相关联的设计.HashMap 用于快速访问, TreeMap 保持键的排序,LinkedHashMap 保持元素的插入顺序

  7. Set 不接受重复的元素.HashSet 速度最快,TreeSet 保持元素处于排序状态,LinkedHashSet 保持插入顺序.

  8. Vector / Hashtable / Stack 已经过时了

  9. Java 容器的简图


    JavaCollection.png

容器只有四种:Map / List / Set / Queue

  • 点线框表示接口, 实线框表示具体的类.
  • 带有空心箭头的点线表示一个特点的类实现了一个接口
  • 实心箭头表示某个类可以生成箭头所指的类的对象. 例如,任意的 Collection 可以生成 Iterator,List 可以生成 ListIterator
    在线作图工具地址: https://www.processon.com/diagrams
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • 第十一章 持有对象 Java实用类库还提供了一套相当完整的容器类来解决这个问题,其中基本的类型是List、Set、...
    Lisy_阅读 799评论 0 1
  • 一、集合入门总结 集合框架: Java中的集合框架大类可分为Collection和Map;两者的区别: 1、Col...
    程序员欧阳阅读 11,556评论 2 61
  • 一、基础知识:1、JVM、JRE和JDK的区别:JVM(Java Virtual Machine):java虚拟机...
    杀小贼阅读 2,378评论 0 4
  • 某日的黄昏你我相依树下 拥抱着的是尸体,埋葬的是心 致命的毒药被藏在地上的苹果里 那是手中捧着的爱情 甜言蜜语变成...
    北山鬼阅读 201评论 0 0
  • 今天和室友一起搞起了大甩卖,就在这时我才突然意识到:原来我真的马上就要离开这片土地了,原来我的大学时光真的真的就要...
    阿黎Aria阅读 251评论 1 6