<Java编程思想>持有对象(笔记)

本文深入探讨Java泛型的重要性及容器的基本概念,包括List、Set、Map等接口及其实现类的特点与用法,同时讲解了如何利用泛型增强代码安全性。

持有对象,我们可以简单地理解为C++的数据结构,也可以简单地理解为数据的容器。比如List/Map/Set。

1、论泛型的重要性。

课本的简单例子是:

import java.util.ArrayList;

class Apple{}
class Orange{}
public class Generics {
    @SuppressWarnings("unchecked")
	public static void main(String[] args){
    	@SuppressWarnings("rawtypes")
		ArrayList apples = new ArrayList();
    	for(int i = 0; i < 3; i++)
    		apples.add(new Apple());
    	for(int i = 0; i < 3; i++)
    		apples.add(new Orange());
    	for(Object obj : apples)
    		System.out.println(obj);
    }
}
尽管Eclipse一再提醒错误,但是强制执行,并且加上@SuppressWarning之后,运行结果如下:

Apple@166afb3
Apple@9945ce
Apple@b5dac4
Orange@12d96f2
Orange@110003
Orange@17e4ca

这说明了什么呢:使用Java SE5之前的容器的一个主要的问题就是编译器允许你向容器中插入不正确的类型。

在上面的例子,由于没有加入泛型,使得ArrayList这个容器保存的是Object对象。所以在ArrayList里面我们可以插入原本不应该插入的Orange对象,且如果要把这个Orange对象当做Apple对象来使用,必须使用强制转型,不然会得到语法错误。但是转型之后会得到异常。这是不安全的做法。

加入泛型机制:很简单就是使用语法:ArrayList<Apple>

通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中。在编译期防止的意思是,如果我们在代码中使用了Orange对象并打算将其放到ArrayList<Apple>容器中,这个时候会得到一个错误。(其实由于我们现在使用的是Java 7版本,所以在上面错误的例子我们就可以看到编译器对容器泛型的强制要求了);通过使用泛型还有另外一个好处那就是我们不需要对取出来的对象进行类型的转换了,因为取出来的就是我们原先默认可以放进去的类的实例。

2、Java容器的基本概念

Collection:是一个独立元素的序列,这些独立的元素都服从一条或多条规则,List必须按照插入的顺序保存元素,而Set不能有重复的严肃,Queue按照排队规则来确定对象产生的顺序(这通常与它们被插入的顺序相同)

Map:一组成对的”键值对“对象,允许你使用键来查找值。它也被称为关联数组,或者被称为字典。

Collection下的接口有List和Set,List和Set又各自有自己的实现类,Map接口也有自己的几种实现。我们常常可以看到下面两种不同的构造方法:

List<Apple> apples = new ArrayList<Apple>();//注意List是可变长的。

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

在理想的情况下,我们编写的大部分代码都是在与这些接口打交道,并且我们唯一需要指定所使用的精确类型的容器的地方就是在创建的时候(new ArrayList<Apple>)。我们用这样子的构造方法,在创建一个具体的类的对象的时候将其转型为对应的接口(注意不是转型为其基类),然后在其余的代码中都使用这个接口。但是因为有些类具有额外的功能,比如LinkedList具有在List接口中未包含的额外的方法,TreeMap也具有在Map接口中未包含的方法,如果我们需要用到这些额外的方法,那就不能将它们向上转型为接口。

此外ArrayList还可以向上转型为更一般更通用的接口Collection(注意Collection是接口不是一个类)

比如下面的例子:

import java.util.ArrayList;
import java.util.Collection;
public class SimpleCollection {
   public static void main(String[] args){
	   Collection<Integer> c = new ArrayList<Integer>();
	   for(int i = 0; i < 10; i++)
		   c.add(i);
	   for(Integer i : c)
		   System.out.print(i+ " ");
   }
}
运行结果:0 1 2 3 4 5 6 7 8 9 

当然:Set<Integer> c =  new ArrayList<Integer>();这样子的写法是错误的。

在上面的例子中,我们使用了foreach的语法,所有的Collection都可以使用foreach语法进行遍历。

3、添加一组元素

直接入例子:

