java容器之一_概述

来源:互联网 发布:比较好的程序员论坛 编辑:程序博客网 时间:2024/05/18 22:40

1 数组


数组( Array )是一种数据结构,用来存储同一类型值的集合,该类型的值可以是基本类型也可以是对象类型,通过一个整型下标可以访问数据的每一个值,(ps:通过反射模式也可以访问数组元素,即可以是  Array.get()操作,具体方法请查看相关API)

数组的基本使用方法就不需要具体介绍了,主要是特点介绍和一般容器类的对比。不同的书本或博客可能对比的方面具体不同


1.1 基本情况:

唯一:

  • 访问方法唯一 [] (补充:可以使用反射模式Array)
  • 属性或方法唯一 length

不同:

  • 对象数组 保留的是引用
  • 基本类型数组 直接保存基本类型的值

不确定:

  • 只能知道数组大小,无法确切知道有多少元素


1.2 对比:

  • 效率 数组是一种效率最高的存储和随机访问对象引用序列的方法
  • 类型 数组可以处理具体类型数据
  • 通用的容器类 不处理具体类型,所有对象都按Object类型处理。不能保存基本类型。
  • 特点 数组:大小固定 生命周期不可改变
  • 容器:灵活性强 全object类型处理 相互间可以转换


1.3 扩展了解:

Java标准库:

  • System.arraycopy() 比for要高效多的复制数组
  • 对象是采用的浅复制引用。


1.4 基本方法:

四个基本方法(都被基本类型和Object重载过):

  • equals() 比较两个数组是否相等
  • fill() 用某个值填充整个数组
  • sort() 对数组进行排序 Comparable 接口
  • binarySearch() 在已经排序的数组中查找元

asList () 将数组 转换为List容器

  • sort() 比较接口也可以使用策略模式(strategy)

1.5 详细解释:

效率和类型。对于Java来说,为保存和访问一系列对象(实际是对象的句柄)数组,最有效的方法莫过于数组。数组实际代表一个简单的线性序列,它使得元素的访问速度非常快,但我们却要为这种速度付出代价:创建一个数组对象时,它的大小是固定的,而且不可在那个数组对象的"存在时间"内发生改变。可创建特定大小的一个数组,然后假如用光了存储空间,就再创建一个新数组,将所有句柄从旧数组移到新数组。这属于"矢量"(Vector)类的行为。然而,由于为这种大小的灵活性要付出较大的代价,所以我们认为矢量的效率并没有数组高。


C++的矢量类知道自己容纳的是什么类型的对象,但同Java的数组相比,它却有一个明显的缺点:C++矢量类的operator[]不能进行范围检查,所以很容易超出边界(然而,它可以查询vector有多大,而且at()方法确实能进行范围检查)。在Java中,无论使用的是数组还是集合,都会进行范围检查,若超过边界,就会获得一个RuntimeException(运行期违例)错误。这类违例指出的是一个程序员错误,所以不需要在代码中检查它。在另一方面,由于C++的vector不进行范围检查,所以访问速度较快,在Java中,由于对数组和集合都要进行范围检查,所以对性能有一定的影响。


接下来还要学习另外几种常见的集合类:List(队列)、Stack(堆栈)以及Hashtable(散列表)。这些类都涉及对对象的处理,-好象它们没有特定的类型。换言之,它们将其当作Object类型处理(Object类型是Java中所有类的"根"类)。从某个角度看,这种处理方法是非常合理的:我们仅需构建一个集合,然后任何Java对象都可以进入那个集合(除基本数据类型外,-可用Java的基本类型封装类将其作为常数置入集合,或者将其封装到自己的类内,作为可以变化的值使用)。这再一次反映了数组优于常规集合:创建一个数组时,可令其容纳一种特定的类型。这意味着可进行编译期类型检查,预防自己设置了错误的类型,或者错误指定了准备提取的类型。当然,在编译期或者运行期,Java会防止我们将不当的消息发给一个对象。所以我们不必考虑自己的哪种做法更加危险,只要编译器能及时地指出错误,同时在运行期间加快速度,目的也就达到了。此外,用户很少会对一次违例事件感到非常惊讶的。


