java深入(一)-容器

在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容器的使用上应该是不会茫然了吧。



Previous     Next
zhing /
Published under (CC) BY-NC-SA in categories java  tagged with java