您的位置:首页 > 编程语言 > Java开发

JAVA常用集合框架用法详解——提高篇

2016-02-25 16:04 706 查看
这篇文章是我对集合框架的升华总结。文章中没有提到各个集合子类的最基本的操作和方法。想要知道这部分的内容,可以查看我的一篇基础知识的博客--Java集合框架总结基础篇http://blog.csdn.net/lulei1217/article/details/45167433。


这几天一直在看Java的集合框架。通过这几天的学习使我对集合有了一个全新的认识,现在来说说吧。先上集合的家谱图一张,来自Java编程思想书中的。



首先从容器的老祖先Collection接口说起。Collection接口有3个子接口依次是:List、Set 、Queue(这个不常用)。Map接口是自成一派,这个待会介绍。这个Collection接口有一个实现类就是AbstractCollection(抽象类)。这个类我们使用的不多,使用较多的是Collections这个类。此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。而3个子接口也都有实现抽象类分别是:AbstractList、 AbstractSet和AbstractQueue。有了这层概念,接下来就是这些抽象类也会被子类给继承。而这些子类就是我们所常用的。AbstractList的实现子类就是ArrayList和LinkedList(准确的说是孙子类),AbstractSet的实现子类是HashSet和TreeSet。LinkedHashSet是HashSet的子类。至于Map接口也是一样的,首先有一个实现接口的类AbstractMap和一个子接口SortedMap。AbstractMap类的子类有HashMap和WeakHashMap,HashMap有个子类叫做LinkedHashMap。SortedMap子接口有个实现类叫TreeMap(这个类也继承了AbstractMap类)。

上面是介绍了一些有关容器组成的一些内容。其中比较常用的是ArrayList、LinkedList、hashSet和hashMap。在定义这些容器对象的时候我们习惯使用向上转型的做法。比如:

List list=new ArrayList();

这样做的目的是限制了list对象的特有性。这样我们在进行将list对象转成LinkedList是没有任何问题的。这样的做法体现了面向对象的多态性。

--------------------------------------------*--------------------------------------------------

LinkedList link=new LinkedList(list);

上面的这种写法是有问题的,因为没有考虑的泛型的使用。主要原因是编译器允许你向容器中添加错误的类型,比如你想向容器中添加的全是Apple类,但是一不小心放了一个Orange类进去了。这时候编译是没有任何的问题的,但是在运行过程中一旦想遍历容器中的元素就会出现类型异常的错误。为此我们必须在声明和定义的时候就应该说明我这个容器只能装Apple类。我们可以修改代码如下:(加上泛型)

List<apple> list=new ArrayList<apple>();

如果还要考虑apple类会有子类那就需要这样修改:

List<? extends apple> list=new ArrayList<? extends apple>();

所以建议大家在使用容器的时候应该加上泛型。使用泛型后,你会发现如果再次将orange类加入容器中在编译期就会报错。

在所有的容器子类中,这里不得不说一下我们的容器一般只装对象,不会在容器中装基本数据类型(byte,int等是需要进行装箱处理的)。这样是不能通过编译的。如果我们想打印容器中的对象,就必须遍历这个容器类。如何遍历这个容器类呢?

这里Java API给我们提供了一个叫迭代器对象的机制。在Java中,迭代器Iterator对象实际上是一个接口,它有一个子接口叫ListIterator(只能在List中使用)。我们所有的容器类都会为我们提供了一个叫Iterator的迭代器对象,通过使用Iterator的一些方法来进行遍历操作。比如:

List<apple> list=new ArrayList<apple>();

list.add(new apple());//这个方法是向容器中添加元素

list.add(new apple());

list.add(new apple());

list.add(new apple());

list.add(new apple());

//获取容器的迭代器对象

Iterator<apple> it=list.iterator();

while(it.hasNext()){

apple a=it.next();

System.out.println(a);//输出容器中的对象

it.remove();//表示将容器中的对象给删除了

}

这里我们注意:

a.使用迭代器对象来操作容器中的元素(如:remove)和直接操作容器本身是一样的。

b.Iterator迭代器只有remove操作方法,如果想让迭代器对象可以给容器add添加元素和set修改元素则必须使用ListIterator迭代器。这个迭代器和他的名字一样只能操作List容器,当然这个迭代器支持容器对象的向后和向前的遍历。