考虑到执行效率和类型检查,应尽可能地采用数组。然而,当我们试图解决一个更常规的问题时,数组的局限也可能显得非常明显。(也不推荐使用Vector类 属于比较老的方法)

数组的这种局限性也是相对的,下面提供一个对数组使用比较深入的博文地址:

http://cping1982.blog.51cto.com/601635/130062/


2 容器简介和接口图


2.1 接口关系


(图 1.1)


(图 2.2)


2.2 容器简介

容器( Collection )是最基本的集合接口,一个容器( Collection )保存一组对象(Object ),即对象是容器的元素(Elements )。一些 Collection 允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK 不提供直接继承自Collection 的类, Java SDK 提供的类都是继承自Collection的 " 子接口 " 如List 和 Set 。  


所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的   Collection,有一个 Collection参数的构造函数用于创建一个新的 Collection ,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个  Collection。  


如何遍历Collection 中的每一个元素?不论 Collection的实际类型如何,它都支持一个 iterator() 的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

         Iterator it = collection.iterator(); //  获得一个迭代子       while(it.hasNext()) {    Object obj = it.next(); //  得到下一个元素       }

由Collection接口派生的两个接口是List 和 Set 。 List 按对象进入的顺序保存对象,不做排序或编辑操作。Set 对每个对象只接受一次,并使用自己内部的排序方法 ( 通常,你只关心某个元素是否属于 Set, 而不关心它的顺序 --   否则应该使用List)   。


3 基本特征介绍


3.1 接口 list set map介绍与简单对比

List:关心的是顺序,它保证维护元素特定的顺序(允许有相同元素),使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在   List 中的位置,类似于数组下标)来访问  List 中的元素。即可以通过下标   (1,2..)   来取得值,值可以重复。

Set:仅接受一次,并做内部排序。只关心某元素是否属于   Set (不允许有相同元素),而不关心它的顺序。只能通过游标来取值,并且值是不能重复的。

Map: 最大的特点是键值映射,且为一一映射,键不能重复,值可以,所以是用键来索引值。  方法   put(Object key, Object value)   添 加一个"值"   (   想要得东西   )   和与"值"相关联的"键"  (key) (   使用它来查找   )   。方法   get(Object key)   返回与给定"键"相关联的"值" Map同样对每个元素保存一份,但这是基于   "   键   "   的,   Map   也有内置的排序,因而不关心元素添加的顺序。

简单来说 如果是注重顺序,优选实现了list接口的容器。注重键值管理,优选实现了map接口的容器,如果是单纯集合处理 就选实现了set接口的容器。


3.2 list set map实现类的介绍与简单对比


(1)简单对比


初级实现类底层实现特点同步安全性LinkedList链表插入删除没有同步线程不安全ArrayList数组随机访问没有同步线程不安全Vector数组线程安全同步线程安全HashSetHashMapSet中效率高没有同步线程不安全TreeSetTreeMap固定的顺序没有同步线程不安全Hashtablevector不能存储null值 有初始容量和负载因子同步线程安全HashMap哈希表可以存储null没有同步线程不安全TreeMap红黑树总处于平衡状态没有同步线程不安全IdentityHashMap没看到源码==   代 替   equals()解决特殊问题 
没有同步线程不安全


4 常用类的详细说明


4.1 继承list或list子类的常用类

List(interface): 次序是List最重要的特点;它确保维护元素特定的顺序。List为Collection添加了许多方法,使得能够向List中间插入与移除元素(只推荐LinkedList使用)。一个List可以生成ListIterator,使用它可以从两个方向遍历List,也可以从List中间插入和删除元素。


ArrayList: 由数组实现的List。它允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。ListIterator只应该用来由后向前遍历ArrayList,而不是用来插入和删除元素,因为这比LinkedList开销要大很多。 LinkedList: 由列表实现的List。对顺序访问进行了优化,向List中间插入与删除得开销不大,随机访问则相对较慢(可用ArrayList代替)。它具有方法addFirst()、addLast()、getFirst()、getLast()、removeFirst()、removeLast(),这些方法(没有在任何接口或基类中定义过)使得LinkedList可以当作堆栈、队列和双向队列使用。


4.2 继承Set接口的常用类

Set(interface): 存入Set的每个元素必须是唯一的,这也是与List不同的,因为Set不保存重复元素。加入Set的Object必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。

HashSet:HashSet能快速定位一个元素,存入HashSet的对象必须定义hashCode()。

TreeSet: 保持次序的Set,底层为树结构。使用它可以从Set中提取有序的序列。

LinkedHashSet: 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。

HashSet采用散列函数对元素进行排序,这是专门为快速查询而设计的;TreeSet采用红黑树的数据结构进行排序元素。

LinkedHashSet内部使用散列以加快查询速度,同时使用链表维护元素的次序,使得看起来元素是以插入的顺序保存的。需要注意的是,生成自己的类时,Set需要维护元素的存储顺序,因此要实现Comparable接口并定义compareTo()方法


4.3 继承Map接口或Map子类常用类

请注意,Map没有继承Collection接口,Map提供key到 value的映射,你可以通过"键"查找"值"。一个Map中不能包含相同的key,每个key只能映射一个value。Map接口提供3 种集合的视图,Map的内容可以被当作一组key集合、一组value集合或者一组key-value映射。


方法put(Object key, Object value)添加一个"值"(想要得东西)和与"值"相关联的"键" (key) (使用它来查找) 。方法get(Object key)返回与给定"键"相关联的"值"。可以用 containsKey()和containsValue()测试Map中是否包含某个"键"或"值"。标准的Java 类库中包含了几种不同的 Map:HashMap、TreeMap、 LinkedHashMap、WeakHashMap和IdentityHashMap。它们都有同样的基本接口Map,但是行为、效率、排序策略、保存对象的生命周期和判定"键"等价的策略等各不相同。Map同样对每个元素保存一份,但这是基于 "键"的,Map也有内置的排序,因而不关心元素添加的顺序。如果添加元素的顺序对你很重要,应该使用LinkedHashSet或者LinkedHashMap。


执行效率是Map的一个大问题。看看get()要做哪些事,就会明白为什么在ArrayList中搜索"键"是相当慢的,而这正是HashMap提高速度的地方。HashMap使用了特殊的值,称为"散列码"(hash code),来取代对键的缓慢搜索。"散列码"是"相对唯一"用以代表对象的int 值,它是通过将该对象的某些信息进行转换而生成的(在下面总结二:需要的注意的地方有更进一步探讨)。所有Java对象都能产生散列码,因为hashCode()是定义在基类Object中的方法。HashMap就是使用对象的hashCode()进行快速查询的,此方法能够显著提高性能。Hashtable类    Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null )的对象都可作为key或者value。添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。         Hashtable通过初始化容量(initial capacity)和负载因子(load factor)两个参数调整性能。通常缺省的load factor 0.75 较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。          


使用Hashtable的简单示例如下,将1、2、3放到Hashtable中,他们的key分别是"one"、"two"、"three":        Hashtable numbers = new Hashtable();    numbers.put("one", new Integer(1));    numbers.put("two", new Integer(2));    numbers.put("three", new Integer(3)); 要取出一个数,比如2,用相应的key:       Integer n = (Integer)numbers.get("two");    System.out.println("two = " + n); 由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode方法和equals方法。hashCode方法和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希表的操作。    


如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。Hashtable是同步的。   HashMap类    HashMap和Hashtable类似,也是基于散列表的实现。不同之处在于HashMap是非同步的,并且允许null,即null value和null key。将HashMap视为Collection时(values()方法可返回Collection),插入和查询"键值对"的开销是固定的,但其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量(initial capacity)设得过高,或者负载因子(load factor)过低。 LinkedHashMap类:类似于HashMap,但是迭代遍历它时,取得"键值对"的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时反而更快,因为它使用链表维护内部次序。


WeakHashMap 类:弱键(weak key)Map是一种改进的 HashMap,它是为解决特殊问题设计的,对key 实行"弱引用 ",如果一个key不再被外部所引用(没有map之外的引用),那么该key可以被垃圾收集器(GC)回收。

