Java集合排序及java集合类详解--(Collection, List, Set, Map)_对 ‘list

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!

可以看到,Java的Collection的Iterator 能够用来,:

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[i1] 接口实际上并没有直接的实现类。而List是容器的一种,表示列表的意思。当我们不知道存储的数据有多少的情况,我们就可以使用List 来完成存储数据的工作。例如前面提到的一种场景。我们想要在保存一个应用系统当前的在线用户的信息。我们就可以使用一个List来存储。因为List的最大的特点就是能够自动的根据插入的数据量来动态改变容器的大小。下面我们先看看List接口的一些常用方法。

1.3.2         常用方法

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

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

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

u      booleanaddAll(int index, Collection collection) :在index位置后添加容器collection中所有的元素

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

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

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

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

u      Object set(intindex, 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 参数的概念,我们先看看ArrayList在Sun 提供的源码中的实现方式。先看一下它的属性有哪些:

ArrayList 继承了AbstractList 我们主要看看ArrayList中的属性就可以了。

ArrayList中主要包含2个属性:

u       private transient ObjectelementData[];

u       private int size;

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

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

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

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

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

publicArrayList(int initialCapacity)

不过参数initialCapacity=10 。

我们主要看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.     然后通过判断oldCapacity和minCapacity参数谁大来决定是否需要扩容

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

n        如果minCapacity不大于oldCapacity那么就不进行扩容。

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

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

我们主要看看 public booleanadd(Object o)方法,可以发现在添加一个元素到容器中的时候,首先我们会判断是否需要扩容。因为只增加一个元素,所以扩容的大小判断也就为当前的size+1来进行判断。然后,就把新添加的元素放到数组elementData中。

第二个方法publicboolean 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) 方法。如果不覆写该方法的话,那么就会调用Object的equals()方法来进行判断。这就相当于比较两个对象的内存应用地址是否相等了。

在集合框架中,不仅仅是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``voidputAll(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``publicSet keySet() :返回所有的键(``key``),并使用``Set``容器存放

u``public Collection values() :返回所有的值(``Value``),并使用``Collection``存放

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

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

我们看看Map的常用实现类的比较,如下表:

简述实现操作特性成员要求
Map保存键值对成员,基于键找值操作,使用compareTo或compare方法对键进行排序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对象。这样做的好处是:因为HashMap和LinkedHashMap 存储数据的速度比直接使用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元素的Hash值hashcode。通过该hashcode值,就可以直接定位出我们应该把这个Entry元素存入到Hash表的哪个格子中,如果该格子中已经存在元素了,那么只要把新的Entry元存放到这个链表中即可。

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

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

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

01234567

假定我们的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――1;6――6;32――0;12――4;45――5;57――1;89――1;

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

01234567
329114445623
5712
89

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

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

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

里面最重要的3个属性:

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

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

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.        首先是判断参数intinitialCapacity和float loadFactor是否合法

2.        然后确定Hash表的初始化长度。确定的策略是:通过传进来的参数initialCapacity 来找出第一个大于它的2的次方的数。比如说我们传了18这样的一个initialCapacity 参数,那么真实的table数组的长度为2的5次方,即32。之所以采用这种策略来构建Hash表的长度,是因为2的次方的运算对于现代的处理器来说,可以通过一些方法得到更加好的执行效率。

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

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

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

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

1.        首先我们判断如果key为null则使用一个常量来代替该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表中的对象数据全部转移到新的Hash表newTable中,并设置新的重构因子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()种原始的“键值对”中的“键”相同。例如,如果我们不覆写Object的hashCode()方法,那么调用该方法的时候,就会调用Object的hashCode()方法的默认实现。Object的hashCode()方法,返回的是当前对象的内存地址。下次如果我们需要取一个一样的“键”对应的键值对的时候,我们就无法得到一样的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)));

}

最后

面试一面会问很多基础问题,而这些基础问题基本上在网上搜索,面试题都会很多很多。最好把准备一下常见的面试问题,毕竟面试也相当与一次考试,所以找工作面试的准备千万别偷懒。面试就跟考试一样的,时间长了不复习,现场表现肯定不会太好。表现的不好面试官不可能说,我猜他没发挥好,我录用他吧。

96道前端面试题:

常用算法面试题:

前端基础面试题:
内容主要包括HTML,CSS,JavaScript,浏览器,性能优化

c 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)));

}

最后

面试一面会问很多基础问题,而这些基础问题基本上在网上搜索,面试题都会很多很多。最好把准备一下常见的面试问题,毕竟面试也相当与一次考试,所以找工作面试的准备千万别偷懒。面试就跟考试一样的,时间长了不复习,现场表现肯定不会太好。表现的不好面试官不可能说,我猜他没发挥好,我录用他吧。

96道前端面试题:

  • [外链图片转存中…(img-whku79oh-1718178106917)]

常用算法面试题:

  • [外链图片转存中…(img-73KCXjfl-1718178106920)]

前端基础面试题:
内容主要包括HTML,CSS,JavaScript,浏览器,性能优化

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值