Java 有多种方式保存对象(应该说是对象的引用)。

  • 数组
  • 集合类也叫容器

数组是保存一组对象的最有效的方式。

泛型和类型安全的容器

ArrayList 当做“可以自动扩充自身尺寸的数组”。

原生 ArrayList 也就是没加泛型的,保存的是 Object,因此可以添加所有继承自 Object 的对象。
使用预定义泛型很简单,像是这样:

class Apple {}
public class AppleAnd {
	public static void main(String[] args){
		List<Apple> list = new ArrayList<>();
	}
}

尖括号里是类型参数,通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中。而且在取出 list 中的元素时,也不用类型转换。

容器中的元素除了支持 get(index) 的形式来获取某个元素,也可以用 foreach 语法来选择 List 中的每个元素。

当指定了某个类型作为泛型参数时,除了可以放确切的该类型对象,向上转型也可以像作用于其他类型一样作用于泛型。

基本概念

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

  1. Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List 必须按照插入的顺序保存元素,Set 不能有重复元素。 Queue 按照排队规则来确定对象产生的顺序。
  2. Map。 一组成对的“键值对”对象,允许使用键来查找值。 ArrayList 允许使用数字来查找值,因此在某种意义上讲,它将数字与对象关联在了一起。映射表允许我们使用另一个对象来查找某个对象,也被成为“关联数组”,或者成为“字典”。

一般来说,编写的代码都是与接口打交道,方便修改实现。

class A {
	List<Apple> apples = new ArrayList<>();
	//List<Apple> apples = new LinkedList<>();
}

当然,在 LinkedList 、TreeMap 中具有的而接口中不存在的方法,就不能通过转型来使用了。
Collection 接口概括了序列的概念——一种存放一组对象的方式。
add() 方法表明它是要将一个新元素放置到 Collection 中。
所有的 Collection 都可以使用 foreach 语法便利。

添加一组元素

在 java.util 包中的 Arrays 和 Collections 类中都有很多实用方法。
Arrays.asList() 接受一个数组或是用逗号分割的元素列表(使用可变参数),将其转换为一个 List 对象。
Collections.addAll() 接受一个 Collection 对象,以及一个数组或用逗号分割的列表,将元素添加到 Collection 中。
传统的 addAll() 接受一个 Collection。所有 Collection 类型都包含该方法

Collection 的构造器可以接受另一个 Collection,但是 Collection.addAll() 方法运行起来要快的多,而且使用后者更方便,所以是优选方案。

Collection.addAll() 成员方法只能接受另一个 Collection 对象作为参数,没有上文讲到的两种方法更灵活。

Arrays.asList() 方法的限制是它对所产生的 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 AsListTnference {
	public static void main(String[] args) {
		List<Snow> snow1 = Arrays.asList(
				new Crusty(), new Slush(),new Powder());
		//Won't compile:
		//List<Snow> snow2 = Arrays.<Snow>asList(
		//		new Light(), new Heavy());
		List<Snow> snow3 = new ArrayList<>();
		Collections.addAll(snow3, new Light(), new Heavy());
		List<Snow> snow4 = Arrays.<Snow>asList(
			new Light(), new Heavy());
	}
}

当试图创建 snow2 时,Arrays.asList() 中只有 powder 类型,因此它会创建 List<Powder>而不是List<Snow>
Collections.addAll() 工作良好,因为它从第一个参数中了解到了目标类型是什么。
也可以像 snow4 那样,以告诉编译器对于 Arrays.asList() 你要产生的 List 类型,这称为 显示类型参数说明。

容器的打印

对于数组来说,只能使用 Arrays.toString(),多维用 Arrays.deepToString()。
而容器无需任何帮助。
Collection 在每个槽中保存一个元素。此类容器包括

  • List ,以特定的顺序保存一组元素;
  • Set , 元素不能重复;
  • Queue, 只允许在容器的一“断”插入对象,并从另一“端”移除对象。