//import java.awt.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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};//注意Arrays.asList是中括号而这里是大括号
    	collection.addAll(Arrays.asList(moreInts));
    	//collection.addAll(moreInts);//注意这里报错,因为元素是一个数组而不是一个List类型
    	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);
    	for(Integer i : collection)
    		System.out.print(i + " ");
    	System.out.println();
    	for(Integer i : list)
    		System.out.print(i + " ");
    }
}
运行的结果:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 6 7 8 9 10 
16 99 18 19 20 

上面的例子需要掌握的是Arrays.asList还有Collection的addAll(注意与add增加一个单一元素的区别)。此外还需要对比的是Collection对象的addAll方法与Collections的addAll方法的区别。

总的来说,上面的例子想说明的是一个Collection对象可以完成创建的三种方式。

一种是使用Collecion的构造函数,参数为另一个实现Collection的类的对象(书本的翻译有点问题,说的是Collection作为参数)。比如拿Arrays.asList()返回的List作为构造函数的参数,注意List也是Collection的一种

另一种方式就是:使用Collection对象的addAll方法,参数也可以是一个已经实现了的Collection实现类的实例。

最后一种方法就是使用Collections.addAll()方法。注意参数与collection.addAll()方法不一样了。

Arrays.asList方法与Collections.addAll方法比collection.addAll方法来的灵活,因为它们使用的都是可变参数列表。体现在参数的个数上面。

4、容器的打印

1)数组的打印,如果我们没打算用循环来实现数组的打印,那一般我们必须使用Arrays.toString()方法来产生数组的可打印表示。

2)打印容器无需任何帮助,可以直接作为IO输出函数的参数。

import java.util.*;
public class Printing {
   static Collection<String> fill(Collection<String> collection){
	   collection.add("cat");
	   collection.add("dog");
	   collection.add("oxx");
	   collection.add("dog");
	   collection.add("hen");
	   return collection;
   }
   static Map<String,String> fill(Map<String,String> map){
	   map.put("dog", "Lucy");
	   map.put("cat", "Bosco");
	   map.put("dog", "Lucy2");
	   map.put("hen", "Spot");
	   return map;
   }
   public static void main(String[] args){
	   System.out.println(fill(new ArrayList<String>()));//是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>()));
   }
}
有意思的结果如下:

[cat, dog, oxx, dog, hen]
[cat, dog, oxx, dog, hen]
[oxx, cat, dog, hen]
[cat, dog, hen, oxx]
[cat, dog, oxx, hen]
{cat=Bosco, dog=Lucy2, hen=Spot}
{cat=Bosco, dog=Lucy2, hen=Spot}
{dog=Lucy2, cat=Bosco, hen=Spot}

通过上面的结果,我们至少可以发现以下的规律:1)容器Collection和Map可以直接打印;2)Set和Map的不同实现类有不同的存储方式;3)Map和Set的可打印版本表达方式不同;4)Map和Set不允许重复值(Map允许重复键值,不允许重复键)……

此外,Collection还有一个接口,是Queue,还有其他的,参考Java API文档。

我们知道如果不自己重写toString()方法,那么会调用Object或者是该类父类的toString()方法,这也说明了,Collection和Map的实现类都已经实现了自己的toString()方法。

下面对各个接口的近亲实现类进行一次比较。

List:

1)ArrayList:以数组实现的表

2)LinkedList:以链表实现的表。包含的操作多于ArrayList(参考Java API文档)

Set:

1)HashSet:顾名思义就是以哈希的方式存储元素的,最快的获取元素的方式就是哈希存储。所以这是三种Set中获取元素速度最快的。

2)TreeSet:如果存储顺序很重要,可以使用TreeSet,它按照结果的升序保存对象。

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

Map:

参考Set

5、List常用的方法:

contains()方法:remove()方法:顾名思义,这两种方法都会用到equals()方法。(为什么呢、)subList()方法允许我们很容易地从较大的列表中创建出一个片段;containsAll()则是判断是否含有某个子序列(其实Java的命名很有规则,containsAll与addAll……不过在人称上面有点混乱)

课本为了说明containsAll()方法与顺序无关,使用了Collections.sort()方法对原表进行排序,说获得的子序列,包含的结果依然为true。我觉得这是不严谨的,因为获得的子序列会根据原表的顺序变化而变化。我 自己使用的证明很简单:

System.out.println(fill(new ArrayList<String>()).containsAll(Arrays.asList("dog","cat")));
System.out.println(fill(new ArrayList<String>()).containsAll(Arrays.asList("cat","dog")));
上面两个语句在运行的时候都输出true这就证明了containsAll()方法与参数(子表)的顺序无关。

retainAll()方法是一种有效的“交集”操作,交集,不一定是连续的子表。

实例如下:

Collection<String> ls1 = fill(new ArrayList<String>(Arrays.asList("dog")));
	   System.out.println(ls1);//是ArrayList<String>()不要忘记()
	   Collection<String> ls2 = fill(new LinkedList<String>(Arrays.asList("cat","dog")));
	   System.out.println(ls2);
	   ls1.retainAll(ls2);
	   System.out.println("retainAll " + ls1);
输出:

[dog, cat, dog, oxx, dog, hen]
[cat, dog, cat, dog, oxx, dog, hen]
retainAll [dog, cat, dog, oxx, dog, hen]

而如果是:

Collection<String> ls1 = fill(new ArrayList<String>(Arrays.asList("cat")));
	   System.out.println(ls1);//是ArrayList<String>()不要忘记()
	   Collection<String> ls2 = fill(new LinkedList<String>(Arrays.asList("cat","dog")));
	   System.out.println(ls2);
	   ls1.retainAll(ls2);
	   System.out.println("retainAll " + ls1);
结果是:

[cat, cat, dog, oxx, dog, hen]
[cat, dog, cat, dog, oxx, dog, hen]
retainAll [cat, cat, dog, oxx, dog, hen]

可见连续与不连续并不是妨碍的关键。retailAll()方法返回的是boolean值,而且如果ls1.retainAll(ls2),则ls1变为他们的交集。反之也是,ls2也会变成它们的交集。这是奇怪的设计理念。且由于交集内可以存在重复的元素,如果是ls1去retainAll(ls2)则返回那些存在与ls2的ls1的元素集合,而反过来则是返回存在与ls1的ls2集合。所以这两种写法不等价!(这与文氏图不同的是,文氏图是Set,是没有重复的元素的)

我的意思是,如果ls1完全存在与ls2,那么返回的交集其实是ls2本身减去那些不存在ls1的元素,由于重复的可能性,返回的交集可能比ls1还长。

其他的方法诸如:removeAll 移除所有元素;set()表示将某个元素设置为另一个元素;addAll()方法可以接受两个参数,一个是index一个是新表,可以在表的中间插入新的列表,这比Collection的addAll()方法来得灵活,或者只能是插入到最后面的位置。(注意1:All表示操作对象是一个列表;注意2:参考collection.addAll()方法与Collections.addAll()方法与Arrays.asList()方法的区别)

isEmpty()方法,clear()方法,顾名思义。toArray()方法则是将Collection对象转为一个数组。

6、迭代器

1)使用方法iterator()要求容器返回一个Iterator.Iterator将准备好返回序列的第一个元素

2)使用next()获得序列中的下一个元素

3)使用hasNext()检查序列中是否还有下一个元素

4)使用remove()将迭代器新近返回的元素删除(是否会影响原序列的结构,我们来测试一下)

import java.util.*;
public class SimpleIterator {
   public static void main(String[] args){
	   List<Integer> ints = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9));
	   Iterator<Integer> it = ints.iterator();
	   System.out.println(it);
	   System.out.println(it.next());
	   while(it.hasNext()){
		   Integer i = it.next();
		   System.out.print(i + " ");
	   }
	   System.out.println();
	   it = ints.iterator();//重返头部
	   for(int i = 0; i < 5; i++)
		   //it = it.next();这是错误的表示方法
		   it.next();
	   it.remove();
	   it = ints.iterator();
	   while(it.hasNext()){
		   Integer i = it.next();
		   System.out.print(i + " ");
	   }
   }
}
输出结果:

java.util.ArrayList$Itr@6bade9
1
2 3 4 5 6 7 8 9 
1 2 3 4 6 7 8 9 

在调用next()之前使用:it.remove();会得到下面的异常:

Exception in thread "main" java.lang.IllegalStateException
at java.util.ArrayList$Itr.remove(ArrayList.java:804)
at SimpleIterator.main(SimpleIterator.java:6)

书上的原文是:Iterator还可以移除由next()产生的最后一个元素,这意味着在调用remove()之前必须先调用next()。

课本还举了一个这样子的例子,说是有很多不同的容器(当然泛型是同样的类型),有一个display方法,我们需要这个方法可以忽略容器的类型而进行相同标准的打印。当然我们可以把容器所共有的(或者部分容器所共有的)接口作为参数(可以吗,比如Collection或者List有Iterator方法吗,Java API文档帮我们证实了,有)。

或者我们可以把Iterator<Type>作为display()方法的参数,而在main方法中就只需把各种容器的iterator作为参数传递进方法就可以了。


ListIterator就是双向的Iterator,hasPrevious()和hasNext()相似,同理ListIterator.previous()与next()相似。

7、LinkedList

1)getFirst() & element()完全一样  它们都返回列表的第一个元素,当列表为空,则抛NoSuchElement异常;peek()则是返回null

2)removeFirst() & remove()完全一样,移除并返回列表的头,而在列表为空的时候抛出NoSuchElement异常;poll则返回null  注意是头部不是尾部。

3)addFirst add addLast

4)removeLast

8、Stack忽略

9、Set忽略

10、Map

Map与数组和其他的Collection一样、可以很容易地拓展到多维,而我们只需要将其值设置为Map(这些Map的值可以是其他的容器,甚至是其他的Map)。比如Map<String,List<String>> 。

11、Queue

LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现,通过将LinkedList向上转型为Queue(转为接口也叫做向上转型,屏蔽了某些下层的方法,只保留该接口层面的方法)。比如

Queue<Integer> queue = new LinkedList<Integer>();

offer()方法是与Queue相关的方法之一,它在允许的情况下将一个元素插入到队尾或者返回false。

peek() element()都是在不移除的情况下返回对头。不过peek()方法在队列为空的时候返回null而element()方法则抛出NoSuchElement异常。同理还有poll() remove()方法,作用是移除并返回队头(注意Queue的操作在头部)。(P开头的比较安全,不容易抛出异常)

优先队列:

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

需要注意的是:PriorityQueue与Queue的一点不同是:

Queue<Integer> queue = new LinkedList<Integer>();或者是其他的实现类,因为Queue是个接口,而

PriorityQueue<Integer> pqueue = new PriorityQueue<Integer>();


下面的例子是一个Comparator来修改PriorityQueue的排序规则:参考自http://blog.youkuaiyun.com/hiphopmattshi/article/details/7334487

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

public class test {
	private String name;
	private int population;
	public test(String name, int population)
	{
		this.name = name;
	    this.population = population;
	}
	public String getName()
	{
	     return this.name;
	}

	public int getPopulation()
	{
	     return this.population;
	}
	public String toString()
	{
	     return getName() + " - " + getPopulation();
	}
	public static void main(String args[])
	{
		Comparator<test> OrderIsdn =  new Comparator<test>(){//是个接口
			public int compare(test o1, test o2) {
				// TODO Auto-generated method stub
				int numbera = o1.getPopulation();
				int numberb = o2.getPopulation();
				if(numberb > numbera)
				{
					return 1;
				}
				else if(numberb<numbera)
				{
					return -1;
				}
				else
				{
					return 0;
				}
			
			}

			
			
		};
		Queue<test> priorityQueue =  new PriorityQueue<test>(11,OrderIsdn);
		
			    
			
		test t1 = new test("t1",1);
		test t3 = new test("t3",3);
		test t2 = new test("t2",2);
		test t4 = new test("t4",0);
		priorityQueue.add(t1);
		priorityQueue.add(t3);
		priorityQueue.add(t2);
		priorityQueue.add(t4);
		System.out.println(priorityQueue.poll().toString());
	}
}
PriorityQueue(int initialCapacity, Comparator<? super E> comparator) 
          Creates a PriorityQueue with the specified initial capacity that orders its elements according to the specified comparator.(这是Java API文档的介绍)还有其他的构造方法。

Comparator是个接口。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值