在java 1.5之后,对于Collection容器官方是希望我们优先考虑使用Iterable迭代器,而Map容器则继续使用Iterator迭代器。Iterable接口实际上是包含了一个返回Iterator接口的iterator()方法。

当然这里我在提提foreach的语法。在介绍容器之前,我们主要是在数组中使用foreach的。其实在Collection容器中我们也会使用到的。因为我们在foreach中可以直接的在容器中移动Iterable接口的。这就是我说的为什么优先使用Iterable,因为只要实现这个接口我们就允许容器对象成为 "foreach" 语句的目标。 这样就不需要获取迭代器对象了。上一段代码:

import java.util.*;

public class IterableClass implements Iterable<String> {
protected String[] words = ("And that is how " +
"we know the Earth to be banana-shaped.").split(" ");
public Iterator<String> iterator() {
return new Iterator<String>() {//返回的是Iterator对象
private int index = 0;
public boolean hasNext() {
return index < words.length;
}
public String next() { return words[index++]; }
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(String s : new IterableClass())
System.out.print(s + " ");
}
}
输出结果:
And that is how we know the Earth to be banana-shaped.


这里需要注意一个我们在定义某个方法的时候参数可能是个Iterable。比如:

static<T> viod 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[] strs={”123”,”sdfd”,”sdf”};
//test(strs);//不能通过编译
test(Arrays.asList(strs));//这样可以
}


注意:当参数是Iterable的时候,我们可以传List容器类过去,因为List是可以自动转换的。但是不能直接传数组进去,必须将数组先转化为List。还有一个不得不提,在我们new一个容器的时候,我们一般是不太确定容器中元素的个数的,但是使用由数组转化为的List容器实际上我们是知道容器中元素的个数的。

---------------------------------*-----------------------------------

下面将要详细的介绍List、Set和Map的子类知识。首先就是这些容器的子类都是线程不同步的。都是线程不安全的。

Collection 接口派生的接口是 List 和 Set还有Queue。

Collection 接口提供的主要方法:

1. boolean add(Object o) 添加对象到集合;

2. boolean remove(Object o) 删除指定的对象;

3. int size() 返回当前集合中元素的数量;

4. boolean contains(Object o) 查找集合中是否有指定的对象;

5. boolean isEmpty() 判断集合是否为空;

6. Iterator iterator() 返回一个迭代器;

7. boolean containsAll(Collection c) 查找集合中是否有集合 C 中的元素;

8. boolean addAll(Collection c) 将集合 C 中所有的元素添加给该集合;

9. void clear() 删除集合中所有元素;

10. void removeAll(Collection c) 从集合中删除 C 集合中也有的元素;

11. void retainAll(Collection c) 从集合中删除集合 C 中不包含的元素。

但是我们的子接口还是添加了许多的针对自己接口特性的一些方法。这里所有的自己的特性指的是:

List承诺可以将元素维护在指定的序列中。有两种主要的List派生类ArrayList和LinkedList。

ArrayList类他的底层数据结构是由数组完成的,所以他的特点就是适合随机访问元素,直接类似于数组的Arrays[i]的操作。但是对于删除指定位置的元素和在指定的位置中插入元素就会比较麻烦的。因为会涉及到大量的数组元素的移动和复制。

LinkedList类他的底层数据结构是由一个自定义的Node节点类来完成的。这个节点类有3个属性:T date(用来存储元素的)、Node next(指向下一个Node类节点的)、Node Preview(指向前一个Node类节点)。LinkedList就是由这些Node类组成的链表结构组成的。所以LinkedList类适合插入和删除元素的操作,但是对于索引操作就比较的费事了。只能是由传统的迭代器的方式依次来遍历。属于比较耗时的线性操作。

下面使用代码来查看:

package com.wq520.rongqi;

import java.util.ArrayList;
import java.util.LinkedList;