ArrayList 和 LinkedList 都按照插入的顺序保存元素。
HashSet 、 TreeSet 和 LinkedHashSet 都是 Set 类型,每个相同的项只保存一次。

  1. HashSet 是最快的获取元素方式。
  2. TreeSet 按照比较结果的升序保存对象。
  3. LinkedHashSet 按照添加的顺序保存对象。

Map 在每个槽内保存了两个对象。可以用键来查找对象,所以,对于每一个键,Map 只接受存储一次。
Map.put(K,V) Map.get(K) 用来添加和获取。

  1. HashMap 也提供了最快的查找技术
  2. TreeMap 按照比较结果的升序保存键
  3. LinkedHashMap 按照插入顺序保存键,同时还保留了 HashMap 的查询速度。

List

有两种类型的 List

  • ArrayList 长于随机访问元素,但是在 List 中间插入移除元素较慢
  • LinkedList 通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList 在随机访问方面相对较慢,但是它的特性集比 ArrayList 更大。

当确定一个元素是否属于某个 List ,发现某个元素的索引,以及从某个 List 中移除一个元素时,都会用到 equals() 方法。因此,必须要意识到 List 的行为根据 equals() 的行为而有所变化。

subList() 方法允许从大的列表创建出一个片段。containsAll() 不会因为顺序不同而产生不同的行为。
retainAll() 方法是一种有效的交集行为。所产生的行为依赖于 equals() 。
removeAll() 移除所有元素。
set() ,在指定的索引出,用第二个参数替换整个位置的元素。
对于 List ,有一个重载的 addAll() 方法可以在初始 List 的中间插入新的列表,而不仅仅是前文提到的两种方法追加到表尾。
toArray() 方法,将任意的 Collection 转换为一个数组。无参版本返回 Object 数组。如果传递目标类型的数据,在它能通过类型检查的情况下,它将产生指定数据类型的数据。如果参数数组太小,toArray() 方法将创建一个具有合适尺寸的数组。

迭代器

迭代器是一个对象,它的的工作是遍历并选择序列中的对象。此外,迭代器通常被成为轻量级对象:创建它的代价小,因此,经常可以见到迭代器有些奇怪的限制;
例如,Java 的 Iterator 只能单向移动,这个 Iterator 只能用来:

  1. 使用方法 iterator() 要求容器返回一个 Iterator。Iterator 将准备好返回序列的第一个元素。
  2. 使用 next() 获得序列中的下一个元素。
  3. 使用 hasNext() 检查序列中是否还有元素。
  4. 使用 remove() 将迭代器新近返回的元素删除。

如果只向前遍历 List ,使用 foreach 语法更加简介。
另外 Iterator 还可以移除由 next() 产生的最后一个元素,这意味着在调用 remove() 之前必须先调用 next()。

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

display() 方法不包含任何有关它所遍历的序列的类型信息,它将能够遍历序列的操作与序列底层的结构分离。

ListIterator

ListIterator 是一个更加强大的 Iterator 的子类型,它只能用于各种 List 类的访问。Iterator 只能单向移动,而 ListIterator 可以双向移动。它还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引,并且可以使用 set() 方法替换它访问过的最后一个元素。可以通过 ListIterator(n) 方法创建一个一开始就指向列表索引为 n 的元素处的 ListIterator 。

LinkedList

实现了 List 接口,一些优缺点之前已经提到过。
除此之外,LinkedList 还添加了可以使其用作栈、队列和双端队列的方法。这些方法有些彼此只是名称有些差异:

Stack

后进先出的容器。有时也被称为 叠加栈,最后压入栈的元素,第一个弹出栈。
LinkedList 具有能够直接实现栈的所有功能的方法,因此可以直接将 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(); }

Set

Set 不保存重复元素。Set 中最常被使用的是测试归属性,可以很容易地访问某个对象是否在某个 Set 中,正因如此,查找成了 Set 中最重要的操作,所以通常都会选择一个 HashSet 实现,它专门对快速查找进行了优化。
Set “ is-a " Collection 也就是说 Set 具有 Collection 完全一样的接口,没有额外个功能。

  1. TreeMapSet 将元素存储在红-黑数数据结构中,
  2. HashSet 使用的是散列函数
  3. LinkedHashSet 也使用了散列,但是看起来使用了链表来维护元素的插入顺序

