周浩

  BlogJava :: 首页 :: 新随笔 :: 联系 ::  :: 管理 ::
  28 随笔 :: 0 文章 :: 16 评论 :: 0 Trackbacks

Java 容器类学习心得

一心得

1.  接口
整个 Java 容器类的基础是容器接口(例如 Collection Map 等接口),而不是类。使用接口的最大好处在于将容器的实现与容器的接口分开,这就意味着你可以使用相同的方法访问容器而不用关心容器是由什么样的数据结构实现的。同样, Iterator 接口也使得用户可以使用相同的方法访问不同的容器类。以上这些是通用算法的基础。

1.1 Collection 接口
Collection
接口有如下基本方法:
boolean add(Object obj)
:如果添加对象后,集合确实发生了变化,则返回 true ;否则返回 false
Iterator iterator()
:返回一个实现了 Iterator 接口的对象
此外,还有
int size(),boolean isEmpty()
boolean contains(Object obj) void clear() 等许多有用的方法

1.2 Map 接口
Map
用于存放关键字 / 值对。有如下基本方法:
Object get(Object key)
Object put(Object key,Object balue)
Set keySet()
Set entrySet()
此外,还有其他有用的方法。
需要注意的是,从表面看它似乎就是一种由键值对构成的集合,但实际上并不是这样。不过另一方面假如将 Map 的某一部分看作集合,有时候也还是显得非常方便的。换言之你可以创建一个集合用它来表达 Map 的那一部分。综上所述,一个 Map 可以返回的东西包括它的键值构成的一个 Set 、由它的值构成的一个集合或者由它的键值对构成的一个 Set

1.3 Iterator 接口
Iterator
接口有下面 3 个基本方法:
Object next()
:返回迭代器刚越过的元素的引用
boolean hasNext()
:判断容器内是否还有可供访问的元素
void remove()
:删除迭代器刚越过的元素
注意: Java 中的迭代器与 STL 中的迭代器在概念上有很重要的区别。在 STL 中,迭代器类似于数组的索引,使用这种迭代器可以查看存放在该位置上的元素(类似于通过数组索引 i 来访问 c[i] 一样)。 Java 中的迭代器并不这样运行。查看与位置的变化紧密的结合在一起。每次通过 next() 访问一个元素的同时,迭代器的位置会自动向前走一步。
这个问题可以这样理解: Java 中的迭代器指向的位置并不是元素,而是元素之间。这样,每次调用 next() 时,迭代器便越过下一个元素,同时返回它刚越过的那个元素的引用。
根据上面的说明,很容易得出下面的代码是错误的:
it.remove();
it.remove();
而下面的代码是正确的:
it.remove();
it.next();
it.remove();
迭代器的典型应用
Iterator it=c.iterator();
while(it.hasNext())
{
 Object obj=it.next();
 //do something with obj
}

1.4  子接口
1.4.1 List
接口
List
Collection 接口中分立出来是因为 List 的特点——有序的集合。这里指的有序并不是按照大小排好序的( Sorted ),而是指集合是可以以确定的顺序访问的序列。针对 List 的这个特点,它比 Collection 接口增加了通过索引进行操作的方法。例如, add remove get set 等方法的参数表中都可以加入索引的数值,从而操作处在索引位置处的元素。

1.4.2 Set 接口
Set
List 的不同,它里面的元素是无序的;所以,不能通过任何索引的方法来操作 Set 对象

1.4.3 ListIterator 接口
使用与 List 的迭代器,比 Iterator 接口增加了一些方法(例如 add() 等)。此外,由于 List 是双向表,所以还增加了 Object previous() boolean hasPrevious() 方法,用法与 next() hasNext() 一样。

1.4.4 SortedMap 接口
包含如下基本方法:
Comparator comparator()
Object firstKey()
Object lastKey()

2.  抽象容器类
2.1 
抽象容器类包括 AbstractCollection AbstractList AbstractSet 等等

2.2  为什么要有抽象结合类?
例如 Collection 接口中定义了许多有用的方法,如果实现 Collection 接口的每个类都自行实现这么多的方法,那将是非常麻烦的。为了使实现 Collection 接口的类的实现更容易, AbstractCollection 类让一些基本方法(比如 add() iterator() )变成了抽象的方法,而利用这些基本方法的其他方法(例如 addAll() 等等)则具体实现了。