TreeMap类

基于红黑树数据结构的实现。查看"键"或"键值对"时,它们会被排序(次序由Comparabel或Comparator决定)。TreeMap的特点在于:你得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。  

IdentityHashMap类

使用"=="代替equals()对"键"作比较的hashmap。专为解决特殊问题而设计。


5 小结


5.1 比较

数组 (Array),数组类(Arrays)

Java所有"存储及随机访问一连串对象"的做法,array是最有效率的一种。但缺点是容量固定且无法动态改变。array还有一个缺点是,无法判断其中实际存有多少元素, length只是告诉我们array的容量。Java中有一个数组类(Arrays),专门用来操作array。数组类(arrays)中拥有一组 static函数。

equals():比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。

fill():将值填入array中。

sort():用来对array进行排序。

binarySearch():在排好序的array中寻找元素。

System.arraycopy():array的复制。

若编写程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,则array不适用。

  • 容器类与数组的区别

容器类仅能持有对象引用(指向对象的指针),而不是将对象信息copy一份至数列某位置。一旦将对象置入容器内,便损失了该对象的型别信息。

  • 容器   (Collection)与Map的联系与区别

Collection类型,每个位置只有一个元素。

Map类型,持有key-value对(pair),像个小型数据库。

Collections是针对集合类的一个帮助类。提供了一系列静态方法实现对各种集合的搜索、排序、线程完全化等操作。相当于对Array进行类似操作的类------Arrays。

如,Collections.max(Collection coll);  取coll中最大的元素

Collections.sort(List list);  对list中元素排序

List,Set,Map将持有对象一律视为Object 型别。Collection、List、Set、Map都是接口,不能实例化。继承自它们的ArrayList, Vector, HashTable, HashMap是具象class,这些才可被实例化。

vector容器确切知道它所持有的对象隶属什么型别。vector不进行边界检查。


5.2 需要注意的地方

1、Collection只能通过iterator()遍历元素,没有get()方法来取得某个元素。

2、Set和Collection拥有一模一样的接口。但排除掉传入的Collection参数重复的元素。

3、List可以通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get)

4、Map用put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value   。

HashMap会利用对象的hashCode来快速找到key。哈希码   (hashing)   就是将对象的信息经过一些转变形成一个独一无二的int值,这个值存储在一个array中。我们都知道所有存储结构中,array查找速度是最快的。所以,可以加速查找。发生碰撞时,让array指向多个values。即,数组每个位置上又生成一个梿表。

5、Map中元素,可以将key序列、value序列单独抽取出来。

使用keySet()抽取key序列,将map中的所有keys生成一个Set。

使用values()抽取value序列,将map中的所有values生成一个Collection。

为什么一个生成Set,一个生成Collection?那是因为,key总是独一无二的,value允许重复。


5.3 如何选择  

从效率角度:

在各种Lists中,对于需要快速插入,删除元素,应该使用LinkedList(可用LinkedList构造堆栈stack、队列queue),如果需要快速随机访问元素,应该使用ArrayList。最好的做法是以ArrayList作为缺省选择。Vector总是比ArrayList慢,所以要尽量避免使用。

在各种Sets中,HashSet通常优于TreeSet(插入、查找)。只有当需要产生一个经过排序的序列,才用TreeSet。TreeSet存在的唯一理由:能够维护其内元素的排序状态。

在各种Maps中HashMap用于快速查找。HashMap:适用于在Map中插入、删除和定位元素。 TreeMap:适用于按自然顺序或自定义顺序遍历键(key)。HashMap通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,在需要排序的Map时候才用TreeMap。

最后,当元素个数固定,用Array,因为Array效率是最高的。所以结论:最常用的ArrayList,HashSet,HashMap, Array。


5.4 更进一步分析:

如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。要特别注意对哈希表的操作,作为key的对象要同时正确复写equals方法和hashCode方法。尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。如果添加元素的顺序对你很重要,应该使用LinkedHashSet或者LinkedHashMap。

例外 也可以在没有同步的HashMap使用Collections类的静态的synchronizedMap()方法,它创建一个线程安全的Map对象,并把它作为一个封装的对象来返回。这个对象的方法可以让你同步访问潜在的HashMap。这么做的结果就是当你不需要同步时,你不能切断Hashtable中的同步(比如在一个单线程的应用程序中),而且同步增加了很多处理费用


5.5 测试

选有代表性的容器测试 HashMap 和 TreeMap

import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; public class HashMaps { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("a", "aaa"); map.put("b", "bbb"); map.put("c", "ccc"); map.put("d", "ddd"); Iterator<String> iterator = map.keySet().iterator(); while (iterator.hasNext()) { Object key = iterator.next(); System.out.println("map.get(key) is :" + map.get(key)); } // 定义HashTable,用来测试 Hashtable<String, String> tab = new Hashtable<String, String>(); tab.put("a", "aaa"); tab.put("b", "bbb"); tab.put("c", "ccc"); tab.put("d", "ddd"); Iterator<String> iterator_1 = tab.keySet().iterator(); while (iterator_1.hasNext()) { Object key = iterator_1.next(); System.out.println("tab.get(key) is :" + tab.get(key)); } TreeMap<String, String> tmp = new TreeMap<String, String>(); tmp.put("a", "aaa");  tmp.put("b", "bbb"); tmp.put("c", "ccc"); tmp.put("d", "ddd"); Iterator<String> iterator_2 = tmp.keySet().iterator(); while (iterator_2.hasNext()) { Object key = iterator_2.next(); System.out.println("tmp.get(key) is :" + tmp.get(key)); } } }

测试结果 如下 HashMap 输出无序,TreeMap输出有序,Map 是堆栈结构。

map.get(key) is :ddd

map.get(key) is :bbb

map.get(key) is :ccc

map.get(key) is :aaa

tab.get(key) is :bbb

tab.get(key) is :aaa

tab.get(key) is :ddd

tab.get(key) is :ccc

tmp.get(key) is :aaa

tmp.get(key) is :bbb

tmp.get(key) is :ccc

tmp.get(key) is :ddd


6 扩展了解


6.1 关于Properties

Java.util.Properties类是Hashtable的一个子类,设计用于String keys和values。Properties对象的用法同Hashtable的用法相象,但是类增加了两个节省时间的方法,你应该知道。

Store()方法把一个Properties对象的内容以一种可读的形式保存到一个文件中。Load()方法正好相反,用来读取文件,并设定Properties对象来包含keys和values。

注意,因为Properties扩展了Hashtable,你可以用超类的put()方法来添加不是String对象的keys和values。这是不可取的。另外,如果你将store()用于一个不包含String对象的Properties对象,store()将失败。作为put()和get()的替代,你应该用setProperty()和getProperty(),它们用String参数。


6.2 关于Element的HashCode

Element的HashCode方法继承自Object,而Object中的HashCode方法返回的HashCode对应于当前的地址,也就是说对于不同的对象,即使它们的内容完全相同,用HashCode()返回的值也会不同。这样实际上违背了我们的意图。因为我们在使用 HashMap时,希望利用相同内容的对象索引得到相同的目标对象,这就需要HashCode()在此时能够返回相同的值。在上面的例子中,我们期望 new Element (i=5)与 Elementtest=newElement(5)是相同的,而实际上这是两个不同的对象,尽管它们的内容相同,但它们在内存中的地址不同。


6.3 红黑树

红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,他称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。红黑树的持久版本对每次插入或删除需要O(log n)的空间。


7 总结


1 数据容器都离不开基本数据结构的支持。

2 选择已有容器时,先区分是需要用到的set map(对应于群论 和 键值)。

3 确定数据操作的主要瓶颈和需求。即那种操作最常见,那种操作必须满足什么要求,选择满足必须要求时,最优与常见操作的数据容器。

数组可以保存基本类型 而容器不行 容器可以放置任何java对象。

(基本类型 可以使用java包装类将其作为常量包装后存入容器。或者用你自己的类将其变量包装起来存入容器 比如数组封装 http://cping1982.blog.51cto.com/601635/130062/ )

原创粉丝点击