一般用 containsAll() 来测试归属性。

Map

将对象映射到其他对象的能力是一种解决编程问题的杀手锏。
如下一个程序统计 Random 产生数字的分布情况。

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

例如 Map<Person,List<Pet>>
Map 可以返回它的键的 Set ,它的值的 Collection,或者它的键值对的 Set。
KeySet() 方法产生所有键组成的 Set,它在 foreach 语句中被用来遍历该 Map。

Queue

典型的先进先出容器。队列在并发编程中特别重要。
LinkedList 提供了方法以支持队列的行为,并且它实现了 Queue 接口,因此 LinkedList 可以用作 Queue 的一种实现。
offer() 方法是 Queue 相关的方法之一,它在允许的情况下,将一个元素插入到队尾,或者返回 false。
peek() 和 element() 都在不移除的情况下返回对头,但是 peek() 在队列为空,返回 null,element() 会跑出异常。poll() 和 remove() 移除并返回对头,poll() 在队列为空返回 null ,remove() 抛出异常。

PriorityQueue

优先级队列声明下一个弹出的元素是最需要的元素(具有最高的优先级)。
当调用 offer() 方法来插入一个对象时,这个对象会在队列中排序。默认的排序将使用对象在队列中的自然顺序,但是可以通过提供自己的 Comparator 来修改这个顺序。PriorityQueue 可以确保当你调用 peek() 、poll()和remove() 方法时,获取的元素将是队列中优先级最高的元素。

Collection 和 Iterator

Collection 是描述所有序列容器的共性的根接口,他可能会被认为是一个附属接口,因为要表示其他若干个接口的共性而出现的接口。另外,java.util.AbstractCollection 类提供了 Collection 的默认实现,方便你可以创建 AbstractCollection 的子类型,而其中没有不必要的代码重复。
使用接口方便应用于更多的对象类型。如果编写的方法接受一个 Collection ,那么该方法就可以用于所有实现了 Collection 的类。
在Java中,Collection接口这种方式和迭代器这种方式绑定到了一起,意味着实现 Collection 就意味着需要提供 iterator() 方法
Collection 接口和 Iterator 都可以将display() 方法与底层容器的特定类型实现解耦。
事实上,Collection 要更方便一些,因为它是 Iterable 类型,所以可以使用 foreach 结构。
当要实现 Collection 的外部类时,由于它去实现 Collection 接口可能非常困难,因此使用 Iterator 就会变得简单,虽然可以通过继承 AbstractCollection 来实现,但是无论如何还是要实现 iterator() 和 size()。
所以在已经继承某个类之后,只能通过实现 Collection 的情况下,会非常吃力。此时,继承并提供创建迭代器的能力就会容易的多了。

public class CollectionSequence extends AbstractCollection<A> {
	private A[] as = new A[] {
			new A("dog"), new A("cat"), new A("bird"),new A("fish")
	};

