Java-持有对象
通常,程序总是根据运行是才知道的某些条件去创建新对象。在此之前,不会知道所需对象的数量,甚至不知道确切的类型。为了解决这个普通的编程问题, 需要在任意时刻和任意位置创建任意数量的对象。所以,就不能依靠创建命名的引用持有每一个对象:MyType aReference
,因此你不知道实际上会需要多少这样的引用
大多数语言都提供某种方法来解决这个基本问题。Java有多种方式保存对象(应该说是对象的引用)。例如数组,可以保存基本类型数据,但数组具有固定的尺寸。但大多数情况并不清楚需要多少个对象,因此数组尺寸固定这一限制显得过于受限了。
Java使用类库提供了容器解决这个问题。其中基本的类型是List、Set、Queue和Map。这些对象类型称为集合类。也称为容器。容器具有一些特性,如Set对于每个值都保存一个对象,Map是允许将某些对象与其他一些对象关联起来的关联数组,Java容器还可以自动调整自己的尺寸
1. 泛型和类型安全的容器
class Apple{
private static long counter;
private final long id = counter++;
public long id(){
return id;
}
}
class Orange{}
public class AppleAndOrangesWithoutGenerics {
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();
}
}
}
上面的例子中:Apple和Orange类是有区别的,它们除了都是Object之外没有任何共性(记住,如果一个类没有显式地声明继承自哪个类,那么它自动地继承自Object)。因此ArrayList保存的是Object,因为不仅可以通过ArrayList的add()方法将Apple对象放进这个容器,还可以添加Orange对象,而且编译期和运行期都没有问题。但当你用ArrayList的get()方法取出你认为的Apple对象时,得到的只是Object引用,必须将其转型为Apple,因此,需要将整个表达式扩起来,在调用Apple的id()方法之前,强制执行类型。否则,会得到语法错误。但运行时,当你试图将Orange对象转型为Apple时,会得到错误。
如果要想定义用来保存Apple对象的ArrayList,你可以声明ArrayList<Apple>,而不仅仅只是ArrayList,其中尖括号括起来的是类型参数(可以有多个),它指定了这个容器实例可以保存的类型。通过使用泛型,就可以在编译器防止将错误类型的对象放置到容器中。
class Apple{
private static long counter;
private final long id = counter++;
public long id(){
return id;
}
}
class Orange{}
public class ApplesAndOrangesWithGenerics {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
for(int i = 0; i < 3;i++){
apples.add(new Apple());
}
for(int i = 0; i < apples.size();i++){
System.out.println(apples.get(i).id());
}
for(Apple c : apples){
System.out.println(c.id);
}
}
}
现在,编译器可以组织你将Orange放置到apples中,因此它变成了一个编译期错误,而不再是运行时错误。
并且,在将元素从List取出时,类型转换也不再是必需的了。因为List知道它保存的是什么类型,因此它会在调用get()时替你执行转型。这样,通过使用泛型,你不仅知道编译器将会检查你放置到容器中的对象类型,而且在使用容器中的对象时,可以使用更加清晰的语法
上面的例子还说明,如果不需要使用每个元素的索引,可以使用foreach语法来选择List中的每个元素
当指定了某个类型作为泛型参数时,并不仅限于只能将该确切类型的对象放置到容器中,向上转型也可以像作用于其他类型一样作用于泛型。
class Apple{
private static long counter;
private final long id = counter++;
public long id(){
return id;
}
}
class GrannySmith extends Apple{}
class Gala extends Apple{}
class Fuji extends Apple{}
class Braeburn extends Apple{}
public class GenericsAndUpcasting {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
apples.add(new GrannySmith());
apples.add(new Gala());
apples.add(new Fuji());
apples.add(new Braeburn());
for(Apple c : apples){
System.out.println(c);
}
}
}
因此,可以将Apple的子类型添加到被指定保存Apple对象的容器中。
程序的输出是从Object默认的toString()方法产生的,该方法将打印类名,后面跟随者该对象的散列码的无符号十六进制表示(这个散列码是通过hashCode()方法产生的)
2. 基本概念
Java容器类类库的用途是“保存对象”,并将其划分两个不同的概念:
1)Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序(通常于它们被插入的顺序相同)
2)Map。一组成对的“键值对”对象,允许你使用键来查找值。ArrayList允许你使用数字来查找值,因此在某种意义上讲,它将数字与对象关联在了一起。Map允许我们使用另一个对象来查找某个对象
可以像下面这样创建一个list,通过使用接口的方式并在创建的时候指定精确类型,此时ArrayList已经被向上转型为List
List<Apple> apples = new ArrayList<>();
因为某些类具有额外的功能,例如,LinkedList具有在List接口中未包含的额外方法,而TreeMap也具有在Map接口中未包含的方法,如果你需要使用这些方法,就不能将它们向上转型为更通用的接口
Collection接口概括了序列的概念--一种存放一组对象的方式。下面的例子用Integer对象填充了一个Collection(用ArrayList表示),然后打印所产生的容器中的所有元素:
public class SimpleCollection {
public static void main(String[] args) {
Collection<Integer> c = new ArrayList<>();
for(int i = 0;i < 10;i++){
c.add(i);
}
for(Integer i:c){
System.out.println(i + ", ");
}
}
}
3. 添加一组元素
Arrays.asList()方法接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),并将其转换成为一个List对象。
Collection.addAll()方法接受一个Collection对象,以及一个数组或是一个用逗号分隔的列表,将元素添加到Collection中。
public class AddingGroups {
public static void main(String[] args) {
Collection<Integer> collection = new ArrayList<>(Arrays.asList(1,2,3,4,5));
Integer[] moreInts = {6,7,8,9,10};
collection.addAll(Arrays.asList(moreInts)); // Arrays.asList()方法接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),并将其转换成为一个List对象。
Collections.addAll(collection,11,12,13,14,15); //Collection.addAll()方法接受一个Collection对象,以及一个数组或是一个用逗号分隔的列表,将元素添加到Collection中。
Collections.addAll(collection,moreInts);
List<Integer> list = Arrays.asList(16,17,18,19,20);
list.set(1,99);
}
}
4. 容器的打印
必须使用Arrays.toString()来产生数组的可打印表示,但是打印容器不需要其他帮助
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<>()));
System.out.println(fill(new LinkedList<>()));
System.out.println(fill(new HashSet<>()));
System.out.println(fill(new TreeSet<>()));
System.out.println(fill(new LinkedHashSet<>()));
System.out.println(fill(new HashMap<>()));
System.out.println(fill(new TreeMap<>()));
System.out.println(fill(new LinkedHashMap<>()));
}
}
/** 输出结果:
[rat, cat, dog, dog]
[rat, cat, dog, dog]
[rat, cat, dog]
[cat, dog, rat]
[rat, cat, dog]
{rat=Fuzzy, cat=Rags, dog=Spot}
{cat=Rags, dog=Spot, rat=Fuzzy}
{rat=Fuzzy, cat=Rags, dog=Spot}
*/
查看输出结果会发现,默认的打印行为(使用容器的toString()方法)即可生成可读性很好的结果。Collection打印出来的内容用方括号括住,每个元素由逗号分隔。Map则用大括号括住,键与值由等号联系(键在等号左边,值在右边)
ArrayList和LinkedList都是List类型,从结果可以看出,它们都按照被插入的顺序保存元素。两者的不同之处在于执行某些类型的操作时的性能,而且LinkedList包含额操作也多于ArrayList。
HashSet、TreeSet、LinkedHashSet都是Set类型,输出显示Set中,每个相同的项只有保存一次,但是输出也显示了不同的Set实现存储元素的方式不同。HashSet使用的是相当复杂的方式存储元素。这种技术是最快的获取元素方式,因此,存储的顺序看起来并无实际意义(通常你只会关心某事物是够是某个Set的成员,而不关心它在Set出现的顺序)。如果存储顺序很重要,那么可以使用TreeSet,它按照比较结果的升序保存对象;LinkedHashSet按照被添加的顺序保存对象
Map可以使用键来查找对象,键所关联的对象称为值。对于每一个键,Map只接受存储一次。Map.put(key,value) 方法将增加一个值,并将它与某个键关联起来。Map.get(key) 方法将产生与这个键相关联的值。键和值在Map中的保存顺序并不是它们的插入顺序,因为HashMap实现使用的是一种非常块的算法来控制顺序;TreeMap按照比较结果的升序保存键;而LinkedHashMap则按照插入顺序保存键,同时还保留了HashMap的查询速度
5. List
List可以将元素维护在特定的序列中。List接口在Collection的接触上添加了大量的方法,使得可以在List的中间插入和移除元素
有两种类型的List:
1)基本的ArrayList,它擅长随机访问元素,但是List的中间插入和移除元素时比较慢
2)LinkedList,它通过代价较低的在List中间进行插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList更大
List常见的方法:
contains()方法来确定某个对象是否在列表中。
remove()方法移除一个对象
indexOf()发现对象在List中所处位置的索引编号
equals() 确定一个元素是否属于某个List
subList() 允许从较大的列表中创建处一个片段
containsAll() 判断一个列表是否在某个列表中
retainAll() 一种有效的交集操作
removeAll() 将从List中移除在参数List中的所有元素
addAll() 追加列表到末尾
6. 迭代器
迭代器是一个对象,它的工作是遍历并选择序列中的对象。此外迭代器通常被称为轻量级对象:创建它的代价小。Java的Iterator只能单向移动,这个Interator只能用来:
1)使用方法Interator()要求容器返回一个Interator。Interator将准备好返回序列的第一个元素
2)使用next()获得序列中的下一个元素
3)使用hastNext()检查序列中是否还有元素
4)使用remove()将迭代器新返回的元素删除
如果只是向前遍历List,并不打算修改List对象本身,那么foreach语法会显得更加简洁。
Interator还可以移除next()产生的最后一个元素,这意味着调用remove()之前必须先调用next()
ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。它还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个元素。可以通过调用ListIterator()方法产生一个指向List开始处的ListIterator,并且还可以通过调用ListIterator(n) 方法创建一个一开始就指向列表索引为n的元素出的ListIterator。
7. LinkedList
LinkedList在中间插入和移除时比ArrayList更高效,但在随机访问操作方面却要逊色一些,LinkedList还添加了可以使其用作栈、队列或双端队列的方法
getFirst() 返回列表的头(第一个元素),如果列表为空,则报异常
removeFirst() 移除并返回列表的头,而在列表为空时报异常
addFirst() 将某个元素插入到列表的头部
addLast() 将某个元素插入到列表的尾部
removeLast() 移除并返回列表的尾部,而在列表为空时报异常
8. Stack
栈通常是指 先进后出(LIFO)的容器,有时也被称为叠加栈,因为最后压入的元素,第一个弹出栈。
LinkedList具有能够直接实现栈所有功能的方法,因此可以直接将LinkedList作为栈使用,不过,有时一个真正的栈更能把事情讲清楚:
public class Stack<T> {
private LinkedList<T> storge = new LinkedList<>();
public void push(T v){
storge.addFirst(v);
}
public T peek(){
return storge.getFirst();
}
public T pop(){
return storge.removeFirst();
}
public boolean empty(){
return storge.isEmpty();
}
public String toString(){
return storge.toString();
}
}
Stack<T> 类名之后的<T>告诉编译器这将是一个参数化类型,而其中类型参数,即在类被使用时将会被实际类型替换的参数,就是T。大体上,这个类是在声明“我们在定义一个可以持有T类型对象stack”,stack是用LinkedList实现的,而LinkedList也被告知它持有T类型对象。注意,push()接受的是T类型的对象,而peek()和pop()将返回T类型的对象。peel()将提供栈顶元素,但是并不将其从栈顶移除,而pop()将移除并返回栈顶元素
注意,如果只需要栈的行为,使用继承就不合适了
使用刚刚创建stack类
public class StackCollision {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for(String s:"My dog has fleas".split(" ")){
stack.push(s);
}
while (!stack.empty()){
System.out.println(stack.pop() + " ");
}
}
}
9. Set
Set不保存重复的元素,Set具有与Collection完全一样的接口,因此没有任何额外的功能。实际上Set就是Collection,只是行为不同(这是继承与多态思想的典型应用:表现不同的行为)
public class SetOfInteger {
public static void main(String[] args) {
Random rand = new Random(47);
Set<Integer> intset = new HashSet<>();
for(int i = 0; i < 10000; i++){
intset.add(rand.nextInt(30));
}
System.out.println(intset);
}
}
在0-29之间的10000个随机数被添加到Set中,因此你可以想象,每一个数都重复了许多次。但是你可以看到,每一个数只有1个实例出现在结果中。还可以注意到的是,输出的顺序没有任何规律可循,这是由于处于速度原因的考虑,HashSet使用了散列,HashSet所维护的顺序与TreeSet或LinkedHashSet都不同,因为它们具有不同的元素存储方式。TreeSet将元素存储在红-黑树数据结构中,而HashSet使用了散列,LinkedHashSet因为查询速度也使用了散列,但是看起来它使用了链表来维护元素的插入顺序
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 I J K L".split(" "));
set1.add("M");
System.out.println("H: " + set1.contains("H"));
System.out.println("N: " + set1.contains("N"));
Set<String> set2 = new HashSet<>();
Collections.addAll(set2,"H I J K L".split(" "));
System.out.println("set2 in set 1 : " + set1.containsAll(set2));
set1.remove("H");
System.out.println("set1 : " + set1);
System.out.println("set2 in set 1 : " + set1.containsAll(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 set1 : " + set1);
}
}
10. Map
检查Java的Random类的随机性。键由Random产生的数字,而值是该数字出现的次数
public class Statistics {
public static void main(String[] args) {
Random rand = new Random(47);
Map<Integer,Integer> m = new HashMap<>();
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);
}
}
11. Queue
队列是一个典型的先进先出(FIFO)的容器,即从容器的一段放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同。队列常被当作一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中特别重要。因为它们可以安全地将对象从一个任务传输给另一个任务
LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现。通过将LinkedList向上转型为Queue。
public class QueueDemo {
public static void printQ(Queue queue){
while (queue.peek() != null){ // peek在不移除的情况下返回队头,在队列为空时候返回null
System.out.println(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)); // 将int转换为Integer对象
}
System.out.println(queue);
Queue<Character> qc = new LinkedList<>();
for(char c : "Baontosaurus".toCharArray()){
qc.offer(c); // 与Queue相关联的方法之一,允许的情况下,将一个元素插入到队尾,或者返回false
}
System.out.println(qc);
}
}
先进先出描述了最典型的队列规则。队列规则是指在给定一组队列的元素的情况下,确定下一个弹出队列的元素的规则。下一个元素应该是等待时间最长的元素。
优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。PriorityQueue添加到JavaSe5中,是为了提供这种行为的一种自动实现。当在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序,默认的排序将使用对象在队列中的自然顺序,但是你可以通过提供自己的Comparator来修改这个顺序。PriorityQueue可以确保当你调用peek()、poll()和remove()方法时,获取的元素将是队列中优先级最高的元素
public class PriorityQueueDemo {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
Random rand = new Random(47);
for(int i = 0; i < 10;i++){
priorityQueue.offer(rand.nextInt(i + 10));
}
QueueDemo.printQ(priorityQueue);
List<Integer> ints = Arrays.asList(25,22,20,18,14,9,3,1,1,2,3,9,14,18,21,23,25);
priorityQueue = new PriorityQueue<>(ints);
QueueDemo.printQ(priorityQueue);
priorityQueue = new PriorityQueue<>(ints.size(), Collections.reverseOrder());
priorityQueue.addAll(ints);
QueueDemo.printQ(priorityQueue);
String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION";
List<String> strings = Arrays.asList(fact.split(" "));
PriorityQueue<String> springPQ = new PriorityQueue<>(strings);
QueueDemo.printQ(springPQ);
springPQ = new PriorityQueue<>(strings.size(),Collections.reverseOrder());
springPQ.addAll(strings);
QueueDemo.printQ(springPQ);
Set<Character> charSet = new HashSet<>();
for(char c: fact.toCharArray()){
charSet.add(c);
}
PriorityQueue<Character> characterPQ = new PriorityQueue<>(charSet);
QueueDemo.printQ(characterPQ);
}
}
12.Foreach与迭代器
public class ForEachCollections {
public static void main(String[] args) {
Collection<String> cs = new LinkedHashSet<>();
Collections.addAll(cs,"Take the long way home".split(" "));
for(String s:cs){
System.out.println("'" + s + "'");
}
}
}
上面的代码中cs是一个Collection,所以这段代码展示了能够与foreach一起工作是所有Collection对象的特性。
之所以能够工作,是因为JavaSE5引入了新的被称为Iterable的接口,该接口包含一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。如果创建了任何实现Iterable的类,都可以将它用于foreach语句中
public class EnvironmentVariables {
public static void main(String[] args) {
for(Map.Entry entry:System.getenv().entrySet()){ // System.getenv()返回一个Map, entrySet()产生一个由Map.Entry的元素构成的Set
System.out.println(entry.getKey() + "; " + entry.getValue());
}
}
}
foreach 与数组
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(Arrays.asList(strings));
}
}