3.  具体的容器
3.1 ArrayList
LinkedList
都是实现了 List 接口的类,是有序集。 List 接口支持通过索引的方法来访问元素,对于这一点, ArrayList 没有任何问题;但是对于 LinkedList 则有很大的问题,链表本身不应该支持随机存储,但是作为 List 的一个实现,链表也提供了对随机访问的支持,但是效率很低。每次通过索引的方法都是进行一次遍历。我认为,其实就不应该让链表支持随机访问;而 Java 这样实现我想是因为整个集合框架的体系,使得链表与数组可以使用同样的方法使用。综上所述,对于 LinkedList 最好不使用随机访问,而使用迭代器。

3.2 TreeSet
3.2.1 TreeSet
SortedSet 的一个实现。根据数据结构的知识可以知道,树的效率非常高,而且 Java 标准库中有 TreeSet 这样的类,以后应该尽量使用 TreeSet 来提高程序的效率。
3.2.2 
需要注意的是: TreeSet 作为有序集,它通过 compareTo 或者 Comparator 来将集合元素排序。任何具有相同比较值的元素(无论它们是否 equals() ),在 TreeSet 中都作为同一个元素,从而不能有重复。这样以来,即使是不同的对象也不能加入到集合中,这一点有时候很不方便。我在编写 A* 算法时,不同状态有时候对应着同一个启发函数值,那么这些不同的状态就无法加入到 TreeSet 中。

3.3 HashSet
3.3.1 HashSet
是非常高效的数据结构,与 TreeSet 不同, HashSet 是比较对象的 equals() 方法来区分不同的对象。这样只有真正不同的对象才能不被重复的加入到集合中。
3.3.2 
需要注意的是: HashSet 效率非常高,但是对象的 hashCode 函数不好确定。一般默认的对象的 hashCode 函数是根据对象的内存地址得到的。好的 hashCode 函数是 HashSet 成功运用的关键。

4.  视图
4.1 
什么是视图?
对映象类使用 keySet() 方法,仿佛该方法建立了一个新的集合,并将影响的所有关键字都填入这个集合。实际情况并非如此,对这个集合的任何操作都将反映到原始的映象对象上。
实际上, keySet() 返回的是一个实现 Set 接口的对象,对该对象的操作就是对映象的操作。这样的集合成为视图。
4.2 
视图的应用
4.2.1 
将现有的容器变为线程安全的容器:使用 Collections.synchronizedCollection(Collection c) 方法,在 SDK 文档中该方法的解释是“ Returns a synchronized (thread-safe) collection backed by the specified collection ”。
4.2.2 
将现有的容器变为只读的容器:使用 Collections.unmodifiableCollection(Collection c) 方法,在 SDK 文档中该方法的解释是“ Returns an unmodifiable view of the specified collection. ”。
4.2.3 
子范围
4.2.4 Arrays
类中的 asList() 方法


5. 
通用算法
通用的集合接口带来的一大好处就是可以编写通用算法。可以使用 Collections 中的静态通用方法,也可以编写自己的通用方法。
(具体的算法的内容在此略去)


总结:千万记住这句话——没有最好的容器(数据结构),要根据不同的问题选择不同的容器,以此来达到功能的要求和效率的最优。

二、 Java2 中的容器类库

Java1.2 之后 Java 版本统称为 Java2 Java2 中的容器类库才可以说是一种真正意义上的集合框架的实现。基本完全重新设计,但是又对 Java1 中的一些容器类库在新的设计上进行了保留,这主要是为了向下兼容的目的,当用 Java2 开发程序时,应尽量避免使用它们, Java2 的集合框架已经完全可以满足你的需求。有一点需要提醒的是,在 Java1 中容器类库是同步化的,而 Java2 中的容器类库都是非同步化,这可能是对执行效率进行考虑的结果。
Java2
中的集合框架提供了一套设计优良的接口和类,使程序员操作成批的数据或对象元素极为方便。这些接口和类有很多对抽象数据类型操作的 API ,而这是我们常用的且在数据结构中熟知的。例如 Maps Sets Lists Arrays 等。并且 Java 用面向对象的设计对这些数据结构和算法进行了封装,这就极大的减化了程序员编程时的负担。程序员也可以以这个集合框架为基础,定义更高级别的数据抽象,比如栈、队列和线程安全的集合等,从而满足自己的需要。
Java2
的集合框架,抽其核心,主要有三类: List Set Map 。如下图所示:

从图上可以看出, List Set 继承了 Collection ,而 Map 则独成一体。初看上去可能会对 Map 独成一体感到不解,它为什么不也继承 Collection 呢?但是仔细想想,这种设计是合理的。一个 Map 提供了通过 Key Map 中存储的 Value 进行访问,也就是说它操作的都是成对的对象元素,比如 put() get() 方法,而这是一个 Set List 所不就具备的。当然在需要时,你可以由 keySet() 方法或 values() 方法从一个 Map 中得到键的 Set 集或值的 Collection 集。
1
Collection 接口提供了一组操作成批对象的方法,用 UML 表示的方法列表如下:
它提供了基本操作如添加、删除。它也支持查询操作如是否为空 isEmpty() 方法等。为了支持对 Collection 进行独立操作, Java 的集合框架给出了一个 Iterator ,它使得你可以泛型操作一个 Collection ,而不需知道这个 Collection 的具体实现类型是什么。它的功能与 Java1 中的 Enumeration 类似,只是更易掌握和使用,功能也更强大。在建立集合框架时, Sun 的开发团队考虑到需要提供一些灵活的接口,用来操作成批的元素,又为了设计的简便,就把那些对集合进行可选操作的方法与基本方法放到了一起。因为一个接口的实现者必须提供对接口中定义的所有方法的实现,这就需要一种途径让调用者知道它正在调用   的可选方法当前不支持。最后开发团队选择使用一种信号,也即抛出一种不支持操作例外 (UnsupportedOperationException) ,如果你在使用一个 Collection 中遇到一个上述的例外,那就意味着你的操作失败,比如你对一个只读 Collection 添加一个元素时,你就会得到一个不支持操作例外。在你实现一个集合接口时,你可以很容易的在你不想让用户使用的方法中抛出 UnsupportOperationException 来告诉使用者这个方法当前没有实现, UnsupportOperationException RuntimeException 的一个扩展。
另外 Java2 的容器类库还有一种 Fail fast 的机制。比如你正在用一个 Iterator 遍历一个容器中的对象,这时另外一个线程或进程对那个容器进行了修改,那么再用 next() 方法时可能会有灾难性的后果,而这是你不愿看到的,这时就会引发一个 ConcurrentModificationException 例外。这就是 fail fast
2
List 接口对 Collection 进行了简单的扩充,它的具体实现类常用的有 ArrayList LinkedList 。你可以将任何东西放到一个 List 容器中,并在需要时从中取出。 ArrayList 从其命名中可以看出它是一种类似数组的形式进行存储,因此它的随机访问速度极快,而 LinkedList 的内部实现是链表,它适合于在链表中间需要频繁进行插入和删除操作。在具体应用时可以根据需要自由选择。前面说的 Iterator 只能对容器进行向前遍历,而 ListIterator 则继承了 Iterator 的思想,并提供了对 List 进行双向遍历的方法。
3
Set 接口也是 Collection 的一种扩展,而与 List 不同的时,在 Set 中的对象元素不能重复,也就是说你不能把同样的东西两次放入同一个 Set 容器中。它的常用具体实现有 HashSet TreeSet 类。 HashSet 能快速定位一个元素,但是你放到 HashSet 中的对象需要实现 hashCode() 方法,它使用了前面说过的哈希码的算法。而 TreeSet 则将放入其中的元素按序存放,这就要求你放入其中的对象是可排序的,这就用到了集合框架提供的另外两个实用类 Comparable Comparator 。一个类是可排序的,它就应该实现 Comparable 接口。有时多个类具有相同的排序算法,那就不需要在每分别重复定义相同的排序算法,只要实现 Comparator 接口即可。
集合框架中还有两个很实用的公用类: Collections Arrays Collections 提供了对一个 Collection 容器进行诸如排序、复制、查找和填充等一些非常有用的方法, Arrays 则是对一个数组进行类似的操作。
4
Map 是一种把键对象和值对象进行关联的容器,而一个值对象又可以是一个 Map ,依次类推,这样就可形成一个多级映射。对于键对象来说,像 Set 一样,一个 Map 容器中的键对象不允许重复,这是为了保持查找结果的一致性 ; 如果有两个键对象一样,那你想得到那个键对象所对应的值对象时就有问题了,可能你得到的并不是你想的那个值对象,结果会造成混乱,所以键的唯一性很重要,也是符合集合的性质的。当然在使用过程中,某个键所对应的值对象可能会发生变化,这时会按照最后一次修改的值对象与键对应。对于值对象则没有唯一性的要求。你可以将任意多个键都映射到一个值对象上,这不会发生任何问题(不过对你的使用却可能会造成不便,你不知道你得到的到底是那一个键所对应的值对象)。 Map 有两种比较常用的实现: HashMap TreeMap HashMap 也用到了哈希码的算法,以便快速查找一个键, TreeMap 则是对键按序存放,因此它便有一些扩展的方法,比如 firstKey(),lastKey() 等,你还可以从 TreeMap 中指定一个范围以取得其子 Map 。键和值的关联很简单,用 pub(Object key,Object value) 方法即可将一个键与一个值对象相关联。用 get(Object key) 可得到与此 key 对象所对应的值对象。