public class TestArrayListAndLinkedList {
public static void main(String[] args){

ArrayList<Object> Alist = new ArrayList<Object>();
Object obj = new Object();
System.out.println("ArrayList的add方法耗时:(毫秒)");
long start = System.currentTimeMillis();
for(int i=0;i<5000000;i++){
Alist.add(obj);
}
long end = System.currentTimeMillis();
System.out.println(end-start);//查看ArrayList添加元素的耗时时间

LinkedList<Object> Llist = new LinkedList<Object>();
Object obj1 = new Object();
System.out.println("LinkedList的add方法耗时:(毫秒)");
start = System.currentTimeMillis();
for(int i=0;i<5000000;i++){
Llist.add(obj1);
}
end = System.currentTimeMillis();
System.out.println(end-start);//查看LinkedList添加元素的耗时时间

System.out.println("ArrayList的指定位置插入方法耗时:(毫秒)");
start = System.currentTimeMillis();
Object obj2 = new Object();
for(int i=0;i<1000;i++){
Alist.add(0,obj2);
}
end = System.currentTimeMillis();
System.out.println(end-start);

System.out.println("LinkedList的指定位置插入方法耗时:(毫秒)");
start = System.currentTimeMillis();
Object obj3 = new Object();
for(int i=0;i<1000;i++){
Llist.add(0,obj3);
}
end = System.currentTimeMillis();
System.out.println(end-start);

System.out.println("ArrayList的指定位置remove方法耗时:(毫秒)");
start = System.currentTimeMillis();
Alist.remove(0);
end = System.currentTimeMillis();
System.out.println(end-start);

System.out.println("LinkedList的指定位置remove方法耗时:(毫秒)");
start = System.currentTimeMillis();
Llist.remove(0);
end = System.currentTimeMillis();
System.out.println(end-start);
}
}


输出结果:
ArrayList的add方法耗时:(毫秒)
216
LinkedList的add方法耗时:(毫秒)
687
ArrayList的指定位置插入方法耗时:(毫秒)
8803
LinkedList的指定位置插入方法耗时:(毫秒)
0
ArrayList的指定位置remove方法耗时:(毫秒)
8
LinkedList的指定位置remove方法耗时:(毫秒)
0
---------------------------------*----------------------------


这里在来谈谈LinkedList类。在实际的应用中,我们会经常将LinkedList用来实现Stack(堆栈)、Queue(队列)。主要是由于LinkedList的双链表结构可以轻松的完成Stack的进栈和出栈的操作和队列的出队列和入队列的操作。但是用归用,我们这里不得不提的是如果直接使用如下的方式是很危险的:

List<T> stack=new LinkedList<T>();

因为这样做会使我们所谓的堆栈stack可以直接的使用了许多LinkedList类的特有的操作方法。一般也没人用这种方法。正确的做法就是自己定义一个Stack类,然后内部使用LinkedList来完成所有的堆栈专有的操作。即所谓的封装堆栈特有的一些操作方法。下面是定义的Stack类

public class Stack<T>{
private LinkedList<T> stack=new LinkedList<T>();
public void push(T t){
stack.addFirst(v);//压栈操作
}
public T peek(){
return stack.getFirst();//返回到栈顶
}
public T pop(){
return stack.removeFirst();//出栈操作
}
public boolean empty(){
return stack.isEmpty();
}
public String toString(){
return stack.toString();
}
}


好了,这里使用了通用泛型,引入了一些最基本的栈的操作方法。之后我们就可以将该类作为堆栈使用了。

--------------------------------*----------------------------------

接下来说说Set容器接口。该接口使用较多的的派生类是HashSet,TreeSet,和LinkedHashSet(HashSet的子类)。所有的Set接口的派生类都有一个特点就是类中所放的元素是不会重复的。也是说Set中的每一个元素都是唯一的。由于要保证元素的唯一性,所以加入Set的元素必须要定义自己的equals()方法以确保对象的唯一性。同时,Set容器中的元素是乱序的,不会维护元素的次序的。

谈谈Set的派生类。首先是:

HashSet:他是为了快速查找而设计的Set,存入HashSet的元素(记住,是个对象啊)必须要定义hashCode()方法。

TreeSet:他是保存次序的Set,底层的实现是树结构。使用它可以从Set中提取有序的序列。但是元素必须要实现Comparable接口。该接口是用于提供元素的排序规则的。