	@Override
	public Iterator<A> iterator() {
		return new Iterator<A>() {
			private int index = 0;
			@Override
			public boolean hasNext() {
				return index < as.length;
			}

			@Override
			public A next() {
				return as[index++];
			}
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	@Override
	public int size() {
		return as.length;
	}

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

下面是提供创建迭代器的方式:

Class PetSequence {
	protected Pet[] pets = Pets.createArray(8);
}
public class NonCollectionSequence extends PetSequence {
	public Iterator<Pet> iterator() {
		return new Iterator<Pet>() {
			private int index =0;
			public boolean hasNext() {
				return index < pet.length;
			}
			public Pet next() { return pets[index++];}
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}
}

Foreach 与迭代器

foreach 语法主要用于数组,但是也可以应用于任何 Collection 对象。
之所以能够工作,是因为 Java SE5 引入了新的被称为 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 banan-shaped.").split(" ");

	@Override
	public Iterator<String> iterator() {

		return new Iterator<String>() {
			private int index = 0;
			@Override
			public boolean hasNext() {
				return index < words.length;
			}

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

		};
	}

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

}

大量的类都是 Iterable 类型,主要包括所有的 Collection 类,但是不包括各种 Map。
尝试把数组当做一个 Iterable 参数传递会导致失败,说明不存在任何从数组到 Iterable 的自动转换,必须手动执行这种转换

public class ArrayIsNotIterable {
	static <T> void test(Iterable<T> ib) {
		for(T t : ib)
			System.out.println(t + " ");
	}
	public static void main(String[] args) {
		test(Arrays.asList(1, 2, 3));
		String[] strings = {"A", "B", "C"};
		//test(strings);
		test(Arrays.asList(strings));
	}
}

适配器方法惯用法

如果现有一个 Iterable 类,如果想要添加一种或多种在 foreach 语句中使用这个类的方法,直接继承并覆盖 iterator() 方法,显然不是一种好的方式。
一种解决方案是所谓 适配器方法的惯用法。提供特定的接口以满足 foreach 语句。当有一个接口并需要另一个接口时,编写适配器就可以解决问题。

package cn.iunote.learn02.chapter11.test29;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import cn.iunote.learn02.chapter11.test26.IterableClass;

public class MultiIterableClass extends IterableClass {
	public Iterable<String> reversed() {
		return new Iterable<String>() {

			@Override
			public Iterator<String> iterator() {
				return new Iterator<String>() {
					int current = words.length - 1;
					@Override
					public boolean hasNext() {
						return current > -1;
					}

					@Override
					public String next() {
						return words[current--];
					}
					public void remove() {
						throw new UnsupportedOperationException();
					}

				};
			}

		};
	}

	public Iterable<String> randomized(){
		return new Iterable<String>() {

			@Override
			public Iterator<String> iterator() {
				List<String> shuffled = new ArrayList<String>(Arrays.asList(words));
				Collections.shuffle(shuffled, new Random(47));
				return shuffled.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 + " ");
	}
}

如果直接将 ral 对象置于 foreach 语句中,将得到默认的前向迭代器。但是如果在该对象上调用 reversed() 方法,就会产生不同的行为。
如果用ArrayList 将 Arrays.asList() 方法的结果包装起来,就不会不该数组的顺序,如果直接用 Arrays.asList() 方法的结果,那么就会影响底层的数组。

总结

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

  1. 数组将数字与对象联系起来。它保存类型明确的对象,查询对象时,不需要对结果做类型转换。可以是多维的,可以保存基本类型的数据。但是,数组一旦生成,其容量就不能改变。
  2. Collection 保存单一的元素,而 Map 保存相关联的键值对。有了泛型,就可以指定容器中存放的对象类型,因此就不会将错误类型的对象放置到容器中,并且在从容器中获取元素时,不必进行类型转换。他们都能自动调整尺寸。容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所持有的包装器类型之间的双向转换。
  3. 像数组一样,List 也建立数字索引与对象的关联,因此,数组和 List 都是排好序的容器。List能够自动扩充容量。
  4. 如果要进行大量的随机访问,就是用 ArrayList ;如果要经常从表中间插入或删除元素,则应该使用 LinkedList。
  5. 各种 Queue 以及栈的行为,由 LinkedList 提供支持。
  6. Map 是一种对象与对象想关联的涉及。HashMap 设计用来快速访问;TreeMap 保持“键”始终处于排序状态,所以没有 HashMap 快。LinkedHashMap 保持元素插入的顺序,但是也通过散列提供了快速访问的能力。
  7. Set 不接受重复元素。HashSet 提供最快的查询速度,而 TreeSet 保持元素处于排序状态。LinkedHashSet 以插入顺序保存元素。
  8. 新程序中不应该使用过时的 Vector、HashTable 和 Stack 。
10-05 12:16