三、未来的 Java 容器类库

前面几部分对 Java 中容器类库的过去与现在的状况进行了讨论,然而就在写下此文时, Sun 已经开始通过某种途径分发 J2SE1.5 Alpha 测试版了。在今年的 JavaOne 大会上,诸多大师描绘了 Java 的美好未来与在下一个版本中即将加入的一些新特性,其中为容器类库加入的一个重要特性就是泛型。
其实泛型并不是什么新东西,在其它一些面向对象的语言中早已存在,如 C++ 。泛型的基本目标很简单:能够保证你使用一种类型安全的容器。那么到底怎样一种类型安全呢?我们先看下面这一段没有使用泛型特性的代码:

1.    import  java.util.*;

2.    public  class Generics{

3.        /**

4.         *  输出一个 String 类型的列表,假设所给参数 list 中所有元素都为 String

5.         */

6.        public static void printList(List list){

7.            for(int i=0;i<list.size();i++){

8.                System.out.println(((String)list.get(i)).toString());

9.            }

10.       }

11.       public static void main(String[] args){

12.           List list=new ArrayList();

13.           for(int i=0;i<9;i++){

14.               list.add("Number:"+Integer.toString(i));

15.           }

16.           //list.add(new Generics());  //(1)

17.           printList(list);

18.       }

19.   }

上面的代码很简单,定义了一个静态方法来打印一个元素为 String 类型的 List ,然而正如你看到的一样,如果你试着将 (1) 行中前面的注释去掉,你就会得到一个 ClassCastException 例外,因为 printList 会将 list 中的每个元素都转型为 String ,而在遇到最后一个元素时发现它是一个 Generics 类型从而无法完成转型,例外就被抛出。这种情况在 Java 编程中很容易出现,原因是 Java 的容器类库通常保存的是 Object 类型,而这是所有类的直接或间接超类,这就允许你将任何类型的元素添加到一个 List 中而不会给你任何提示,有经验的程序员可能会自己编写一个容器类来限制添加到其中的元素,这是一个编程技巧。但是现在我们就再也不用那样做了,泛型机制会为我们做好这件事。那就看一下用泛型机制对上面代码进行的改进:

1.    import  java.util.*;

2.    public  class Generics{

3.        /**

4.         *  输出一个 String 类型的列表,限制了所给参数 list 中所有元素都为 String

5.         */

6.        public static void printList(ArrayList<String> list){

7.            for(int i=0;i<list.size();i++){

8.                System.out.println(list.get(i).toString());

9.                //get() 返回的不再是 Object 类型,而是 String 类型

10.           }

11.       }

12.       public static void main(String[] args){

13.           ArrayList list=new ArrayList<String>(); // 注意此行中声明语法的变化

14.           for(int i=0;i<9;i++){

15.               list.add("Number:"+Integer.toString(i)); // 只能向其中添加 String 类型

16.           }

17.           list.add(new Generics());  // 无法通过,编译时错误

18.           printList(list);

19.       }

  1. }
    正如在代码中所看到的,容器的声明有了变化,即在一个容器类后面用 <> 来说明你想要放入这个容器中的元素类型,那么接下来你只能向这个容器加那种类型,否则编译就无法通过。在 printList 中也省去了转型的麻烦。当然有了泛型,并不是说以前的声明方法不能用了,你完全可以还用以前的方法,这没有任何问题。其实根据 JSR 中对 for 语句功能的增强,遍历一个容器也会更加简单。
    当然泛型的使用方法不只如此,这里并没有对它进行完整描述,只想告诉你,泛型确实为我们编程提供了便利,但是仍然需要用心去学习和掌握。
    随着 Java 的进一步完善,它的功能和易用性也得到提高,我有理由相信 Java 在计算机语言中所占的位置也会更加牢固,让喜爱 Java 的人更加喜爱它。祝愿 Java 一路走好!
posted on 2006-04-28 16:23 Derek 阅读(6432) 评论(0)  编辑  收藏

只有注册用户登录后才能发表评论。


网站导航:
 
Google