LinkdHashSet: 它有HashSet的查询速度,且内部使用链表维护元素的顺序(插入顺序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。元素也必须定义hashCode()方法。

----------------------------*----------------------------------

因为HashSet存入的是唯一的元素,所以我们必须能够使用一种方法来保证元素的唯一性。这里我们通常在元素类中覆写hashCode()来计算哈希值的方法。一般来说我们还会在元素类中覆写equals()方法,主要是确保当前的元素对象的确是唯一的。这里我们就会确保元素的唯一性,根据不同的哈希值来判断。当然我这样说是有不妥的,后面在讲解具体的哈希值是怎么实现的。

现在一旦有这个元素的哈希值和Set容器中的任何一个元素的哈希值是一样的,容器就会拒绝添加该元素。当然HashSet类除了操作比较快,就是元素的无序性。这也是它快的主要原因所在。

但是我们想让一个Set容器保存元素即能够是唯一的(这点是肯定的)又能够是有序的。这时候使用HashSet就没辙了。我们这时候只能使用TreeSet了。

默认的时候,当我们向TreeSet容器中添加元素的时候,容器会将我们的元素按照自然顺序来排序。但是我们是可以自己修改这种排序规则的。这时我们必须让添加的元素类实现一个叫做Comparable的接口。并且还要覆写该接口的compareTo()方法,在该方法里我们可以编写自己的排序规则代码。所以一旦你定义了一个TreeSet容器,我建议你必须元素类实现Comparable接口,并且实现compareTo()方法。

-----------------------------*--------------------------------

接下来说说队列。在Java SE中目前Queue的实现只有两个就是LinkList和PriorityQueue。他们两的差异不是他们的性能不一样,而是他们的排序行为不一样。

LinkedList是按照标准的火车站买票模型(不准插队的模式哦)来进行出队列和入队列,所以说第一个排队的肯定是第一个出来的。

PriorityQueue类是按照优先级来将当前容器中的元素先进行个排序,然后在出队列。比如说同样是火车站买票的模型,可能有的人排在最前但是火车要等好久才到站,而有的人是排在后面但是火车即将到站了,所以必须进行购票上车。一般购票员也允许他们插队,假如我们将当前排队的人按照火车到站的顺序来进行购票排队。这样不至于有些人还没买票火车就走的现象出现。这个按照火车到站的顺序就是一个排序规则。

我们的PriorityQueue容器就可以完成上面所说的改进的排队方式。这里容器的对象必须实现一个Comparable的接口,同时要覆写compareTo()方法,这个方法就是写我们自定义的优先出队列的规则。这时容器中的元素就会按照这个规则出队列。

在这里不得不提的一个就是Java API中是没有双向链表的。如果想实现双向链表,我们可以通过组合LinkedList类来实现一个Dequeue类。并且直接在LinkedList中暴露一些操作方法。这种操作和之前的Stack类是一样的。

----------------------------------*----------------------------------

最后我们来讨论容器的另一个家族就是Map接口。这个接口取代了Dictionary抽象类(该类有个子方法叫HashTable),成为了新的解决对象与对象间的映射关系的新方法。Map容器存储的元素是一个个的键值对(Key-Value)。我们可以简单的理解为夫妻关系哦。

在生活中有太多可以使用Map容器表示的例子。比如某位先生的月收入是5000。我们可以这样表示。这里也是优先使用泛型的,并且优先考虑使用HashMap。

Map<Person,Integer> map=new HashMap<Person,Integer>();

map.put(new Person(“张三”),5000);

当然Map是和数组一样的是很容易就能扩展到多维的。比如某个人养了多少个宠物。Map格式如下:Map<Person,List<Pet>>。List<Pet>容器中表示存储的宠物。

我们甚至可以在Map中存放Map作为值:比如某个人养了多少宠物,都是什么物种和名字分别叫什么。这样的Map格式如下:

Map<Person,Map<Animal,Names>>。

从上面的例子我们可以看出Map就是一个映射表。这里使用数组来模拟一下Map的操作:

public class AssociativeArray<K,V> {
private Object[][] pairs;
private int index;
public AssociativeArray(int length) {
pairs = new Object[length][2];
}
public void put(K key, V value) {
if(index >= pairs.length)
throw new ArrayIndexOutOfBoundsException();
pairs[index++] = new Object[]{ key, value };
}
@SuppressWarnings("unchecked")
public V get(K key) {
for(int i = 0; i < index; i++)
if(key.equals(pairs[i][0]))
return (V)pairs[i][1];
return null; // Did not find key
}
public String toString() {
StringBuilder result = new StringBuilder();
for(int i = 0; i < index; i++) {
result.append(pairs[i][0].toString());
result.append(" : ");
result.append(pairs[i][1].toString());
if(i < index - 1)
result.append("\n");
}
return result.toString();
}
public static void main(String[] args) {
AssociativeArray<String,String> map =
new AssociativeArray<String,String>(6);
map.put("sky", "blue");
map.put("grass", "green");
map.put("ocean", "dancing");
map.put("tree", "tall");
map.put("earth", "brown");
map.put("sun", "warm");
try {
map.put("extra", "object"); // Past the end
} catch(ArrayIndexOutOfBoundsException e) {
System.out.println("Too many objects!");
}
System.out.println(map);
System.out.println(map.get("ocean"));
}
}
输出结果是:
Too many objects!
sky : blue
grass : green
ocean : dancing
tree : tall
earth : brown
sun : warm
dancing


-------------------------------*----------------------------------

上面的例子只是一个用数组实现Map功能的类。Java API中肯定是不会用这种方法的。首先就是性能的问题,上面使用的get()方法使用的是线性搜索,这种方法执行的速度会比较慢。所以Map接口的实现类使用了哈希值来取代对键的线性搜索。Map的实现类有:HashMap,LinkedHashMap,TreeMap,WeakHashMap,ConcurrentHashMap和IdentityHashMap。

注意在使用Map容器的时候我们要特别注意键元素(Key)的要求。它应该和Set容器中的元素是一样的。任何的键都必须具有一个equals方法,如果键被用在了HashMap中,那么键元素类就必须有一个合适的hashCode方法。但是如果键被用在了TreeMap中,那么键元素类还必须要实现Comparable接口。

--------------------------*-----------------------------------

说到HashMap(或者HashSet)就不得不提哈希值和哈希函数(就是hashCode()方法)。在HashMap中我们是为了提高操作速度而不得不使用哈希的。他的运行模式如下。

1、首先HashMap会为我们提供一个指定大小的可以存放键元素的哈希值的桶位数组。(可以看成数组)

2、我们需要让键元素类有自己的hashCode()和equals()方法。这里会使用一些特定的哈希函数来进行计算的。

3、HashMap容器会通过键元素计算的哈希值,将其放入指定索引的桶位数组中。注意这里不是随机放的,虽然表面上看是很乱。实际上是根据不同的哈希方法获取哈希值,然后将哈希值对桶位数组求余。

4、接着会在当前索引的位置中创建了一个链表(内部完成的),然后将该键(Key)对应的值(Value)放入链表中。这个链表是存储计算哈希值一样但不是同一个实例对象的元素的。

5、如果下次插入的键元素计算的哈希值是一样的,就会将该键元素对应的值放入链表中存储。



注意:桶位数组的容量在HashMap的含参构造器中是可以设置的。

API中的定义如下:

HashMap(int initialCapacity, float loadFactor)

//构造一个带指定初始容量和加载因子(即上面的桶位数组中空桶位数量占的比例低于loadFactor就需要新的桶位了)的空 HashMap。

还有一个TreeMap接口,它的一些操作和TreeSet是一样的,只是存储的对象不是单个的对象而是键值对这种映射关系对象对。他是一种创建有序Map的方式。排序规则是通过实现Comparable接口来完成的(即需要覆写compareTo()方法)。

----------------------------------*----------------------------------

Map接口使用最多的就是get和put方法了。put方法是向Map接口中添加键值对,而get是取出键值对。在Map中我们是如何取元素的呢?这里肯定是少不了遍历的操作的。Map接口实际上给我们提供了3中集合视图来供我们遍历取值的。

1、首先是使用keySet()方法来获取key的Set集合对象,然后遍历这个Set集合对象即可。这是遍历获取Key的元素的方法。

2、第二种是使用values()方法来直接返回value的集合。然后在遍历这个value集合。这时遍历获取Value的元素的方法。
3、第三种方法使用的较多。是使用entrySet()方法来返回一个Map.Entry<K,V>的键值对接口的集合。然后调用Map.Entry<K,V>的getKey()和getValue()方法来获取键和值的对象。既然说Map是存储映射关系的,所以我们肯定是希望Key和Value能够一起出现的遍历中。所以使用entrySet()的较多。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: