今天在网上搜索了一下,发现一篇关于java集合的博文,里面整理得非常好, 特意copy过来和大家分享一下
本讲内容:集合 collection
讲集合collection之前,我们先分清三个概念:
SCJP考试要求了解的接口有:Collection , Set , SortedSet , List , Map , SortedMap , Queue , NavigableSet , NavigableMap, 还有一个 Iterator 接口也是必须了解的。
SCJP考试要求了解的类有: HashMap , Hashtable ,TreeMap , LinkedHashMap , HashSet , LinkedHashSet ,TreeSet , ArrayList , Vector , LinkedList , PriorityQueuee , Collections , Arrays
下面给出一个集合之间的关系图:
上图中加粗线的ArrayList 和 HashMap 是我们重点讲解的对象。下面这张图看起来层级结构更清晰些。
我们这里说的集合指的是小写的collection,集合有4种基本形式,其中前三种的父接口是Collection。
一、Collection 接口
Collection接口是 Set 、List 和 Queue 接口的父接口,提供了多数集合常用的方法声明,包括 add()、remove()、contains() 、size() 、iterator() 等。
add(E e) | 将指定对象添加到集合中 |
remove(Object o) | 将指定的对象从集合中移除,移除成功返回true,不成功返回false |
contains(Object o) | 查看该集合中是否包含指定的对象,包含返回true,不包含返回flase |
size() | 返回集合中存放的对象的个数。返回值为int |
clear() | 移除该集合中的所有对象,清空该集合。 |
iterator() | 返回一个包含所有对象的iterator对象,用来循环遍历 |
toArray() | 返回一个包含所有对象的数组,类型是Object |
toArray(T[] t) | 返回一个包含所有对象的指定类型的数组 |
我们在这里只举一个把集合转成数组的例子,因为Collection本身是个接口所以,我们用它的实现类ArrayList做这个例子:
01 |
import java.util.ArrayList; |
02 |
import java.util.Collection; |
03 |
04 |
public class CollectionTest { |
05 |
06 |
public static void main(String[] args) { |
07 |
08 |
String a = "a" ,b= "b" ,c= "c" ; |
09 |
Collection list = new ArrayList(); |
10 |
list.add(a); |
11 |
list.add(b); |
12 |
list.add(c); |
13 |
14 |
String[] array = list.toArray( new String[ 1 ]); |
15 |
16 |
for (String s : array){ |
17 |
System.out.println(s); |
18 |
} |
19 |
} |
20 |
} |
编译并运行程序,检查结果:
二、几个比较重要的接口和类简介
1、List接口
List 关心的是索引,与其他集合相比,List特有的就是和索引相关的一些方法:get(int index) 、 add(int index,Object o) 、 indexOf(Object o) 。
ArrayList 可以将它理解成一个可增长的数组,它提供快速迭代和快速随机访问的能力。
LinkedList 中的元素之间是双链接的,当需要快速插入和删除时LinkedList成为List中的不二选择。
Vector 是ArrayList的线程安全版本,性能比ArrayList要低,现在已经很少使用
2、Set接口
Set关心唯一性,它不允许重复。
HashSet 当不希望集合中有重复值,并且不关心元素之间的顺序时可以使用此类。
LinkedHashset 当不希望集合中有重复值,并且希望按照元素的插入顺序进行迭代遍历时可采用此类。
TreeSet 当不希望集合中有重复值,并且希望按照元素的自然顺序进行排序时可以采用此类。(自然顺序意思是某种和插入顺序无关,而是和元素本身的内容和特质有关的排序方式,譬如“abc”排在“abd”前面。)
3、Queue接口
Queue用于保存将要执行的任务列表。
LinkedList 同样实现了Queue接口,可以实现先进先出的队列。
PriorityQueue 用来创建自然排序的优先级队列。番外篇中有个例子http://android.yaohuiji.com/archives/3454你可以看一下。
4、Map接口
Map关心的是唯一的标识符。他将唯一的键映射到某个元素。当然键和值都是对象。
HashMap 当需要键值对表示,又不关心顺序时可采用HashMap。
Hashtable 注意Hashtable中的t是小写的,它是HashMap的线程安全版本,现在已经很少使用。
LinkedHashMap 当需要键值对,并且关心插入顺序时可采用它。
TreeMap 当需要键值对,并关心元素的自然排序时可采用它。
三、ArrayList的使用
ArrayList是一个可变长的数组实现,读取效率很高,是最常用的集合类型。
1、ArrayList的创建
在Java5版本之前我们使用:
1 |
List list = new ArrayList(); |
在Java5版本之后,我们使用带泛型的写法:
1 |
List<String> list = new ArrayList<String>(); |
上面的代码定义了一个只允许保存字符串的列表,尖括号括住的类型就是参数类型,也成泛型。带泛型的写法给了我们一个类型安全的集合。关于泛型的知识可以参见这里。
2、ArrayList的使用:
01 |
List<String> list = new ArrayList<String>(); |
02 |
list.add( "nihao!" ); |
03 |
list.add( "hi!" ); |
04 |
list.add( "konikiwa!" ); |
05 |
list.add( "hola" ); |
06 |
list.add( "Bonjour" ); |
07 |
System.out.println(list.size()); |
08 |
System.out.println(list.contains( 21 )); |
09 |
System.out.println(list.remove( "hi!" )); |
10 |
System.out.println(list.size()); |
关于List接口中的方法和ArrayList中的方法,大家可以看看JDK中的帮助。
3、基本数据类型的的自动装箱:
我们知道集合中存放的是对象,而不能是基本数据类型,在Java5之后可以使用自动装箱功能,更方便的导入基本数据类型。
1 |
List<Integer> list = new ArrayList<Integer>(); |
2 |
list.add( new Integer( 42 )); |
3 |
list.add( 43 ); |
4、ArrayList的排序:
ArrayList本身不具备排序能力,但是我们可以使用Collections类的sort方法使其排序。我们看一个例子:
01 |
import java.util.ArrayList; |
02 |
import java.util.Collections; |
03 |
import java.util.List; |
04 |
05 |
public class Test { |
06 |
07 |
public static void main(String[] args) { |
08 |
List<String> list = new ArrayList<String>(); |
09 |
list.add( "nihao!" ); |
10 |
list.add( "hi!" ); |
11 |
list.add( "konikiwa!" ); |
12 |
list.add( "hola" ); |
13 |
list.add( "Bonjour" ); |
14 |
15 |
System.out.println( "排序前:" + list); |
16 |
17 |
Collections.sort(list); |
18 |
19 |
System.out.println( "排序后:" + list); |
20 |
} |
21 |
22 |
} |
编译并运行程序查看结果:
排序前:[nihao!, hi!, konikiwa!, hola, Bonjour]
排序后:[Bonjour, hi!, hola, konikiwa!, nihao!]
5、数组和List之间的转换
从数组转换成list,可以使用Arrays类的asList()方法:
01 |
import java.util.ArrayList; |
02 |
import java.util.Collections; |
03 |
import java.util.List; |
04 |
05 |
public class Test { |
06 |
07 |
public static void main(String[] args) { |
08 |
09 |
String[] sa = { "one" , "two" , "three" , "four" }; |
10 |
List list = Arrays.asList(sa); |
11 |
System.out.println( "list:" +list); |
12 |
System.out.println( "list.size()=" +list.size()); |
13 |
} |
14 |
15 |
} |
6、Iterator和for-each
在for-each出现之前,我们想遍历ArrayList中的每个元素我们会使用Iterator接口:
01 |
import java.util.Arrays; |
02 |
import java.util.Iterator; |
03 |
import java.util.List; |
04 |
05 |
public class Test { |
06 |
07 |
public static void main(String[] args) { |
08 |
09 |
// Arrays类为我们提供了一种list的便捷创建方式 |
10 |
List<String> list = Arrays.asList( "one" , "two" , "three" , "four" ); |
11 |
12 |
// 转换成Iterator实例 |
13 |
Iterator<String> it = list.iterator(); |
14 |
15 |
//遍历 |
16 |
while (it.hasNext()) { |
17 |
System.out.println(it.next()); |
18 |
} |
19 |
20 |
} |
21 |
22 |
} |
在for-each出现之后,遍历变得简单一些:
01 |
import java.util.Arrays; |
02 |
import java.util.Iterator; |
03 |
import java.util.List; |
04 |
05 |
public class Test { |
06 |
07 |
public static void main(String[] args) { |
08 |
09 |
// Arrays类为我们提供了一种list的便捷创建方式 |
10 |
List<String> list = Arrays.asList( "one" , "two" , "three" , "four" ); |
11 |
12 |
for (String s : list) { |
13 |
System.out.println(s); |
14 |
} |
15 |
16 |
} |
17 |
18 |
} |
本讲内容:Map HashMap
前面课程中我们知道Map是个接口,它关心的是映射关系,它里面的元素是成对出现的,键和值都是对象且键必须保持唯一。这一点上看它和Collection是很不相同的。
一、Map接口
Map接口的常用方法如下表所示:
put(K key, V value) | 向集合中添加指定的键值对 |
putAll(Map <? extends K,? extends V> t) | 把一个Map中的所有键值对添加到该集合 |
containsKey(Object key) | 如果包含该键,则返回true |
containsValue(Object value) | 如果包含该值,则返回true |
get(Object key) | 根据键,返回相应的值对象 |
keySet() | 将该集合中的所有键以Set集合形式返回 |
values() | 将该集合中所有的值以Collection形式返回 |
remove(Object key) | 如果存在指定的键,则移除该键值对,返回键所对应的值,如果不存在则返回null |
clear() | 移除Map中的所有键值对,或者说就是清空集合 |
isEmpty() | 查看Map中是否存在键值对 |
size() | 查看集合中包含键值对的个数,返回int类型 |
因为Map中的键必须是唯一的,所以虽然键可以是null,只能由一个键是null,而Map中的值可没有这种限制,值为null的情况经常出现,因此get(Object key)方法返回null,有两种情况一种是确实不存在该键值对,二是该键对应的值对象为null。为了确保某Map中确实有某个键,应该使用的方法是 containsKey(Object key) 。
二、HashMap
HashMap是最常用的Map集合,它的键值对在存储时要根据键的哈希码来确定值放在哪里。
1、HashMap的基本使用:
01 |
import java.util.Collection; |
02 |
import java.util.HashMap; |
03 |
import java.util.Map; |
04 |
import java.util.Set; |
05 |
06 |
public class Test { |
07 |
08 |
public static void main(String[] args) { |
09 |
10 |
Map<Integer,String> map = new HashMap<Integer,String>(); |
11 |
12 |
map.put( 1 , "白菜" ); |
13 |
map.put( 2 , "萝卜" ); |
14 |
map.put( 3 , "茄子" ); |
15 |
map.put( 4 , null ); |
16 |
map.put( null , null ); |
17 |
map.put( null , null ); |
18 |
19 |
System.out.println( "map.size()=" +map.size()); |
20 |
System.out.println( "map.containsKey(1)=" +map.containsKey( 2 )); |
21 |
System.out.println( "map.containsKey(null)=" +map.containsKey( null )); |
22 |
System.out.println( "map.get(null)=" +map.get( null )); |
23 |
24 |
System.out.println( "map.get(2)=" +map.get( 2 )); |
25 |
map.put( null , "黄瓜" ); |
26 |
System.out.println( "map.get(null)=" +map.get( null )); |
27 |
28 |
Set set = map.keySet(); |
29 |
System.out.println( "set=" +set); |
30 |
31 |
Collection<String> c = map.values(); |
32 |
33 |
System.out.println( "Collection=" +c); |
34 |
35 |
} |
36 |
37 |
} |
编译并运行程序,查看结果:
1 |
map.size()= 5 |
2 |
map.containsKey( 1 )= true |
3 |
map.containsKey( null )= true |
4 |
map.get( null )= null |
5 |
map.get( 2 )=萝卜 |
6 |
map.get( null )=黄瓜 |
7 |
set=[ null , 1 , 2 , 3 , 4 ] |
8 |
Collection=[黄瓜, 白菜, 萝卜, 茄子, null ] |
2、HashMap 中作为键的对象必须重写Object的hashCode()方法和equals()方法
下面看一个我花了1个小时构思的例子,熟悉龙枪的朋友看起来会比较亲切,设定了龙和龙的巢穴,然后把它们用Map集合对应起来,我们可以根据龙查看它巢穴中的宝藏数量,例子只是为了说明hashCode这个知识点,所以未必有太强的故事性和合理性,凑合看吧:
01 |
import java.util.HashMap; |
02 |
import java.util.Map; |
03 |
04 |
public class Test { |
05 |
06 |
public static void main(String[] args) { |
07 |
08 |
// 龙和它的巢穴映射表 |
09 |
Map<dragon , Nest> map = new HashMap<dragon , Nest>(); |
10 |
11 |
// 在Map中放入四只克莱恩大陆上的龙 |
12 |
map.put( new Dragon( "锐刃" , 98 ), new Nest( 98 )); |
13 |
map.put( new Dragon( "明镜" , 95 ), new Nest( 95 )); |
14 |
map.put( new Dragon( "碧雷" , 176 ), new Nest( 176 )); |
15 |
map.put( new Dragon( "玛烈" , 255 ), new Nest( 255 )); |
16 |
17 |
// 查看宝藏 |
18 |
System.out.println( "碧雷巢穴中有多少宝藏:" + map.get( new Dragon( "碧雷" , 176 )).getTreasure()); |
19 |
} |
20 |
21 |
} |
22 |
23 |
// 龙 |
24 |
class Dragon { |
25 |
26 |
Dragon(String name, int level) { |
27 |
this .level = level; |
28 |
this .name = name; |
29 |
} |
30 |
31 |
// 龙的名字 |
32 |
private String name; |
33 |
34 |
// 龙的级别 |
35 |
private int level; |
36 |
37 |
public int getLevel() { |
38 |
return level; |
39 |
} |
40 |
41 |
public void setLevel( int level) { |
42 |
this .level = level; |
43 |
} |
44 |
45 |
public String getName() { |
46 |
return name; |
47 |
} |
48 |
49 |
public void setName(String name) { |
50 |
this .name = name; |
51 |
} |
52 |
53 |
} |
54 |
55 |
// 巢穴 |
56 |
class Nest { |
57 |
58 |
//我研究的龙之常数 |
59 |
final int DRAGON_M = 4162 ; |
60 |
61 |
// 宝藏 |
62 |
private int treasure; |
63 |
64 |
// 居住的龙的级别 |
65 |
private int level; |
66 |
67 |
Nest( int level) { |
68 |
this .level = level; |
69 |
this .treasure = level * level * DRAGON_M; |
70 |
} |
71 |
72 |
int getTreasure() { |
73 |
return treasure; |
74 |
} |
75 |
76 |
public int getLevel() { |
77 |
return level; |
78 |
} |
79 |
80 |
public void setLevel( int level) { |
81 |
this .level = level; |
82 |
this .treasure = level * level * DRAGON_M; |
83 |
} |
84 |
85 |
} |
编译并运行查看结果:
1 |
Exception in thread "main" java.lang.NullPointerException |
2 |
at Test.main(Test.java: 18 ) |
我们发现竟然报了错误,第18行出了空指针错误,也就是说get方法竟然没有拿到预期的巢穴对象。
在这里我们就要研究一下为什么取不到了。我们这里先解释一下HashMap的工作方式。
假设现在有个6张中奖彩票的存根,放在5个桶里(彩票首位只有1-5,首位是1的就放在一号桶,是2的就放在2号桶,依次类推),现在你拿了3张彩票来兑奖,一个号码是113,一个号码是213,一个号码是313。那么现在先兑第一张,取出一号桶里的存根发现存根号码和你的号码不符,所以你第一张没中奖。继续兑第二张,二号桶里就没存根所以就直接放弃了,把三号桶里的所有彩票存根都拿出来对应一番,最后发现有一个存根恰好是313,那么恭喜你中奖了。
HashMap在确定一个键对象和另一个键对象是否是相同时用了同样的方法,每个桶就是一个键对象的散列码值,桶里放的就是散列码相同的彩票存根,如果散列码不同,那么肯定没有相关元素存在,如果散列码相同,那么还要用键的equals()方法去比较是否相同,如果相同才认为是相同的键。简单的说就是 hashCode()相同 && equals()==true 时才算两者相同。
到了这里我们应该明白了,在没有重写一个对象的hashcode()和equals()方法之前,它们执行的是Object中对应的方法。而 Object的hashcode()是用对象在内存中存放的位置计算出来的,每个对象实例都不相同。Object的equals()的实现更简单就是看两个对象是否==,也就是两个对象除非是同一个对象,否则根本不会相同。因此上面的例子虽然都是名字叫碧雷的龙,但是HashMap中却无法认可它们是相同的。
因此我们只有重写Key对象的hashCode()和equals()方法,才能避免这种情形出现,好在Eclipse可以帮我们自动生成一个类的hashCode()和equals(),我们把上面的例子加上这两个方法再试试看:
001 |
import java.util.HashMap; |
002 |
import java.util.Map; |
003 |
004 |
public class Test { |
005 |
006 |
public static void main(String[] args) { |
007 |
008 |
// 龙和它的巢穴映射表 |
009 |
Map<dragon , Nest> map = new HashMap<dragon , Nest>(); |
010 |
011 |
// 在Map中放入四只克莱恩大陆上的龙 |
012 |
map.put( new Dragon( "锐刃" , 98 ), new Nest( 98 )); |
013 |
map.put( new Dragon( "明镜" , 95 ), new Nest( 95 )); |
014 |
map.put( new Dragon( "碧雷" , 176 ), new Nest( 176 )); |
015 |
map.put( new Dragon( "玛烈" , 255 ), new Nest( 255 )); |
016 |
017 |
// 查看宝藏 |
018 |
System.out.println( "碧雷巢穴中有多少宝藏:" + map.get( new Dragon( "碧雷" , 176 )).getTreasure()); |
019 |
} |
020 |
021 |
} |
022 |
023 |
// 龙 |
024 |
class Dragon { |
025 |
026 |
Dragon(String name, int level) { |
027 |
this .level = level; |
028 |
this .name = name; |
029 |
} |
030 |
031 |
// 龙的名字 |
032 |
private String name; |
033 |
034 |
// 龙的级别 |
035 |
private int level; |
036 |
037 |
public int getLevel() { |
038 |
return level; |
039 |
} |
040 |
041 |
public void setLevel( int level) { |
042 |
this .level = level; |
043 |
} |
044 |
045 |
public String getName() { |
046 |
return name; |
047 |
} |
048 |
049 |
public void setName(String name) { |
050 |
this .name = name; |
051 |
} |
052 |
053 |
@Override |
054 |
public int hashCode() { |
055 |
final int PRIME = 31 ; |
056 |
int result = 1 ; |
057 |
result = PRIME * result + level; |
058 |
result = PRIME * result + ((name == null ) ? 0 : name.hashCode()); |
059 |
return result; |
060 |
} |
061 |
062 |
@Override |
063 |
public boolean equals(Object obj) { |
064 |
if ( this == obj) |
065 |
return true ; |
066 |
if (obj == null ) |
067 |
return false ; |
068 |
if (getClass() != obj.getClass()) |
069 |
return false ; |
070 |
final Dragon other = (Dragon) obj; |
071 |
if (level != other.level) |
072 |
return false ; |
073 |
if (name == null ) { |
074 |
if (other.name != null ) |
075 |
return false ; |
076 |
} else if (!name.equals(other.name)) |
077 |
return false ; |
078 |
return true ; |
079 |
} |
080 |
081 |
} |
082 |
083 |
// 巢穴 |
084 |
class Nest { |
085 |
086 |
//我研究的龙之常数 |
087 |
final int DRAGON_M = 4162 ; |
088 |
089 |
// 宝藏 |
090 |
private int treasure; |
091 |
092 |
// 居住的龙的级别 |
093 |
private int level; |
094 |
095 |
Nest( int level) { |
096 |
this .level = level; |
097 |
this .treasure = level * level * DRAGON_M; |
098 |
} |
099 |
100 |
int getTreasure() { |
101 |
return treasure; |
102 |
} |
103 |
104 |
public int getLevel() { |
105 |
return level; |
106 |
} |
107 |
108 |
public void setLevel( int level) { |
109 |
this .level = level; |
110 |
this .treasure = level * level * DRAGON_M; |
111 |
} |
112 |
113 |
} |
编译并运行查看结果:
1 |
碧雷巢穴中有多少宝藏: 128922112 |
这一次正常输出了,真不容易^_^
好了本讲就到这里。