java集合详解(转自楼道小组)

 

 

 

 

 

 

 

集合 html

 

版本号:1.0 java

 

 

 

 

 

 

 

 

 

做者:huangdos 程序员

 日期: 2006 6 06 算法


    数据库

 

摘要内容 编程

Java里面最重要,最经常使用也就是集会一部分了。可以用好集合和理解好集合对于作Java程序的开发拥有无比的好处。本文详细解释了关于Java中的集合是如何实现的,以及他们的实现原理。 设计模式

 

关键字: 数组

Collection , List ,Set , Map , 集合,框架。安全



集合

1         集合框架

1.1         集合框架概述

1.1.1         容器简介

到目前为止,咱们已经学习了如何建立多个不一样的对象,定义了这些对象之后,咱们就能够利用它们来作一些有意义的事情。

       举例来讲,假设要存储许多雇员,不一样的雇员的区别仅在于雇员的身份证号。咱们能够经过身份证号来顺序存储每一个雇员,可是在内存中实现呢?是否是要准备足够的内存来存储1000个雇员,而后再将这些雇员逐一插入?若是已经插入了500条记录,这时须要插入一个身份证号较低的新雇员,该怎么办呢?是在内存中将500条记录所有下移后,再从开头插入新的记录? 仍是建立一个映射来记住每一个对象的位置?当决定如何存储对象的集合时,必须考虑以下问题。

       对于对象集合,必须执行的操做主要如下三种:

u       添加新的对象

u       删除对象

u       查找对象

咱们必须肯定如何将新的对象添加到集合中。能够将对象添加到集合的末尾、开头或者中间的某个逻辑位置。

从集合中删除一个对象后,对象集合中现有对象会有什么影响呢?可能必须将内存移来移去,或者就在现有对象所驻留的内存位置下一个“洞”。

       在内存中创建对象集合后,必须肯定如何定位特定对象。可创建一种机制,利用该机制可根据某些搜索条件(例如身份证号)直接定位到目标对象;不然,便须要遍历集合中的每一个对象,直到找到要查找的对象为止。

       前面你们已经学习过了数组。数组的做用是能够存取一组数据。可是它却存在一些缺点,使得没法使用它来比较方便快捷的完成上述应用场景的要求。

1.         首先,在不少数状况下面,咱们须要可以存储一组数据的容器,这一点虽然数组能够实现,可是若是咱们须要存储的数据的个数多少并不肯定。好比说:咱们须要在容器里面存储某个应用系统的当前的全部的在线用户信息,而当前的在线用户信息是时刻均可能在变化的。 也就是说,咱们须要一种存储数据的容器,它可以自动的改变这个容器的所能存放的数据数量的大小。这一点上,若是使用数组来存储的话,就显得十分的笨拙。

2.         咱们再假设这样一种场景:假定一个购物网站,通过一段时间的运行,咱们已经存储了一系列的购物清单了,购物清单中有商品信息。若是咱们想要知道这段时间里面有多少种商品被销售出去了。那么咱们就须要一个容器可以自动的过滤掉购物清单中的关于商品的重复信息。若是使用数组,这也是很难实现的。

3.         最后再想一想,咱们常常会遇到这种状况,我知道某我的的账号名称,但愿可以进一步了解这我的的其余的一些信息。也就是说,咱们在一个地方存放一些用户信息,咱们但愿可以经过用户的账号来查找到对应的该用户的其余的一些信息。再举个查字典例子:假设咱们但愿使用一个容器来存放单词以及对于这个单词的解释,而当咱们想要查找某个单词的意思的时候,可以根据提供的单词在这个容器中找到对应的单词的解释。若是使用数组来实现的话,就更加的困难了。

为解决这些问题,Java里面就设计了容器集合,不一样的容器集合以不一样的格式保存对象。

 

数学背景

在常见用法中,集合(collection和数学上直观的集(set的概念是相同的。集是一个惟一项组,也就是说组中没有重复项。实际上,集合框架包含了一个 Set 接口和许多具体的 Set 类。但正式的集概念却比 Java 技术提早了一个世纪,那时英国数学家 George Boole 按逻辑正式的定义了集的概念。大部分人在小学时经过咱们熟悉的维恩图引入的集的交集的并学到过一些集的理论。 

集的基本属性以下:

u       集内只包含每项的一个实例

u       集能够是有限的,也能够是无限的

u       能够定义抽象概念

集不只是逻辑学、数学和计算机科学的基础,对于商业和系统的平常应用来讲,它也很实用。链接池这一律念就是数据库服务器的一个开放链接集。Web 服务器必须管理客户机和链接集。文件描述符提供了操做系统中另外一个集的示例。

映射是一种特别的集。它是一种对(pair)集,每一个对表示一个元素到另外一元素的单向映射。一些映射示例有:

u       IP 地址到域名(DNS)的映射

u       关键字到数据库记录的映射

u       字典(词到含义的映射)

u       2 进制到 10 进制转换的映射

就像集同样,映射背后的思想比 Java 编程语言早的多,甚至比计算机科学还早。而Java中的Map 就是映射的一种表现形式。

1.1.2        容器的分类

既然您已经具有了一些集的理论,您应该可以更轻松的理解集合框架集合框架由一组用来操做对象的接口组成。不一样接口描述不一样类型的组。在很大程度上,一旦您理解了接口,您就理解了框架。虽然您总要建立接口特定的实现,但访问实际集合的方法应该限制在接口方法的使用上;所以,容许您更改基本的数据结构而没必要改变其它代码。框架接口层次结构以下图所示。

 

Java容器类类库的用途是“保存对象”,并将其划分为两个不一样的概念:

1)  Collection 一组对立的元素,一般这些元素都服从某种规则。List必须保持元素特定的顺序,而Set 不能有重复元素。

2)  Map 一组 成对的“键值对”对象。初看起来这彷佛应该是一个Collection ,其元素是成对的对象,可是这样的设计实现起来太笨拙了,因而咱们将Map明确的提取出来造成一个独立的概念。另外一方面,若是使用Collection 表示Map的部份内容,会便于查看此部份内容。所以Map同样容易扩展成多维Map ,无需增长新的概念,只要让Map中的键值对的每一个“值”也是一个Map便可。

CollectionMap的区别在于容器中每一个位置保存的元素个数。Collection 每一个位置只能保存一个元素(对象)。此类容器包括:List ,它以特定的顺序保存一组元素;Set 则是元素不能重复。

Map保存的是“键值对”,就像一个小型数据库。咱们能够经过“键”找到该键对应的“值”。

u     Collection 对象之间没有指定的顺序,容许重复元素。

u     Set   对象之间没有指定的顺序,不容许重复元素

u     List  对象之间有指定的顺序,容许重复元素,并引入位置下标。

u     Map   接口用于保存关键字(Key)和数值(Value)的集合,集合中的每一个对象加入时都提供数值和关键字。Map 接口既不继承 Set 也不继承 Collection

 

ListSetMap共同的实现基础是Object数组

除了四个历史集合类外,Java 2 框架还引入了六个集合实现,以下表所示。

 

 

接口

实现

历史集合类

Set

HashSet

 

 

TreeSet

 

List

ArrayList

Vector

 

LinkedList

Stack

Map

HashMap

Hashtable

 

TreeMap

Properties

 

这里没有 Collection 接口的实现,接下来咱们再来看一下下面的这张关于集合框架的大图:

       这张图看起来有点吓人,熟悉以后就会发现其实只有三种容器:MapListSet ,它们各自有两个三个实现版本。经常使用的容器用黑色粗线框表示。

点线方框表明“接口”,虚线方框表明抽象类,而实线方框表明普通类(即具体类,而非抽象类)。虚线箭头指出一个特定的类实现了一个接口(在抽象类的状况下,则是“部分”实现了那个接口)。实线箭头指出一个类可生成箭头指向的那个类的对象。例如任何集合( Collection )都能产生一个迭代器( Iterator ),而一个List 除了能生成一个ListIterator 列表迭代器)外,还能生成一个普通迭代器,由于List 正是从集合继承来的.

 

 

1.2         Collection

1.2.1         经常使用方法

Collection 接口用于表示任何对象或元素组。想要尽量以常规方式处理一组元素时,就使用这一接口。Collection 在前面的大图也能够看出,它是ListSet 的父类。而且它自己也是一个接口。它定义了做为集合所应该拥有的一些方法。以下:

 

注意:

集合必须只有对象,集合中的元素不能是基本数据类型。

Collection接口支持如添加和除去等基本操做。设法除去一个元素时,若是这个元素存在,除去的仅仅是集合中此元素的一个实例。

u     boolean add(Object element)

u     boolean remove(Object element)

Collection 接口还支持查询操做:

u     int size()

u     boolean isEmpty()

u     boolean contains(Object element)

u     Iterator iterator()

组操做 Collection 接口支持的其它操做,要么是做用于元素组的任务,要么是同时做用于整个集合的任务。

u     boolean containsAll(Collection collection)

u     boolean addAll(Collection collection)

u     void clear()

u     void removeAll(Collection collection)

u     void retainAll(Collection collection)

containsAll() 方法容许您查找当前集合是否包含了另外一个集合的全部元素,即另外一个集合是不是当前集合的子集。其他方法是可选的,由于特定的集合可能不支持集合更改。 addAll() 方法确保另外一个集合中的全部元素都被添加到当前的集合中,一般称为 clear() 方法从当前集合中除去全部元素。 removeAll() 方法相似于 clear() ,但只除去了元素的一个子集。 retainAll() 方法相似于 removeAll() 方法,不过可能感到它所作的与前面正好相反:它从当前集合中除去不属于另外一个集合的元素,即

 

咱们看一个简单的例子,来了解一下集合类的基本方法的使用:

import java.util.*;

public class CollectionToArray {

    public static void main(String[] args) {     

       Collection collection1=new ArrayList();//建立一个集合对象

       collection1.add("000");//添加对象到Collection集合中

       collection1.add("111");

       collection1.add("222");

       System.out.println("集合collection1的大小:"+collection1.size());

       System.out.println("集合collection1的内容:"+collection1);

       collection1.remove("000");//从集合collection1中移除掉 "000" 这个对象

       System.out.println("集合collection1移除 000 后的内容:"+collection1);

       System.out.println("集合collection1中是否包含000 "+collection1.contains("000"));

       System.out.println("集合collection1中是否包含111 "+collection1.contains("111"));

       Collection collection2=new ArrayList();

       collection2.addAll(collection1);//collection1 集合中的元素所有都加到collection2

       System.out.println("集合collection2的内容:"+collection2);

       collection2.clear();//清空集合 collection1 中的元素

       System.out.println("集合collection2是否为空 "+collection2.isEmpty());

       //将集合collection1转化为数组

       Object s[]= collection1.toArray();

       for(int i=0;i<s.length;i++){

           System.out.println(s[i]);

       }

    }

}

运行结果为:

集合collection1的大小:3

集合collection1的内容:[000, 111, 222]

集合collection1移除 000 后的内容:[111, 222]

集合collection1中是否包含000 false

集合collection1中是否包含111 true

集合collection2的内容:[111, 222]

集合collection2是否为空 true

111

222

 

这里须要注意的是,Collection 它仅仅只是一个接口,而咱们真正使用的时候,确是建立该接口的一个实现类。作为集合的接口,它定义了全部属于集合的类所都应该具备的一些方法。

ArrayList (列表)类是集合类的一种实现方式。

 

这里须要一提的是,由于Collection的实现基础是数组,因此有转换为Object数组的方法:

u     Object[] toArray()

u     Object[] toArray(Object[] a)

其中第二个方法Object[] toArray(Object[] a) 的参数 a 应该是集合中全部存放的对象的类的父类。

 

1.2.2         迭代器

任何容器类,都必须有某种方式能够将东西放进去,而后由某种方式将东西取出来。毕竟,存放事物是容器最基本的工做。对于ArrayListadd()是插入对象的方法,而get()是取出元素的方式之一。ArrayList很灵活,能够随时选取任意的元素,或使用不一样的下标一次选取多个元素。

若是从更高层的角度思考,会发现这里有一个缺点:要使用容器,必须知道其中元素的确切类型。初看起来这没有什么很差的,可是考虑以下状况:若是本来是ArrayList ,可是后来考虑到容器的特色,你想换用Set ,应该怎么作?或者你打算写通用的代码,它们只是使用容器,不知道或者说不关心容器的类型,那么如何才能不重写代码就能够应用于不一样类型的容器?

       因此迭代器(Iterator)的概念,也是出于一种设计模式就是为达成此目的而造成的。因此Collection不提供get()方法。若是要遍历Collectin中的元素,就必须用Iterator

       迭代器(Iterator)自己就是一个对象,它的工做就是遍历并选择集合序列中的对象,而客户端的程序员没必要知道或关心该序列底层的结构。此外,迭代器一般被称为“轻量级”对象,建立它的代价小。可是,它也有一些限制,例如,某些迭代器只能单向移动。

Collection 接口的 iterator() 方法返回一个 IteratorIterator 和您可能已经熟悉的 Enumeration 接口相似。使用 Iterator 接口方法,您能够从头到尾遍历集合,并安全的从底层 Collection 中除去元素。

 

 

 

下面,咱们看一个对于迭代器的简单使用:

 

import java.util.ArrayList;

import java.util.Collection;

import java.util.Iterator;

 

public class IteratorDemo {

    public static void main(String[] args) {

       Collection collection = new ArrayList();

       collection.add("s1");

       collection.add("s2");

       collection.add("s3");

       Iterator iterator = collection.iterator();//获得一个迭代器

       while (iterator.hasNext()) {//遍历

           Object element = iterator.next();

           System.out.println("iterator = " + element);

       }

       if(collection.isEmpty())

           System.out.println("collection is Empty!");

       else

           System.out.println("collection is not Empty! size="+collection.size());

       Iterator iterator2 = collection.iterator();

       while (iterator2.hasNext()) {//移除元素

           Object element = iterator2.next();

           System.out.println("remove: "+element);

           iterator2.remove();

       }     

       Iterator iterator3 = collection.iterator();

       if (!iterator3.hasNext()) {//察看是否还有元素

           System.out.println("还有元素");

       }  

       if(collection.isEmpty())

           System.out.println("collection is Empty!");

       //使用collection.isEmpty()方法来判断

    }

}

程序的运行结果为:

iterator = s1

iterator = s2

iterator = s3

collection is not Empty! size=3

remove: s1

remove: s2

remove: s3

还有元素

collection is Empty!

 

能够看到,JavaCollectionIterator 可以用来,:

1)      使用方法 iterator() 要求容器返回一个Iterator .第一次调用Iterator next() 方法时,它返回集合序列的第一个元素。

2)      使用next() 得到集合序列的中的下一个元素。

3)      使用hasNext()检查序列中是否元素。

4)      使用remove()将迭代器新返回的元素删除。

须要注意的是:方法删除由next方法返回的最后一个元素,在每次调用next时,remove方法只能被调用一次

你们看,Java 实现的这个迭代器的使用就是如此的简单。Iterator(跌代器)虽然功能简单,但仍然能够帮助咱们解决许多问题,同时针对List 还有一个更复杂更高级的ListIterator。您能够在下面的List讲解中获得进一步的介绍。

 

1.3         List

1.3.1        概述

前面咱们讲述的Collection接口实际上并无直接的实现类。而List是容器的一种,表示列表的意思。当咱们不知道存储的数据有多少的状况,咱们就可使用List 来完成存储数据的工做。例如前面提到的一种场景。咱们想要在保存一个应用系统当前的在线用户的信息。咱们就可使用一个List来存储。由于List的最大的特色就是可以自动的根据插入的数据量来动态改变容器的大小。下面咱们先看看List接口的一些经常使用方法。

1.3.2         经常使用方法

List 就是列表的意思,它是Collection 的一种,继承了 Collection 接口,以定义一个容许重复项的有序集合。该接口不但可以对列表的一部分进行处理,还添加了面向位置的操做。List 是按对象的进入顺序进行保存对象,而不作排序或编辑操做。它除了拥有Collection接口的全部的方法外还拥有一些其余的方法。

面向位置的操做包括插入某个元素或 Collection 的功能,还包括获取、除去或更改元素的功能。在 List 中搜索元素能够从列表的头部或尾部开始,若是找到元素,还将报告元素所在的位置。

u       void add(int index, Object element) :添加对象element到位置index

u       boolean addAll(int index, Collection collection) :在index位置后添加容器collection中全部的元素

u       Object get(int index) :取出下标为index的位置的元素

u       int indexOf(Object element) :查找对象element List中第一次出现的位置

u       int lastIndexOf(Object element) :查找对象element List中最后出现的位置

u       Object remove(int index) :删除index位置上的元素

u       Object set(int index, Object element) :将index位置上的对象替换为element 并返回老的元素。

 

 

先看一下下面表格:

 

 

简述

实现

操做特性

成员要求

List

提供基于索引的对成员的随机访问

ArrayList

提供快速的基于索引的成员访问,对尾部成员的增长和删除支持较好

成员可为任意Object子类的对象

LinkedList

对列表中任何位置的成员的增长和删除支持较好,但对基于索引的成员访问支持性能较差

成员可为任意Object子类的对象

集合框架中有两种常规的 List 实现:ArrayList LinkedList。使用两种 List 实现的哪种取决于您特定的须要。若是要支持随机访问,而没必要在除尾部的任何位置插入或除去元素,那么,ArrayList 提供了可选的集合。但若是,您要频繁的从列表的中间位置添加和除去元素,而只要顺序的访问列表元素,那么,LinkedList 实现更好。

 

咱们以ArrayList 为例,先看一个简单的例子:

例子中,咱们把12个月份存放到ArrayList 中,而后用一个循环,并使用get()方法将列表中的对象都取出来。

LinkedList 添加了一些处理列表两端元素的方法(下图只显示了新方法):

使用这些新方法,您就能够轻松的把 LinkedList 看成一个堆栈、队列或其它面向端点的数据结构。

咱们再来看另一个使用LinkedList 来实现一个简单的队列的例子:

import java.util.*;

 

public class ListExample {

  public static void main(String args[]) {

    LinkedList queue = new LinkedList();

    queue.addFirst("Bernadine");

    queue.addFirst(" Elizabeth ");

    queue.addFirst("Gene");

    queue.addFirst(" Elizabeth ");

    queue.addFirst("Clara");

    System.out.println(queue);

    queue.removeLast();

    queue.removeLast();

    System.out.println(queue);

  }

}

运行程序产生了如下输出。请注意,与 Set 不一样的是 List 容许重复。

[Clara, Elizabeth, Gene, Elizabeth, Bernadine] 
 
  

  
  
  
  [Clara, Elizabeth, Gene]

该的程序演示了具体 List 类的使用。第一部分,建立一个由 ArrayList 支持的 List。填充完列表之后,特定条目就获得了。示例的 LinkedList 部分把 LinkedList 看成一个队列,从队列头部添加东西,从尾部除去。

List 接口不但以位置友好的方式遍历整个列表,还能处理集合的子集:

u       ListIterator listIterator() :返回一个ListIterator 跌代器,默认开始位置为0

u       ListIterator listIterator(int startIndex) :返回一个ListIterator 跌代器,开始位置为startIndex

u       List subList(int fromIndex, int toIndex) :返回一个子列表List ,元素存放为从 fromIndex toIndex以前的一个元素。

处理 subList() 时,位于 fromIndex 的元素在子列表中,而位于 toIndex 的元素则不是,提醒这一点很重要。如下 for-loop 测试案例大体反映了这一点:

for (int i=fromIndex; i<toIndex; i++) { 
 
  
  // process element at position i 
 
  

  
  
  
  }

此外,咱们还应该提醒的是:对子列表的更改(如 add()remove() set() 调用)对底层 List 也有影响。

ListIterator 接口

ListIterator 接口继承 Iterator 接口以支持添加或更改底层集合中的元素,还支持双向访问。

如下源代码演示了列表中的反向循环。请注意 ListIterator 最初位于列表尾以后(list.size()),由于第一个元素的下标是0

List list = ...; 
 
  
ListIterator iterator = list.listIterator(list.size()); 
 
  
while (iterator.hasPrevious()) { 
 
  
  Object element = iterator.previous(); 
 
  
  // Process element 
 
  

  
  
  
  }

正常状况下,不用 ListIterator 改变某次遍历集合元素的方向向前或者向后。虽然在技术上可能实现时,但在 previous() 后马上调用 next(),返回的是同一个元素。把调用 next() previous() 的顺序颠倒一下,结果相同。

咱们看一个List的例子:

 

import java.util.*;

 

public class ListIteratorTest {

 

    public static void main(String[] args) {

       List list = new ArrayList();

       list.add("aaa");

       list.add("bbb");

       list.add("ccc");

       list.add("ddd"); 

       System.out.println("下标0开始:"+list.listIterator(0).next());//next()

       System.out.println("下标1开始:"+list.listIterator(1).next());

       System.out.println("List 1-3:"+list.subList(1,3));//子列表

       ListIterator it = list.listIterator();//默认从下标0开始

       //隐式光标属性add操做 ,插入到当前的下标的前面

       it.add("sss");

       while(it.hasNext()){

           System.out.println("next Index="+it.nextIndex()+",Object="+it.next());

       }     

       //set属性

       ListIterator it1 = list.listIterator();

       it1.next();

       it1.set("ooo");

       ListIterator it2 = list.listIterator(list.size());//下标

       while(it2.hasPrevious()){

           System.out.println("previous Index="+it2.previousIndex()+",Object="+it2.previous());

       }

    }

}

程序的执行结果为:

下标0开始:aaa

下标1开始:bbb

List 1-3:[bbb, ccc]

next Index=1,Object=aaa

next Index=2,Object=bbb

next Index=3,Object=ccc

next Index=4,Object=ddd

previous Index=4,Object=ddd

previous Index=3,Object=ccc

previous Index=2,Object=bbb

previous Index=1,Object=aaa

previous Index=0,Object=ooo

咱们还须要稍微再解释一下 add() 操做。添加一个元素会致使新元素马上被添加到隐式光标的前面。所以,添加元素后调用 previous() 会返回新元素,而调用 next() 则不起做用,返回添加操做以前的下一个元素。下标的显示方式,以下图所示:

对于List 的基本用法咱们学会了,下面咱们来进一步了解一下List的实现原理,以便价升咱们对于集合的理解。

1.3.3         实现原理

前面已经提了一下Collection的实现基础都是基于数组的。下面咱们就已ArrayList 为例,简单分析一下ArrayList 列表的实现方式。首先,先看下它的构造函数。

下列表格是在SUN提供的API中的描述:

 

ArrayList()           Constructs an empty list with an initial capacity of ten.

ArrayList(Collection c)           Constructs a list containing the elements of the specified collection, in the order they are returned by the collection's iterator.

ArrayList(int initialCapacity)           Constructs an empty list with the specified initial capacity.

其中第一个构造函数ArrayList()和第二构造函数ArrayList(Collection c) 是按照Collection 接口文档所述,所应该提供两个构造函数,一个无参数,一个接受另外一个 Collection

3个构造函数:

ArrayList(int initialCapacity) ArrayList实现的比较重要的构造函数,虽然,咱们不经常使用它,可是某认的构造函数正是调用的该带参数:initialCapacity 的构造函数来实现的。 其中参数:initialCapacity 表示咱们构造的这个ArrayList列表的初始化容量是多大。若是调用默认的构造函数,则表示默认调用该参数为initialCapacity =10 的方式,来进行构建一个ArrayList列表对象。

为了更好的理解这个initialCapacity 参数的概念,咱们先看看ArrayListSun 提供的源码中的实现方式。先看一下它的属性有哪些:

ArrayList 继承了AbstractList 咱们主要看看ArrayList中的属性就能够了。

ArrayList中主要包含2个属性:

u       private transient Object elementData[];

u       private int size;

其中数组:elementData[] 是列表的实现核心属性:数组。 咱们使用该数组来进行存放集合中的数据。而咱们的初始化参数就是该数组构建时候的长度,即该数组的length属性就是initialCapacity 参数。

Keystransient 表示被修饰的属性不是对象持久状态的一部分,不会自动的序列化。

2个属性:size表示列表中真实数据的存放个数。

咱们再来看一下ArrayList的构造函数,加深一下ArrayList是基于数组的理解。

从源码中能够看到默认的构造函数调用的就是带参数的构造函数: 

public ArrayList(int initialCapacity)

不过参数initialCapacity10 。

咱们主要看ArrayList(int initialCapacity) 这个构造函数。能够看到:

this.elementData = new Object[initialCapacity];

咱们就是使用的initialCapacity 这个参数来建立一个Object数组。而咱们全部的往该集合对象中存放的数据,就是存放到了这个Object数组中去了。

咱们在看看另一个构造函数的源码:

这里,咱们先看size() 方法的实现形式。它的做用便是返回size属性值的大小。而后咱们再看另一个构造函数public ArrayList(Collection c) ,该构造函数的做用是把另一个容器对象中的元素存放到当前的List 对象中。

能够看到,首先,咱们是经过调用另一个容器对象C 的方法size()来设置当前的List对象的size属性的长度大小。

    接下来,就是对elementData 数组进行初始化,初始化的大小为原先容器大小的1.1倍。最后,就是经过使用容器接口中的Object[] toArray(Object[] a) 方法来把当前容器中的对象都存放到新的数组elementData 中。这样就完成了一个ArrayList 的创建。

    可能你们会存在一个问题,那就是,咱们创建的这个ArrayList 是使用数组来实现的,可是数组的长度一旦被定下来,就不能改变了。而咱们在给ArrayList对象中添加元素的时候,却没有长度限制。这个时候,ArrayList 中的elementData 属性就必须存在一个须要动态的扩充容量的机制。咱们看下面的代码,它描述了这个扩充机制:

 

这个方法的做用就是用来判断当前的数组是否须要扩容,应该扩容多少。其中属性:modCount是继承自父类,它表示当前的对象对elementData数组进行了多少次扩容,清空,移除等操做。该属性至关因而一个对于当前List 对象的一个操做记录日志号。 咱们主要看下面的代码实现:

1.      首先获得当前elementData 属性的长度oldCapacity

2.      而后经过判断oldCapacityminCapacity参数谁大来决定是否须要扩容

n         若是minCapacity大于oldCapacity,那么咱们就对当前的List对象进行扩容。扩容的的策略为:取(oldCapacity * 3)/2 + 1minCapacity之间更大的那个。而后使用数组拷贝的方法,把之前存放的数据转移到新的数组对象中

n         若是minCapacity不大于oldCapacity那么就不进行扩容。

下面咱们看看上的那个ensureCapacity方法的是如何使用的:

上的两个add方法都是往List 中添加元素。每次在添加元素的时候,咱们就须要判断一下,是否须要对于当前的数组进行扩容。

咱们主要看看  public boolean add(Object o)方法,能够发如今添加一个元素到容器中的时候,首先咱们会判断是否须要扩容。由于只增长一个元素,因此扩容的大小判断也就为当前的size+1来进行判断。而后,就把新添加的元素放到数组elementData中。

第二个方法public boolean addAll(Collection c)也是一样的原理。将新的元素放到elementData数组以后。同时改变当前List 对象的size属性。

    相似的List 中的其余的方法也都是基于数组进行操做的。你们有兴趣能够看看源码中的更多的实现方式。

    最后咱们再看看如何判断在集合中是否已经存在某一个对象的:

由源码中咱们能够看到,public boolean contains(Object elem)方法是经过调用public int indexOf(Object elem)方法来判断是否在集合中存在某个对象elem。咱们看看indexOf方法的具体实现。

u       首先咱们判断一下elem 对象是否为null ,若是为null的话,那么遍历数组elementData 把第一个出现null的位置返回。

u       若是elem不为null 的话,咱们也是遍历数组elementData ,并经过调用elem对象的equals()方法来获得第一个相等的元素的位置。

这里咱们能够发现,ArrayList中用来判断是否包含一个对象,调用的是各个对象本身实现的equals()方法。在前面的高级特性里面,咱们能够知道:若是要判断一个类的一个实例对象是否等于另一个对象,那么咱们就须要本身覆写Object类的public boolean equals(Object obj) 方法。若是不覆写该方法的话,那么就会调用Objectequals()方法来进行判断。这就至关于比较两个对象的内存应用地址是否相等了。

    在集合框架中,不只仅是List,全部的集合类,若是须要判断里面是否存放了的某个对象,都是调用该对象的equals()方法来进行处理的。

1.4         Map

1.4.1        概述

数学中的映射关系在Java中就是经过Map来实现的。它表示,里面存储的元素是一个对(pair,咱们经过一个对象,能够在这个映射关系中找到另一个和这个对象相关的东西。

前面提到的咱们对于根据账号名获得对应的人员的信息,就属于这种状况的应用。咱们讲一我的员的账户名和这人员的信息做了一个映射关系,也就是说,咱们把账户名和人员信息当成了一个“键值对”,“键”就是账户名,“值”就是人员信息。下面咱们先看看Map 接口的经常使用方法。

1.4.2         经常使用方法

Map 接口不是 Collection 接口的继承。而是从本身的用于维护键-值关联的接口层次结构入手。按定义,该接口描述了从不重复的键到值的映射。

咱们能够把这个接口方法分红三组操做:改变、查询和提供可选视图。

改变操做容许您从映射中添加和除去键-值对。键和值均可觉得 null。可是,您不能把 Map 做为一个键或值添加给自身。

u       Object put(Object key,Object value):用来存放一个键-值对Map

u       Object remove(Object key):根据key(),移除一个键-值对,并将值返回

u       void putAll(Map mapping) :将另一个Map中的元素存入当前的Map

u       void clear() :清空当前Map中的元素

查询操做容许您检查映射内容:

u       Object get(Object key) :根据key()取得对应的值

u       boolean containsKey(Object key) :判断Map中是否存在某键(key

u       boolean containsValue(Object value):判断Map中是否存在某值(value)

u       int size():返回Map -值对的个数

u       boolean isEmpty() :判断当前Map是否为空

最后一组方法容许您把键或值的组做为集合来处理。

u       public Set keySet() :返回全部的键(key),并使用Set容器存放

u       public Collection values() :返回全部的值(Value),并使用Collection存放

u       public Set entrySet() 返回一个实现 Map.Entry 接口的元素 Set

由于映射中键的集合必须是惟一的,就使用 Set 来支持。由于映射中值的集合可能不惟一,就使用 Collection 来支持。最后一个方法返回一个实现 Map.Entry 接口的元素 Set

咱们看看Map的经常使用实现类的比较,以下表:

 

简述

实现

操做特性

成员要求

Map

保存键值对成员,基于键找值操做,使用compareTocompare方法对键进行排序

HashMap

能知足用户对Map的通用需求

键成员可为任意Object子类的对象,但若是覆盖了equals方法,同时注意修改hashCode方法。

TreeMap

支持对键有序地遍历,使用时建议先用HashMap增长和删除成员,最后从HashMap生成TreeMap 附加实现了SortedMap接口,支持子Map等要求顺序的操做

键成员要求实现Comparable接口,或者使用Comparator构造TreeMap键成员通常为同一类型。

LinkedHashMap

保留键的插入顺序,用equals 方法检查键和值的相等性

成员可为任意Object子类的对象,但若是覆盖了equals方法,同时注意修改hashCode方法。

下面咱们看一个简单的例子:

 

import java.util.*;

 

public class MapTest {

public static void main(String[] args) {

        Map map1 = new HashMap();

       Map map2 = new HashMap();

       map1.put("1","aaa1");

       map1.put("2","bbb2");   

       map2.put("10","aaaa10");

       map2.put("11","bbbb11");

//根据键 "1" 取得值:"aaa1"

       System.out.println("map1.get(/"1/")="+map1.get("1"));

// 根据键 "1" 移除键值对"1"-"aaa1"

       System.out.println("map1.remove(/"1/")="+map1.remove("1"));

        System.out.println("map1.get(/"1/")="+map1.get("1"));

       map1.putAll(map2);//map2所有元素放入map1

       map2.clear();//清空map2

       System.out.println("map1 IsEmpty?="+map1.isEmpty());

       System.out.println("map2 IsEmpty?="+map2.isEmpty());

       System.out.println("map1 中的键值对的个数size = "+map1.size());

       System.out.println("KeySet="+map1.keySet());//set

       System.out.println("values="+map1.values());//Collection

       System.out.println("entrySet="+map1.entrySet());    

       System.out.println("map1 是否包含键:11 = "+map1.containsKey("11"));

       System.out.println("map1 是否包含值:aaa1 = "+map1.containsValue("aaa1"));

    }

}

运行输出结果为:

map1.get("1")=aaa1

map1.remove("1")=aaa1

map1.get("1")=null

map1 IsEmpty?=false

map2 IsEmpty?=true

map1 中的键值对的个数size = 3

KeySet=[10, 2, 11]

values=[aaaa10, bbb2, bbbb11]

entrySet=[10=aaaa10, 2=bbb2, 11=bbbb11]

map1 是否包含键:11 = true

map1 是否包含值:aaa1 = false

 

在该例子中,咱们建立一个HashMap,并使用了一下Map接口中的各个方法。

其中Map中的entrySet()方法先提一下,该方法返回一个实现 Map.Entry 接口的对象集合。集合中每一个对象都是底层 Map 中一个特定的键-值对。

Map.Entry 接口是Map 接口中的一个内部接口,该内部接口的实现类存放的是键值对。在下面的实现原理中,咱们会对这方面再做介绍,如今咱们先无论这个它的具体实现。

咱们再看看排序的Map是如何使用:

import java.util.*;

 

public class MapSortExample {

  public static void main(String args[]) {

    Map map1 = new HashMap();

    Map map2 = new LinkedHashMap();

    for(int i=0;i<10;i++){

    double s=Math.random()*100;//产生一个随机数,并将其放入Map

     map1.put(new Integer((int) s)," "+i+" 个放入的元素:"+s+"/n");

     map2.put(new Integer((int) s)," "+i+" 个放入的元素:"+s+"/n");

    }

   

    System.out.println("未排序前HashMap"+map1);

    System.out.println("未排序前LinkedHashMap"+map2);

    //使用TreeMap来对另外的Map进行重构和排序

    Map sortedMap = new TreeMap(map1);

    System.out.println("排序后:"+sortedMap);

    System.out.println("排序后:"+new TreeMap(map2));

  }

}

该程序的一次运行结果为:

未排序前HashMap{64= 1 个放入的元素:64.05341725531845

, 15= 9 个放入的元素:15.249165766266382

, 2= 4 个放入的元素:2.66794706854534

, 77= 0 个放入的元素:77.28814965781416

, 97= 5 个放入的元素:97.32893518378948

, 99= 2 个放入的元素:99.99412014935982

, 60= 8 个放入的元素:60.91451419025399

, 6= 3 个放入的元素:6.286974058646977

, 1= 7 个放入的元素:1.8261658496439903

, 48= 6 个放入的元素:48.736039522423106

}

未排序前LinkedHashMap{77= 0 个放入的元素:77.28814965781416

, 64= 1 个放入的元素:64.05341725531845

, 99= 2 个放入的元素:99.99412014935982

, 6= 3 个放入的元素:6.286974058646977

, 2= 4 个放入的元素:2.66794706854534

, 97= 5 个放入的元素:97.32893518378948

, 48= 6 个放入的元素:48.736039522423106

, 1= 7 个放入的元素:1.8261658496439903

, 60= 8 个放入的元素:60.91451419025399

, 15= 9 个放入的元素:15.249165766266382

}

排序后:{1= 7 个放入的元素:1.8261658496439903

, 2= 4 个放入的元素:2.66794706854534

, 6= 3 个放入的元素:6.286974058646977

, 15= 9 个放入的元素:15.249165766266382

, 48= 6 个放入的元素:48.736039522423106

, 60= 8 个放入的元素:60.91451419025399

, 64= 1 个放入的元素:64.05341725531845

, 77= 0 个放入的元素:77.28814965781416

, 97= 5 个放入的元素:97.32893518378948

, 99= 2 个放入的元素:99.99412014935982

}

排序后:{1= 7 个放入的元素:1.8261658496439903

, 2= 4 个放入的元素:2.66794706854534

, 6= 3 个放入的元素:6.286974058646977

, 15= 9 个放入的元素:15.249165766266382

, 48= 6 个放入的元素:48.736039522423106

, 60= 8 个放入的元素:60.91451419025399

, 64= 1 个放入的元素:64.05341725531845

, 77= 0 个放入的元素:77.28814965781416

, 97= 5 个放入的元素:97.32893518378948

, 99= 2 个放入的元素:99.99412014935982

}

从运行结果,咱们能够看出,HashMap的存入顺序和输出顺序无关。而LinkedHashMap 则保留了键值对的存入顺序。TreeMap则是对Map中的元素进行排序。在实际的使用中咱们也常常这样作:使用HashMap或者LinkedHashMap 来存放元素,当全部的元素都存放完成后,若是使用则是须要一个通过排序的Map的话,咱们再使用TreeMap来重构原来的Map对象。这样作的好处是:由于HashMapLinkedHashMap 存储数据的速度比直接使用TreeMap 要快,存取效率要高。当完成了全部的元素的存放后,咱们再对整个的Map中的元素进行排序。这样能够提升整个程序的运行的效率,缩短执行时间。

这里须要注意的是,TreeMap中是根据键(Key)进行排序的。而若是咱们要使用TreeMap来进行正常的排序的话,Key 中存放的对象必须实现Comparable 接口。

咱们简单介绍一下这个接口:

1.4.3        Comparable 接口

java.lang 包中,Comparable 接口适用于一个类有天然顺序的时候。假定对象集合是同一类型,该接口容许您把集合排序成天然顺序。

它只有一个方法:compareTo() 方法,用来比较当前实例和做为参数传入的元素。若是排序过程当中当前实例出如今参数前(当前实例比参数大),就返回某个负值。若是当前实例出如今参数后(当前实例比参数小),则返回正值。不然,返回零。若是这里不要求零返回值表示元素相等。零返回值能够只是表示两个对象在排序的时候排在同一个位置。

上面例子中的整形的包装类:Integer 就实现了该接口。咱们能够看一下这个类的源码:

能够看到compareTo 方法里面经过判断当前的Integer对象的值是否大于传入的参数的值来获得返回值的。

Java 2 SDK,版本 1.2 中有十四个类实现 Comparable 接口。下表展现了它们的天然排序。虽然一些类共享同一种天然排序,但只有相互可比的类才能排序。

排序

BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short

按数字大小排序

Character

Unicode 值的数字大小排序

CollationKey

按语言环境敏感的字符串排序

Date

按年代排序

File

按系统特定的路径名的全限定字符的 Unicode 值排序

ObjectStreamField

按名字中字符的 Unicode 值排序

String

按字符串中字符 Unicode 值排序

 

这里只是简单的介绍一下排序接口,若是要详细的了解排序部份内容的话,能够参考文章最后的附录部分对于排序的更加详细的描述。

咱们再回到Map中来,Java提升的API中除了上面介绍的几种Map比较经常使用觉得还有一些Map,你们能够了解一下:

u   WeakHashMap WeakHashMap Map 的一个特殊实现,它只用于存储对键的弱引用。当映射的某个键在 WeakHashMap 的外部再也不被引用时,就容许垃圾收集器收集映射中相应的键值对。使用 WeakHashMap 有益于保持相似注册表的数据结构,其中条目的键再也不能被任何线程访问时,此条目就没用了。

u   IdentifyHashMap Map的一种特性实现,关键属性的hash码不是由hashCode()方法计算,而是由System.identityHashCode 方法计算,使用==进行比较而不是equals()方法。

 

经过简单的对与Map中各个经常使用实现类的使用,为了更好的理解Map,下面咱们再来了解一下Map的实现原理。

1.4.4         实现原理

有的人可能会认为 Map 会继承 Collection。在数学中,映射只是对(pair)的集合。可是,在集合框架中,接口 Map Collection 在层次结构没有任何亲缘关系,它们是大相径庭的。这种差异的缘由与 Set Map Java 库中使用的方法有关。Map 的典型应用是访问按关键字存储的值。它支持一系列集合操做的所有,但操做的是键-值对,而不是单个独立的元素。所以 Map 须要支持 get() put() 的基本操做,而 Set 不须要。此外,还有返回 Map 对象的 Set 视图的方法:

Set set = aMap.keySet();

       下面咱们以HashMap为例,对Map的实现机制做一下更加深刻一点的理解。

由于HashMap里面使用Hash算法,因此在理解HashMap以前,咱们须要先了解一下Hash算法和Hash表。

Hash,通常翻译作散列,也有直接音译为"哈希"的,就是把任意长度的输入(又叫作 预映射, pre-image),经过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间一般远小于输入的空间,不一样的输入可能 会散列成相同的输出,而不可能从散列值来惟一的肯定输入值。

说的通俗一点,Hash算法的意义在于提供了一种快速存取数据的方法,它用一种算法创建键值与真实值之间的对应关系,(每个真实值只能有一个键值,可是一个键值能够对应多个真实值),这样能够快速在数组等里面存取数据。

看下图:

咱们创建一个HashTable(哈希表),该表的长度为N,而后咱们分别在该表中的格子中存放不一样的元素。每一个格子下面存放的元素又是以链表的方式存放元素。

u       当添加一个新的元素Entry 的时候,首先咱们经过一个Hash函数计算出这个Entry元素的Hashhashcode。经过该hashcode值,就能够直接定位出咱们应该把这个Entry元素存入到Hash表的哪一个格子中,若是该格子中已经存在元素了,那么只要把新的Entry元存放到这个链表中便可。

u       若是要查找一个元素Entry的时候,也一样的方式,经过Hash函数计算出这个Entry元素的Hashhashcode。而后经过该hashcode值,就能够直接找到这个Entry是存放到哪一个格子中的。接下来就对该格子存放的链表元素进行逐个的比较查找就能够了。

 举一个比较简单的例子来讲明这个算法的运算方式:

假定咱们有一个长度为8Hash表(能够理解为一个长度为8的数组)。在这个Hash表中存放数字:以下表

0

1

2

3

4

5

6

7

假定咱们的Hash函数为:

Hashcode = X%8    -------- 8 取余数

其中X就是咱们须要放入Hash表中的数字,而这个函数返回的Hashcode就是Hash码。

假定咱们有下面10个数字须要依次存入到这个Hash表中:

11 , 23 , 44 , 9 , 6 , 32 , 12 , 45 , 57 , 89

经过上面的Hash函数,咱们能够获得分别对应的Hash码:

11――3 ; 23――7 ;44――4 ;9――16――632――012――445――557――189――1

计算出来的Hash码分别表明,该数字应该存放到Hash表中的哪一个对应数字的格子中。若是改格子中已经有数字存在了,那么就以链表的方式将数字依次存放在该格子中,以下表:

0

1

2

3

4

5

6

7

32

9

 

11

44

45

6

23

 

57

 

 

12

 

 

 

 

89

 

 

 

 

 

 

Hash表和Hash算法的特色就是它的存取速度比数组差一些,可是比起单纯的链表,在查找和存储方面却要好不少。同时数组也不利于数据的重构而排序等方面的要求。

更具体的说明,读者能够参考数据结构相关方面的书籍。

 

简单的了解了一下Hash算法后,咱们就来看看HashMap的属性有哪些:

里面最重要的3个属性:

n         transient Entry[] table: 用来存放键值对的对象Entry数组,也就是Hash

n         transient int size当前Map中存放的键值对的个数

n         final float loadFactor负载因子,用来决定什么状况下应该对Entry进行扩容

咱们Entry 对象是Map接口中的一个内部接口。便是使用它来保存咱们的键值对的。

咱们看看这个Entry 内部接口在HashMap中的实现:

 

经过查看源码,咱们能够看到Entry类有点相似一个单向链表。其中:

        final Object key 和 Object value;存放的就是咱们放入Map中的键值对。

属性Entry next;表示当前键值对的下一个键值对是哪一个Entry

接下来,咱们看看HashMap的主要的构造函数:

咱们主要看看 public HashMap(int initialCapacity, float loadFactor)

由于,另外两个构造函数实行也是一样的方式进行构建一个HashMap 的。

该构造函数:

1.         首先是判断参数int initialCapacityfloat loadFactor是否合法

2.         而后肯定Hash表的初始化长度。肯定的策略是:经过传进来的参数initialCapacity 来找出第一个大于它的2的次方的数。好比说咱们传了18这样的一个initialCapacity 参数,那么真实的table数组的长度为25次方,即32之因此采用这种策略来构建Hash表的长度,是由于2的次方的运算对于现代的处理器来讲,能够经过一些方法获得更加好的执行效率。

3.         接下来就是获得重构因子(threshold)了,这个属性也是HashMap中的一个比较重要的属性,它表示,当Hash表中的元素被存放了多少个以后,咱们就须要对该Hash表进行重构。

4.         最后就是使用获得的初始化参数capacity 来构建Hash表:Entry[] table

 

下面咱们看看一个键值对是如何添加到HashMap中的。

 

put方法是用来添加一个键值对(key-value)到Map中,若是Map中已经存在相同的键的键值对的话,那么就把新的值覆盖老的值,并把老的值返回给方法的调用者。若是不存在同样的键,那么就返回null 。咱们看看方法的具体实现:

1.         首先咱们判断若是keynull则使用一个常量来代替该key值,该行为在方法maskNull()终将key替换为一个非null 的对象k

2.         计算key值的Hash码:hash

3.         经过使用Hash码来定位,咱们应该把当前的键值对存放到Hash表中的哪一个格子中。indexFor()方法计算出的结果:i 就是Hash表(table)中的下标。

4.         而后遍历当前的Hash表中table[i]格中的链表。从中判断已否已经存在同样的键(Key)的键值对。若是存在同样的key的键,那么就用新的value覆写老的value,并把老的value返回

5.         若是遍历后发现没有存在一样的键值对,那么就增长当前键值对到Hash表中的第i个格子中的链表中。并返回null

最后咱们看看一个键值对是如何添加到各个格子中的链表中的:

咱们先看void addEntry(int hash, Object key, Object value, int bucketIndex)方法,该方法的做用就用来添加一个键值对到Hash表的第bucketIndex个格子中的链表中去。这个方法做的工做就是:

1.         建立一个Entry对象用来存放键值对。

2.         添加该键值对 ---- Entry对象到链表中

3.         最后在size属性加一,并判断是否须要对当前的Hash表进行重构。若是须要就在void resize(int newCapacity)方法中进行重构。

之因此须要重构,也是基于性能考虑。你们能够考虑这样一种状况,假定咱们的Hash表只有4个格子,那么咱们全部的数据都是放到这4个格子中。若是存储的数据量比较大的话,例如100。这个时候,咱们就会发现,在这个Hash表中的4个格子存放的4个长长的链表。而咱们每次查找元素的时候,其实至关于就是遍历链表了。这种状况下,咱们用这个Hash表来存取数据的性能实际上和使用链表差很少了。

可是若是咱们对这个Hash表进行重构,换为使用Hash表长度为200的表来存储这100个数据,那么平均2个格子里面才会存放一个数据。这个时候咱们查找的数据的速度就会很是的快。由于基本上每一个格子中存放的链表都不会很长,因此咱们遍历链表的次数也就不多,这样也就加快了查找速度。可是这个时候又存在了另外的一个问题。咱们使用了至少200个数据的空间来存放100个数据,这样就形成至少100个数据空间的浪费。 在速度和空间上面,咱们须要找到一个适合本身的中间值。在HashMap中咱们经过负载因子(loadFactor)来决定应该何时应该重构咱们的Hash表,以达到比较好的性能状态。

咱们再看看重构Hash表的方法:void resize(int newCapacity)是如何实现的:

它的实现方式比较简单:

1.      首先判断若是Hash表的长度已经达到最大值,那么就不进行重构了。由于这个时候Hash表的长度已经达到上限,已经没有必要重构了。

2.      而后就是构建新的Hash

3.      把老的Hash表中的对象数据所有转移到新的HashnewTable中,并设置新的重构因子threshold

 

对于HashMap中的实现原理,咱们就分析到这里。你们可能会发现,HashCode的计算,是用来定位咱们的键值对应该放到Hash表中哪一个格子中的关键属性。而这个HashCode的计算方法是调用的各个对象本身的实现的hashCode()方法。而这个方法是在Object对象中定义的,因此咱们本身定义的类若是要在集合中使用的话,就须要正确的覆写hashCode() 方法。下面就介绍一下应该如何正确覆写hashCode()方法。

1.4.5        覆写hashCode()

在明白了HashMap具备哪些功能,以及实现原理后,了解如何写一个hashCode()方法就更有意义了。固然,在HashMap中存取一个键值对涉及到的另一个方法为equals (),由于该方法的覆写在高级特性已经讲解了。这里就不作过多的描述。

    设计hashCode()时最重要的因素就是:不管什么时候,对同一个对象调用hashCode()都应该生成一样的值。若是在将一个对象用put()方法添加进HashMap时产生一个hashCode()值,而用get()取出时却产生了另一个hashCode()值,那么就没法从新取得该对象了。因此,若是你的hashCode()方法依赖于对象中易变的数据,那用户就要当心了,由于此数据发生变化时,hashCode()就会产生一个不一样的hash码,至关于产生了一个不一样的“键”。

    此外,也不该该使hashCode()依赖于具备惟一性的对象信息,尤为是使用this的值,这只能产生很糟糕的hashCode()。由于这样作没法生成一个新的“键”,使之与put()种原始的“键值对”中的“键”相同。例如,若是咱们不覆写ObjecthashCode()方法,那么调用该方法的时候,就会调用ObjecthashCode()方法的默认实现。ObjecthashCode()方法,返回的是当前对象的内存地址。下次若是咱们须要取一个同样的“键”对应的键值对的时候,咱们就没法获得同样的hashCode值了。由于咱们后来建立的“键”对象已经不是存入HashMap中的那个内存地址的对象了。

咱们看一个简单的例子,就能更加清楚的理解上面的意思。假定咱们写了一个类:Person (人),咱们判断一个对象“人”是否指向同一我的,只要知道这我的的身份证号一直就能够了。

先看咱们没有实现hashCode的状况:

package c08.hashEx;

 

import java.util.*;

//身份证类

class Code{

    final int id;//身份证号码已经确认,不能改变

    Code(int i){

       id=i;

    }

    //身份号号码相同,则身份证相同

    public boolean equals(Object anObject) {

        if (anObject instanceof Code){

           Code other=(Code) anObject;

           return this.id==other.id;

       }

       return false;

    }

    public String toString() {

       return "身份证:"+id;

    }  

}

//人员信息类

class Person {

    Code id;// 身份证

    String name;// 姓名

    public Person(String name, Code id) {

       this.id=id;

       this.name=name;

    }

    //若是身份证号相同,就表示两我的是同一我的

    public boolean equals(Object anObject) {

       if (anObject instanceof Person){

           Person other=(Person) anObject;

           return this.id.equals(other.id);

       }

       return false;

    }

    public String toString() {

       return "姓名:"+name+" 身份证:"+id.id+"/n";

    }

}

 

public class HashCodeEx {

    public static void main(String[] args) {

       HashMap map=new HashMap();

       Person p1=new Person("张三",new Code(123));

       map.put(p1.id,p1);//咱们根据身份证来做为key值存放到Map

       Person p2=new Person("李四",new Code(456));

       map.put(p2.id,p2);

       Person p3=new Person("王二",new Code(789));

       map.put(p3.id,p3);

       System.out.println("HashMap 中存放的人员信息:/n"+map);

       // 张三 更名为:张山 可是仍是同一我的。

       Person p4=new Person("张山",new Code(123));

       map.put(p4.id,p4);

       System.out.println("张三更名后 HashMap 中存放的人员信息:/n"+map);

       //查找身份证为:123 的人员信息

       System.out.println("查找身份证为:123 的人员信息:"+map.get(new Code(123)));

    }

}

运行结果为:

HashMap 中存放的人员信息:

{身份证:456=姓名:李四 身份证:456

, 身份证:123=姓名:张三 身份证:123

, 身份证:789=姓名:王二 身份证:789

}

张三更名后 HashMap 中存放的人员信息:

{身份证:456=姓名:李四 身份证:456

, 身份证:123=姓名:张三 身份证:123

, 身份证:123=姓名:张山 身份证:123

, 身份证:789=姓名:王二 身份证:789

}

查找身份证为:123 的人员信息:null

 

上面的例子的演示的是,咱们在一个HashMap中存放了一些人员的信息。并以这些人员的身份证最为人员的“键”。当有的人员的姓名修改了的状况下,咱们须要更新这个HashMap。同时假如咱们知道某个身份证号,想了解这个身份证号对应的人员信息如何,咱们也能够根据这个身份证号在HashMap中获得对应的信息。

而例子的输出结果表示,咱们所作的更新和查找操做都失败了。失败的缘由就是咱们的身份证类:Code 没有覆写hashCode()方法。这个时候,当查找同样的身份证号码的键值对的时候,使用的是默认的对象的内存地址来进行定位。这样,后面的全部的身份证号对象new Code(123) 产生的hashCode()值都是不同的。因此致使操做失败。

下面,咱们给Code类加上hashCode()方法,而后再运行一下程序看看:

//身份证类

class Code{

    final int id;//身份证号码已经确认,不能改变

    Code(int i){

       id=i;

    }

    //身份号号码相同,则身份证相同

    public boolean equals(Object anObject) {

       if (anObject instanceof Code){

           Code other=(Code) anObject;

           return this.id==other.id;

       }

       return false;

    }

    public String toString() {

       return "身份证:"+id;

    }  

    //覆写hashCode方法,并使用身份证号做为hash

    public int hashCode(){

       return id;

    }

}

再次执行上面的HashCodeEx 的结果就为:

HashMap 中存放的人员信息:

{身份证:456=姓名:李四 身份证:456

, 身份证:789=姓名:王二 身份证:789

, 身份证:123=姓名:张三 身份证:123

}

张三更名后 HashMap 中存放的人员信息:

{身份证:456=姓名:李四 身份证:456

, 身份证:789=姓名:王二 身份证:789

, 身份证:123=姓名:张山 身份证:123

}

查找身份证为:123 的人员信息:姓名:张山 身份证:123

 

这个时候,咱们发现。咱们想要作的更新和查找操做都成功了。

对于Map部分的使用和实现,主要就是须要注意存放“键值对”中的对象的equals()方法和hashCode()方法的覆写。若是须要使用到排序的话,那么还须要实现Comparable 接口中的compareTo() 方法。咱们须要注意Map中的“键”是不能重复的,而是否重复的判断,是经过调用“键”对象的equals()方法来决定的。而在HashMap中查找和存取“键值对”是同时使用hashCode()方法和equals()方法来决定的。

 

1.5        Set

1.5.1        概述

Java 中的Set和正好和数学上直观的集(set的概念是相同的。Set最大的特性就是不容许在其中存放的元素是重复的。根据这个特色,咱们就可使用Set 这个接口来实现前面提到的关于商品种类的存储需求。Set 能够被用来过滤在其余集合中存放的元素,从而获得一个没有包含重复新的集合。

1.5.2        经常使用方法

按照定义,Set 接口继承 Collection 接口,并且它不容许集合中存在重复项。全部原始方法都是现成的,没有引入新方法。具体的 Set 实现类依赖添加的对象的 equals() 方法来检查等同性。

咱们简单的描述一下各个方法的做用:

u     public int size() :返回set中元素的数目,若是set包含的元素数大于Integer.MAX_VALUE,返回Integer.MAX_VALUE

u     public boolean isEmpty() :若是set中不含元素,返回true

u     public boolean contains(Object o) :若是set包含指定元素,返回true

u     public Iterator iterator()

l         返回set中元素的迭代器

l         元素返回没有特定的顺序,除非set是提升了该保证的某些类的实例

u     public Object[] toArray() :返回包含set中全部元素的数组

u     public Object[] toArray(Object[] a) :返回包含set中全部元素的数组,返回数组的运行时类型是指定数组的运行时类型

u     public boolean add(Object o) :若是set中不存在指定元素,则向set加入

u     public boolean remove(Object o) :若是set中存在指定元素,则从set中删除

u     public boolean removeAll(Collection c) :若是set包含指定集合,则从set中删除指定集合的全部元素

u     public boolean containsAll(Collection c) :若是set包含指定集合的全部元素,返回true。若是指定集合也是一个set,只有是当前set的子集时,方法返回true

u     public boolean addAll(Collection c) :若是set中中不存在指定集合的元素,则向set中加入全部元素

u     public boolean retainAll(Collection c) :只保留set中所含的指定集合的元素(可选操做)。换言之,从set中删除全部指定集合不包含的元素。 若是指定集合也是一个set,那么该操做修改set的效果是使它的值为两个set的交集

u     public boolean removeAll(Collection c) :若是set包含指定集合,则从set中删除指定集合的全部元素

u     public void clear() :从set中删除全部元素

 

集合框架支持 Set 接口两种普通的实现:HashSet TreeSet以及LinkedHashSet。下表中是Set的经常使用实现类的描述:

 

 

简述

实现

操做特性

成员要求

Set

成员不能重复

HashSet

外部无序地遍历成员。

成员可为任意Object子类的对象,但若是覆盖了equals方法,同时注意修改hashCode方法。

TreeSet

外部有序地遍历成员;

附加实现了SortedSet, 支持子集等要求顺序的操做

成员要求实现Comparable接口,或者使用Comparator构造TreeSet。成员通常为同一类型。

LinkedHashSet

外部按成员的插入顺序遍历成员

成员与HashSet成员相似

 

在更多状况下,您会使用 HashSet 存储重复自由的集合。同时HashSet中也是采用了Hash算法的方式进行存取对象元素的。因此添加到 HashSet 的对象对应的类也须要采用恰当方式来实现 hashCode() 方法。虽然大多数系统类覆盖了 Object 中缺省的 hashCode() 实现,但建立您本身的要添加到 HashSet 的类时,别忘了覆盖 hashCode()

    对于Set的使用,咱们先以一个简单的例子来讲明:

import java.util.*;

 

public class HashSetDemo {

    public static void main(String[] args) {

       Set set1 = new HashSet();

       if (set1.add("a")) {//添加成功

           System.out.println("1 add true");

       }

       if (set1.add("a")) {//添加失败

           System.out.println("2 add true");

       }     

       set1.add("000");//添加对象到Set集合中

       set1.add("111");

       set1.add("222");

       System.out.println("集合set1的大小:"+set1.size());

       System.out.println("集合set1的内容:"+set1);

       set1.remove("000");//从集合set1中移除掉 "000" 这个对象

       System.out.println("集合set1移除 000 后的内容:"+set1);

       System.out.println("集合set1中是否包含000 "+set1.contains("000"));

       System.out.println("集合set1中是否包含111 "+set1.contains("111"));

       Set set2=new HashSet();

       set2.add("111");

       set2.addAll(set1);//set1 集合中的元素所有都加到set2

       System.out.println("集合set2的内容:"+set2);

       set2.clear();//清空集合 set1 中的元素

       System.out.println("集合set2是否为空 "+set2.isEmpty());

        Iterator iterator = set1.iterator();//获得一个迭代器

       while (iterator.hasNext()) {//遍历

           Object element = iterator.next();

           System.out.println("iterator = " + element);

       }

       //将集合set1转化为数组

       Object s[]= set1.toArray();

       for(int i=0;i<s.length;i++){

           System.out.println(s[i]);

       }

    }

}

程序执行的结果为:

1 add true

集合set1的大小:4

集合set1的内容:[222, a, 000, 111]

集合set1移除 000 后的内容:[222, a, 111]

集合set1中是否包含000 false

集合set1中是否包含111 true

集合set2的内容:[222, a, 111]

集合set2是否为空 true

iterator = 222

iterator = a

iterator = 111

222

a

111

从上面的这个简单的例子中,咱们能够发现,Set中的方法与直接使用Collection中的方法同样。惟一须要注意的就是Set中存放的元素不能重复。

    咱们再看一个例子,来了解一下其它的Set的实现类的特性:

package c08;

 

import java.util.*;

 

public class SetSortExample {

  public static void main(String args[]) {

    Set set1 = new HashSet();

    Set set2 = new LinkedHashSet();

    for(int i=0;i<5;i++){

    //产生一个随机数,并将其放入Set

    int s=(int) (Math.random()*100);

     set1.add(new Integer( s));

     set2.add(new Integer( s));

     System.out.println(" "+i+" 次随机数产生为:"+s);

    }

    System.out.println("未排序前HashSet"+set1);

    System.out.println("未排序前LinkedHashSet"+set2);

    //使用TreeSet来对另外的Set进行重构和排序

    Set sortedSet = new TreeSet(set1);

    System.out.println("排序后 TreeSet "+sortedSet);

  }

}

该程序的一次执行结果为:

0 次随机数产生为:96

1 次随机数产生为:64

2 次随机数产生为:14

3 次随机数产生为:95

4 次随机数产生为:57

未排序前HashSet[64, 96, 95, 57, 14]

未排序前LinkedHashSet[96, 64, 14, 95, 57]

排序后 TreeSet [14, 57, 64, 95, 96]

从这个例子中,咱们能够知道HashSet的元素存放顺序和咱们添加进去时候的顺序没有任何关系,而LinkedHashSet 则保持元素的添加顺序。TreeSet则是对咱们的Set中的元素进行排序存放。

通常来讲,当您要从集合中以有序的方式抽取元素时,TreeSet 实现就会有用处。为了能顺利进行,添加到 TreeSet 的元素必须是可排序的。 而您一样须要对添加到TreeSet中的类对象实现 Comparable 接口的支持。对于Comparable接口的实现,在前一小节的Map中已经简单的介绍了一下。咱们暂且假定一棵树知道如何保持 java.lang 包装程序器类元素的有序状态。通常说来,先把元素添加到 HashSet,再把集合转换为 TreeSet 来进行有序遍历会更快。这点和HashMap的使用很是的相似。

其实Set的实现原理是基于Map上面的。经过下面咱们对Set的进一步分析你们就能更加清楚的了解这点了。

1.5.3         实现原理

JavaSet的概念和数学中的集合(set)一致,都表示一个集内能够存放的元素是不能重复的。

       前面咱们会发现,Set中不少实现类和Map中的一些实现类的使用上很是的类似。并且前面再讲解Map的时候,咱们也提到:Map中的“键值对”,其中的“键”是不能重复的。这个和Set中的元素不能重复一致。咱们以HashSet为例来分析一下,会发现其实Set利用的就是Map中“键”不能重复的特性来实现的。

先看看HashSet中的有哪些属性:

再结合构造函数来看看:

经过这些方法,咱们能够发现,其实HashSet的实现,所有的操做都是基于HashMap来进行的。咱们看看是如何经过HashMap来保证咱们的HashSet的元素不重复性的:

看到这个操做咱们能够发现HashSet的巧妙实现:就是创建一个“键值对”,“键”就是咱们要存入的对象,“值”则是一个常量。这样能够确保,咱们所须要的存储的信息之是“键”。而“键”在Map中是不能重复的,这就保证了咱们存入Set中的全部的元素都不重复。而判断是否添加元素成功,则是经过判断咱们向Map中存入的“键值对”是否已经存在,若是存在的话,那么返回值确定是常量:PRESENT ,表示添加失败。若是不存在,返回值就为null 表示添加成功。

咱们再看看其余的方法实现:

       了解了这些后,咱们就不难理解,为何HashMap中须要注意的地方,在HashSet中也一样的须要注意。其余的Set的实现类也是差很少的原理。

       至此对于Set咱们就应该可以比较好的理解了。

1.6        总结:集合框架中经常使用类比较

集合框架设计软件时,记住该框架四个基本接口的下列层次结构关系会有用处:

  • Collection 接口是一组容许重复的对象。
  • Set 接口继承 Collection,但不容许重复。
  • List 接口继承 Collection,容许重复,并引入位置下标。
  • Map 接口既不继承 Set 也不继承 Collection, 存取的是键值对

咱们如下面这个图表来描述一下经常使用的集合的实现类之间的区别:

Collection/Map

接口

成员重复性

元素存放顺序(Ordered/Sorted

元素中被调用的方法

基于那中数据结构来实现的

HashSet

Set

Unique elements

No order

equals()

hashCode()

Hash

LinkedHashSet

Set

Unique elements

Insertion order

equals()

hashCode()

Hash 表和双向链表

TreeSet

SortedSet

Unique elements

Sorted

equals()

compareTo()

平衡树(Balanced tree

ArrayList

List

Allowed

Insertion order

equals()

数组

LinkedList

List

Allowed

Insertion order

equals()

链表

Vector

List

Allowed

Insertion order

equals()

数组

HashMap

Map

Unique keys

No order

equals()

hashCode()

Hash

LinkedHashMap

Map

Unique keys

Key insertion order/Access order of entries

equals()

hashCode()

Hash 表和双向链表

Hashtable

Map

Unique keys

No order

equals()

hashCode()

Hash

TreeMap

SortedMap

Unique keys

Sorted in key order

equals()

compareTo()

平衡树(Balanced tree

2        练习

撰写一个Person class,表示一我的员的信息。令该类具有多辆Car的信息,表示一我的能够拥有的车子的数据,以及:

       Certificate code: 身份证对象

name: 姓名

cash: 现金

List car;   拥有的汽车,其中存放的是Car对象

boolean buycar

相关文章
相关标签/搜索