在java学习和编写java程序的时候,容器是用的最多的类库之一,如果只是简单地使用它,java的api文档 就够了,但是想充分利用容器的效率,以便写出更好的代码,就需要对其深入研究了,甚至去看它的源代码 。在前面的博客中,我说过java容器类库构造是很复杂的,这篇文章将尝试着将关系理清楚,并尽量在编写 容器代码的时候做最好的选择。
Collection
Collection是java容器的基类,将java的所有类追溯到最顶端就是Collection类。很显然它是一个接口, 但是所有的类库都不是直接继承自它,中间一般都有一个或多个Abstract类作为中介,为什么呢?
java类库都是由接口和Abstract类继承而来,接口都是一些继承类必须实现的方法定义(接口中不能有具 体的实现),而Abstract类实现其中一部分的公用方法,到最后的可实例化类就可以只实现跟它相关的方法, 这里充分体现了面向接口编程的思想。
由Collection继承而来的三个容器接口构成了java容器的基础,它们是List,Set和Map。将这三类容器使用 好基本上java容器就轻车熟路了。
List选择
在java容器中一般数List最常用,它就是一个动态的线性表接口,其有两种基本的形式,分别是ArrayList 和LinkedList。
ArrayList 源码中就是用数组来实现存储的,所以它具有随机存储的特性,所以ArrayList的set与 get方法效率较高
LinkedList 是线性表的链表实现,与c++中的链表原理一样,所以它不具有随机存储的特性,所以 在LinkedList中的删除和添加操作会比较高效。
在容器选择的时候跟我们在c++中的数组和链表的选择一样,一般ArrayList是首选,但是当数据量比较大, 且需要频繁做插入和删除操作的时候,第一选择肯定是LinkedList。
Set选择
Set,顾名思义就是不允许重复的集合。它一般也有两种选择,即HashSet和TreeSet,但是选择Set可没有 List那么纠结,一般HashSet都是首选,因为HashSet的性能总是比TreeSet好,TreeSet存在的唯一原因就是 它维持元素的排序状态。如果我们需要一个排好序的Set的时候,TreeSet才是首选。
那么HashSet和TreeSet又是怎么实现的呢,其实在jdk1.7源码里面,他们都是通过Map来维持的,关于hashCode 等会在Map中讲到。
Map选择
Map也是java中一个使用极其频繁的容器。它亦有两种形式:
HashMap 可以想见HashMap中的索引是通过HashCode来散列的,HashMap中的键值对是 (Object key, Object value),如果不指定key的equals()和hashCode函数,那么key所对应的散列值是通过Object基类中 的hashCode()方法计算出来的,它使用的是对象的地址码,也就是所即便两个对象的内容全部相同,但是 由于地址码不一样(即两个不同的对象)对应的key值也是不一样的。这样想,为什么一般key值会选择String? 因为String是java中的常量,它存储在常量区,所以内容相同的String值往往对应相同的键值。我们如果 要实现两个对象的内容一样即对应相同的key值,那么就需要覆写equals()与hashCode方法,缺一不可。
TreeMap 它的键值对是通过红黑树来存储的,所以它的查找和插入操作都是基于二叉树形结构的,通过keySet 取出来的键值集合也是排序的。
在进行Map选择的时候,我们也是首选HashMap,而一般不会要求Map始终保持有序。
静态方法
Collections和Arrays两个类为我们提供大量的静态方法可以操作容器和数组,打开这两个类,里面都是 静态方法,所以它们是提供一系列方法的工具类。
Collections 使用它最多的时候就是利用它来进行排序,它能够接收基本类型也可以接收泛型对象,非基本类型必须 提供compareTo方法,我们在写算法的时候经常使用它来为容器排序。
Arrays 一看这个名字就知道它是提供数组方法的,但是数组和容器可以很方便地相互转换:
Array2List:Arrays.asList(); List2Array: List.toArray();注意此处返回的数组是object[]
从上面可以很清晰的看到,java中的向上转型后的具体类型信息是在运行时确定的。在写java程序的时候 我们可以尽量使用Arrays中的方法来操作数组。
sort() 数组排序,Collections中的排序实际上是调用的此方法
fill() 填充数组
copyOf()/copyOfRange() 区段拷贝数组
binarysearch() 数组的二分查找
迭代器
我们在同步操作容器的时候,如在for循环中添加和删除对象经常会遇到ConcurrentModificationException 异常,网上有完整的解释:
Iterator是工作在一个独立的线程中,并且拥有一个 mutex锁,就是说Iterator在工作的时候,是不允许被迭代的对象被改变的。Iterator被创建的时候,建立了一个内存索引表(单链表),这 个索引表指向原来的对象,当原来的对象数量改变的时候,这个索引表的内容没有同步改变,所以当索引指针往下移动的时候,便找不到要迭代的对象,于是产生错 误。List、Set等是动态的,可变对象数量的数据结构,但是Iterator则是单向不可变,只能顺序读取,不能逆序操作的数据结构,当 Iterator指向的原始数据发生变化时,Iterator自己就迷失了方向。
这么时候我们得借助Iterator来帮我们实现同步的删除操作,实际上上文的意思很简单:使用foreach来 遍历Map等容器的时候使用的是Iterator,如果我们使用容器来直接更改就会导致和Iterator的不一致, 如果使用Iterator的remove()函数修改则不会有问题。
迭代器虽然提供的API有限但是它给我们提供了一个很好的操作和容器分离的环境,有人说Iterator只能 单向移动就错了,容器中提供双向移动的迭代子ListIterator。
容器延伸
好像我们学完了java容器,似乎它只是一个线性表而已,我们在数据结构课中学习的队列Queue,栈(Stack) 等数据结构哪里去了,这里做个解释。其实Queue和Stack有实现:
Queue Queue也是java中一个比较重要的接口,我们通常使用LinkedList来实现它,如:
Queue<Integer> a=new LinkedList<Integer>(); for(int i=0;i<30;i++){ a.offer(i); } while(!a.isEmpty()){ System.out.print(a.poll()+" "); }
实在很简单吧。
Stack java也有专门的Stack类库(继承自Vector),但是我们并不推荐使用它,因为它并不是一个良好的实现, 保留它只是为了向前兼容而已。我们可以使用LinkedList来代替Stack的实现,如:
LinkedList<Integer> a= new LinkedList<Integer>(); for(int i=0;i<30;i++){ a.addFirst(i); } while(!a.isEmpty()){ System.out.print(a.getFirst()+" "); a.removeFirst(); }
- Vector 在c++中,vector很受欢迎,但是在java中Vector的性能不佳,List就是它的替代品,既然我们不推荐使用 Stack(继承自Vector),那么连Vector也一块废弃了吧。
后话
基本上了解了以上内容以后,在java容器的使用上应该是不会茫然了吧。