Java集合类原理详解

文章目录

  • 1 集合框架
    • 1.1 集合框架概述
      • 1.1.1 容器简介
      • 1.1.1 容器的分类
    • 1.2 Collection
      • 1.2.1 常用方法
      • 1.2.2 迭代器
    • 1.3 List
      • 1.3.1 概述
      • 1.3.2 常用方法
      • 1.3.3 实现原理
    • 1.4 Map
      • 1.4.1 概述
      • 1.4.2 常用方法
      • 1.4.3 Comparable 接口
      • 1.4.4 实现原理
      • 1.4.5 覆写 hashCode()
    • 1.5 Set
      • 1.5.1 概述
      • 1.5.2 常用方法
      • 1.5.3 实现原理
    • 1.6 总结:集合框架中常用类比较
  • 2 练习
  • 3 附录:排序

1 集合框架

1.1 集合框架概述

1.1.1 容器简介

到目前为止,我们已经学习了如何创建多个不同的对象,定义了这些对象以后,我们就可以利用它们来做一些有意义的事情。

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

对于对象集合,必须执行的操作主要以下三种:
♦添加新的对象
♦删除对象
♦查找对象

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

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

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

前面大家已经学习过了数组。数组的作用是可以存取一组数据。但是它却存在一些缺点, 使得无法使用它来比较方便快捷的完成上述应用场景的要求。

  1. 首先,在很多数情况下面,我们需要能够存储一组数据的容器,这一点虽然数组可 以实现,但是如果我们需要存储的数据的个数多少并不确定。比如说,我们需要在 容器里面存储某个应用系统的当前的所有的在线用户信息,而当前的在线用户信息是时刻都可能在变化的。 也就是说,我们需要一种存储数据的容器,它能够自动的改变这个容器的所能存放的数据数量的大小。这一点上,如果使用数组来存储的话,就显得十分的笨拙。

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

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

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

数学背景

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

在这里插入图片描述

集的基本属性如下:

♦集内只包含每项的一个实例
♦集可以是有限的,也可以是无限的
♦可以定义抽象概念

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

映射是一种特别的集。它是一种对(pair)集,每个对表示一个元素到另一元素的单向映射。 一些映射示例有:
♦ IP地址到域名(DNS)的映射
♦ 关键字到数据库记录的映射
♦ 字典(词到含义的映射)
♦ 2 进制到 10 进制转换的映射

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

1.1.1 容器的分类

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

在这里插入图片描述

Java 容器类类库的用途是“保存对象”,并将其划分为两个不同的概念:
1) Collection 。 一组对立的元素,通常这些元素都服从某种规则。List必须保持元素特定 的顺序,而 Set 不能有重复元素。

2) Map 。 一组 成对的“键值对”对象。初看起来这似乎应该是一个 Collection ,其元素 是成对的对象,但是这样的设计实现起来太笨拙了,于是我们将Map明确的提取出来形 成一个独立的概念。另一方面,如果使用Collection表示Map的部分内容,会便于查看 此部分内容。因此Map 一样容易扩展成多维Map,无需增加新的概念,只要让Map 中的键值对的每个"值”也是一个Map即可。

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

Map保存的是“键值对”,就像一个小型数据库。我们可以通过“键”找到该键对应的“值”。

♦ Collection -对象之间没有指定的顺序,允许重复元素。
♦ Set - 对象之间没有指定的顺序,不允许重复元素
♦ List- 对象之间有指定的顺序,允许重复元素,并引入位置下标。
♦ Map - 接口用于保存关键字(Key)和数值(Value)的集合,集合中的每个对象 加入时都提供数值和关键字。 Map 接口既不继承 Set 也不继承 Collection。

List、Set、Map共同的实现基础是Object数组。

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

在这里插入图片描述
这里没有 Collection 接口的实现,接下来我们再来看一下下面的这张关于集合框架的大 图:

在这里插入图片描述

这张图看起来有点吓人,熟悉之后就会发现其实只有三种容器:Map, List和Set,它 们各自有两个三个实现版本。

1.2 Collection

1.2.1 常用方法

Collection 接口用于表示任何对象或元素组。想要尽可能以常规方式处理一组元素时, 就使用这一接口。Collection在前面的大图也可以看出,它是List和Set的父类。并且它本 身也是一个接口。它定义了作为集合所应该拥有的一些方法。如下:
在这里插入图片描述
注意: 集合必须只有对象,集合中的元素不能是基本数据类型。

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

♦ boolean add(Object element)
♦ boolean remove(Object element)

Collection 接口还支持查询操作:

♦ int size()
♦ boolean isEmpty()
♦ boolean contains(Object element)
♦ Iterator iterator()

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

♦ boolean containsAll(Collection collection)
♦ boolean addAll(Collection collection)
♦ void clear()
♦ void removeAll(Collection collection)
♦ 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();//仓U建一个集合对象
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("集合collection中是否包含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数组的方法:

♦ Object[] toArray()
♦ Object[] toArray(Object[] a)

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

1.2.2 迭代器

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

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

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

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

Collection 接口的 iterator() 方法返回一个 Iterator。 Iterator 和您可 能已经熟悉的 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!

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

1.3.2 常用方法

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

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

♦ void add(int index, Object element) :添加对象 element 到位置 index 上
♦ boolean addAll(int index, Collection collection) :在 index 位置后添加容器 collection 中所有的元素
♦ Object get(int index) :取出下标为 index 的位置的元素
♦ int indexOf(Object element) :查找对象 element 在 List 中第一次出现的位置
♦ int lastIndexOf(Object element) :查找对象 element 在 List 中最后出现的位置
♦ Object remove(int index) :删除 index 位置上的元素
♦ Object set(int index, Object element) :将 index 位置上的对象替换为 element 并返回 老的元素。

先看一下下面表格:

在这里插入图片描述

在“集合框架”中有两种常规的 List 实现: ArrayList 和 LinkedList 。使用两种List 实现的哪一种取决于您特定的需要。如果要支持随机访问,而不必在除尾部的任何位 置插入或除去元素,那么, ArrayList 提供了可选的集合。但如果,您要频繁的从列表的 中间位置添加和除去元素,而只要顺序的访问列表元素,那么, LinkedList 实现更好。
我们以ArrayList为例,先看一个简单的例子:

public class ListDemo {
pulilic static void main(String[] a) {
String strMonths[] = { 
"1","2","3","4","5","6","7","8","9","10","11","12"
};
// get count of months
int nMonthLen = strMonths.length;
// create arrayList object
List months = new ArrayList();
// put months value to array list object
for (int i = 0; i < nMonthLen; i++)	{
months.add(strMonths[i]);
}
// print all element in ArrayList
for (int i = months.seze() - 1; i >= 0; i--) {
System.out.println(months.get(i));
}
}
}

例子中,我们把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 接口不但以位置友好的方式遍历整个列表,还能处理集合的子集:

♦ ListIterator listIterator():返回一个 ListIterator 迭代器,默认开始位置为 0
♦ ListIterator listIterator(int startIndex):返 回一个 ListIterator 迭代器,开始位置为 startIndex
♦ 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 j ava.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 ()和第二构造函数ArrayList (Collection c)是按照 Collection 接口文档所述,所应该提供两个构造函数,一个无参数,一个接受另一个 Collection。

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

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

public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8683452581122892189L;
/** 
* The array buffer into which the elements of the ArrayList are stored・
* The capac ity of the ArrayL ist is the length of this array buffer.
* 列表的实现核心属性:数组。我们使用诊数组未进行存放集合中的数据。
* 而我们的初始化参数就是该数组构建时候的长度,即访数组的性就是initialCapacity 
*/
private transient Object elementData[];
/**	
* The size of the ArrayList (the number of elements it contains).
* 列表中真实数据的存放个数
* @serial
*/
private int size;

ArrayList 继承了AbstractList 我们主要看看 ArrayList 中的属性就可以了。
ArrayList中主要包含 2 个属性:

♦ private transient Object elementData[];
♦ private int size;

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

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

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

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

public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IIlegalArgumentException("IIlegal Capacity:" + initialCapacity); // 构建一个初始化长度为initialcapacity的数组对象
this.elementData = new Object[initialcapacity];
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this(10);
}

从源码中可以看到默认的构造函数调用的就是带参数的构造函数:
public ArrayList(int initialCapacity)
不过参数 initialCapacity = 10
我们主要看 ArrayList(int initialCapacity) 这个构造函数。可以看到: this.elementData = new Object[initialCapacity];
我们就是使用的 initialCapacity 这个参数来创建一个 Object 数组。而我们所有的往该集合对象中存放的数据,就是存放到了这个Object数组中去了。

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

* 通过另外一个容器对象要构建一个List,构建的数组初始化长度为另外一个容器的size属性的1.1* @param c the collection whose elements are to be placed into this list.
* @throws NullPointerException if the specified collection is null.
*/
public ArrayList(Collection c) {
size = c.size(); // 当前元素的个数为另外一个容器中的元素的个数
// Allow 10% room for growth (扩充1.1倍的容量)
elementData = new Object[(int) Math.min((size * 110L) / 100, Integer.MAX_VALUE)];
c.toArray(elementData);
}
/**
* Returns the number of elements in this list.
* 返回List中元素的个数
* @returnthe number of elements in this list.
*/
public int size () {
return size;
}

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

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

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

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

* 该方法用来判断当前的数组是否需要扩容,应该扩容多少
* @param minCapacity the desired minimum capacity
*/public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
// 如果minCapacity大于老的elementData数组的长度,那么就需要扩容
Object oldData[] = elementData;
// 新的数组的长度为原来长度1.5倍加1,或者为minCapacity
int newCapacity = (oldCapacity * 3) / 2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = new Object[newCapacity];
System.arraycopy(oldData, 0, elementData, 0, size);
}
}

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

  1. 首先得到当前 elementData 属性的长度 oldCapacity
  2. 然后通过判断 oldCapacity 和 minCapacity 参数谁大来决定是否需要扩容
  • 如果minCapacity大于oldCapacity,那么我们就对当前的List对象进 行扩容。扩容的的策略为:取 (oldCapacity * 3)/2 + 1和minCapacity 之间更大的那个。然后使用数组拷贝的方法,把以前存放的数据转移到新的数 组对象中
  • 如果 minCapacity 不大于 oldCapacity 那么就不进行扩容

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

*	Appends the specified element to the end of this list.
*	@param o element to be appended to this list.
*	@return <tt>true</tt> (如果添加成功就返回true).
*/
public boolean add(Object o)  {
/ /调用ensureCapacity方法来确定是否需要扩容
ensureCapacity (size + 1) ;	 //  Increments modCount!!
elementData[size++] = o;
return true;
}*  该方法用来将另外一个容器C中的元素都添加到当前的List*	@param c the elements to be inserted into this list.
*	@return  <tt>true</tt>  如果添加的元素个数不为0,就返回true
*	@throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection c) {
Object[] a = c.toArray();
int numNew = a.length;
// 判断是否需要扩容
ensureCapacity (size + numNew) ; // Increments modCount!!
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

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

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

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

类似的 List 中的其他的方法也都是基于数组进行操作的。大家有兴趣可以看看源码中的更多的实现方式。
最后我们再看看如何判断在集合中是否已经存在某一个对象的:

/*
* 判断在当前的List是否包含某个对象
* @param elem element whose presence in this List is to be tested
* @return 存在返回<code>true</code>;否则返回false
*/
public boolean contains (Object elem) {
// 调用indexOf方法,如果返回值大于-1则表示存在该elem对象
return indexOf(elem) >= 0;
}
/*
* 将是包含某于对象elem的位置返回,如果不包含该对象则返回-1
*	@param	elem an object
*	@return 返回该对象第一次出现的位置,如果环存在则返回-1
*	@see	Object#equaIs(Object)[通过对象的equals来判断是否相等]
*/
public int inclexOf(Object elem) {
if (elem == null) {
for (int i = 0; i < size; i++)
if (elementData[i] == null)
return i;
} else {
// 使用elem象的equals来判断是否在数组中存在该对象
for (int i = 0; i < size; i++)
if (elem.equals(elementData[i]))
return i;
}
return -1;
}

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

♦ 首先我们判断一下elem对象是否为null ,如果为null的话,那么遍历数组 elementData 把第一个出现 null的位置返回。
♦ 如果 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 作为一个键或值添加给自身。

♦ Object put(Object key,Object value):用来存放一个键-值对 Map 中
♦ Object remove(Object key):根据key (键),移除一个键-值对,并将值返回
♦ void putAll(Map mapping) :将另外一个 Map 中的元素存入当前的 Map 中
♦ void clear():清空当前 Map 中的元素

查询操作允许您检查映射内容:

♦ Object get (Object key):根据key (键)取得对应的值
♦ boolean containsKey (Object key):判断 Map 中是否存在某键(key)
♦ boolean containsValue(Object value):判断 Map 中是否存在某值 (value)
♦ int size ():返回Map中键-值对的个数
♦ boolean isEmpty():判断当前 Map 是否为空

最后一组方法允许您把键或值的组作为集合来处理:

♦ public Set keySet ():返回所有的键(key),并使用Set容器存放
♦ public Collection values ():返回所有的值(Value),并使用 Collection 存放
♦ public Set entrySet() :返回一个实现 Map.Entry 接口的元素 Set, 因为映射中键的集合必须是唯一的,就使用 Set 来支持。因为映射中值的集合可能不唯一, 就使用 Collection 来支持。最后一个方法返回一个实现 Map.Entry 接口的元素 Set。
我们看看 Map 的常用实现类的比较,如下表:

在这里插入图片描述

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

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 就实现了该接口。我们可以看一下这个类的源码:

public final class Integer extends Number implements Comparable {
public int compareTo(Object o) {
return compareTo((Integer)o);
}public int compareTo(Integer anotherInteger) {
int thisVal = this.value;
int anotherVal = anotherInteger.value;
return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
}

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

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

在这里插入图片描述
这里只是简单的介绍一下排序接口,如果要详细的了解排序部分内容的话,可以参考文章最 后的附录部分对于排序的更加详细的描述。

我们再回到Map中来,Java提高的API中除了上面介绍的几种Map比较常用以为还有一些Map,大家可以了解一下:
WeakHashMapWeakHashMapMap 的一个特殊实现,它只用于存储对键的弱引用。当映射的某个键在 WeakHashMap 的外部不再被引用时,就允许垃圾收集器收集映射中相应的键值对。使用 WeakHashMap 有益于保持类似注册表的数据结构,其中条目的键不再被任何线程访问时,此条目就没用了。
IdentifyHashMapMap 的一种特性实现, 关键属性的 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,然后我们分别在该表中的格子中存放 不同的元素。每个格子下面存放的元素又是以链表的方式存放元素。
♦ 当添加一个新的元素Entry的时候,首先我们通过一个Hash函数计算出这个Entry元 素的Hash值hashcode。通过该hashcode值,就可以直接定位出我们应该把这个Entry 元素存入到Hash表的哪个格子中,如果该格子中己经存在元素了,那么只要把新的 Entry 元存放到这个链表中即可。
♦ 如果要查找一个元素Entry的时候,也同样的方式,通过Hash函数计算出这个Entry 元素的Hash值hashcode。然后通过该hashcode值,就可以直接找到这个Entry是存放到哪个格子中的。接下来就对该格子存放的链表元素进行逐个的比较查找就可以了。
举一个比较简单的例子来说明这个算法的运算方式:
假定我们有一个长度为8的Hash表(可以理解为一个长度为8的数组)。在这个Hash表中 存放数字:如下表
在这里插入图片描述

假定我们的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表中的哪个对应数字的格子中。如果改格子中已经有数字存在了,那么就以链表的方式将数字依次存放在该格子中,如下表:
在这里插入图片描述

Hash 表和 Hash 算法的特点就是它的存取速度比数组差一些,但是比起单纯的链表,在查找和存储方面却要好很多。同时数组也不利于数据的重构和排序等方面的要求。
更具体的说明,读者可以参考数据结构相关方面的书籍。

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

public class HashMap extends AbstractMap implements Map, Cloneable, Serializable {/*
* 哈希表,Entry对象中存放的是犍值对。并且该数组的长度为2的次方
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
/*
* 键值对的个数
* The number of key-value mappings contained in this identity hash map・ 
*/
transient int size;
/*
* The load factor for the hash table.
* 哈希表的负载因子
* @serial
* /
final float loadFactor;

里面最重要的3个属性:

  1. transient Entry [] table:用来存放键值对的对象Entry数组,也就是Hash 表
  2. transient int size:当前Map中存放的键值对的个数
  3. final float loadFactor:负载因子,用来决定什么情况下应该对Entry进行扩容

Entry对象是Map接口中的一个内部接口。即使用它来保存键值对。 我们看看这个Entry内部接口在HashMap中的实现:

static class Entry implements Map.Entry { 
final Object key; // 键,并且不可修改 
Object value; // 值
final int hash; // hash码 
Entry next; // 当前键值对的下一个键值对
Entry (int h, Object k, Object v, Entry n) { 
value = v;
next =n;
key = k; 
hash = h;
}

通过查看源码,我们可以看到Entry类有点类似一个单向链表。其中:
final Object keyObject value存放的就是我们放入Map中的键值对。 而属性Entry next表示当前键值对的下一个键值对。
接下来,我们看看 HashMap 的主要的构造函数:

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认负载因子为0.75threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);table = new Entry[DEFAULT_INITIAL_CAPACITY]; // 默认初始化hash表长度为16init();}
public HashMap(int initialCapacity,float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity:" + initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))  throw new IllegalArgumentException("Illegal load factor:" + loadFactor);// Find a power of 2 >= initialCapacity// 找出第一个大于initialCapacity的2次方的数为初始化hash表的长度int capacity = 1;while (capacity < initialCapacity)capacity <<= 1;// 负载因子this.loadFactor = loadFactor;// threshold表示hash表什么长度的时候需要重构threshold = (int)(capacity * loadFactor);table = new Entry[capacity]; // 构建Entry数组作为hash表init();}public HashMap(int initialCapacity) {// 默认负载因子为0.75this(initialCapacity,DEFAULT_LOAD_FACTOR);}

我们主要看看 public HashMap (int initialcapacity, float loadFactor) 因为,另外两个构造函数实际也是同样的方式进行构建一个HashMap的。 该构造函数:

  1. 首先是判断参数int initialCapacity和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 中的。
public Object put(Object key, Object value) {
// 如果key为null则使用一个常量来代替该key值
Object k = maskNull(key);
int hash = hash(k); // 计算key值的hash码
// 通过使用hash码来定位,我们应该把当前的键值对存放到hash表中的哪个格子中
int i = indexFor(hash, table.length); // 计算出为第i个格子
// 遍历当前的Hash表table[i]格中的链表
for (Entry e = table[i]; e != null;e = e.next()) {// 判断当前的Hash表table[i]格中的链表,是否已经存在相同的key的键值对if (e.hash == hash && eq(k,e.key)) {// 如果存在一样的key,那么把新的value覆写老的value,并把老的value返回Object oldValue = e.value;e.value = value;e.recordAccess(this); // 值(value)覆写事件,HashMap中该方法没有做任何处理return oldValue;
}
}modCount++; // 计数器+1
// 如果遍历后发现没有存在相同的键(key),那么就增加当前的键值对到hash表中
addEntry(hash,k,value,i);
return null;
}

该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) {
// 创建一个Entry对象用来存放键值对
// 并把原来的格子中链表的第一位置的元素作为当前Entry对象的下一个元素
// 换句话说,就是把当前创建的Entry对象,加到链表的第一个位置,其它的挂到它的后面
table[bucketIndex] = new Entry(hash, k, value, table[bucketIndex]);
// 如果存放元素的个数大于重构因子threshold,那么就进行重构
if (size++ >= threshold) {resize(2 * table.length);
}

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

  1. 创建一个 Entry 对象用来存放键值对
  2. 添加该键值对 Entry 对象到链表中
  3. 最后在size属性+1,并判断是否需要对当前的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)是如何实现的:

// 该方法用来重构Hash表
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {// 如果Hash表的长度已经达到最大值,那么就不进行重构了threshold = Integer.MAX_VALUE;return;
}
// 构建新的Hash表
Entry[] newTable = new Entry[newCapacity];
// 将所有老的Hash表中的元素都转移存放到新的Hash表newTable中
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}

它的实现方式比较简单:

  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)));
}
}
运行结果为:
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(Obj ect 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() 方法来检查等同性。
在这里插入图片描述

我们简单的描述一下各个方法的作用:
public int size():返回set中元素的数目,如果 set 包含的元素数大于 Integer.MAX_VALUE,返回 Integer.MAX_VALUE
public boolean isEmpty() :如果 set 中不含元素,返回 true
public boolean contains(Object o) :如果 set 包含指定元素,返回 true
public Iterator iterator():返回 set 中元素的迭代器,元素返回没有特定的顺序
public Object[] toArray() :返回包含 set 中所有元素的数组
public Object[] toArray(Object[] a):返回包含set中所有元素的数组,返回数组的运 行时类型是指定数组的运行时类型
public boolean add(Object o):如果set中不存在指定元素,则向set加入
public booleanremove(Objecto):如果set中存在指定元素,则从set中删除
public boolean removeAll(Collection c):如果 set 包含指定集合,则从 set 中删除指 定集合的所有元素
public boolean containsAll(Collection c):如果 set 包含指定集合的所有元素,返回 true。如果指定集合也是一个set,只有是当前set的子集时,方法返回true
public boolean addAll(Collection c):如果 set 中中不存在指定集合的元素,则向 set 中加入所有元素
public boolean retainAll(Collection c):只保留 set 中所含的指定集合的元素(可选操 作)。换言之,从set中删除所有指定集合不包含的元素。如果指定集合也是一个 set,那么该操作修改set的效果是使它的值为两个set的交集
public boolean removeAll(Collection c):如果 set 包含指定集合,则从 set 中删除指 定集合的所有元素
public void clear():从 set 中删除所有元素

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

在这里插入图片描述

在更多情况下,您会使用 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转化为数组
Obj ect s[]= set].toArray(); for(int i=0;i<s.length;i++){
System.out.println(s[i]);
}
}
}

程序执行的结果为:

1 add true
集合set]的大小:4
集合 set]的内容:[222, a, 000, ]]]]
集合set]移除000后的内容:[222, a, ]]]]
集合set]中是否包含000 : false
集合set]中是否包含]]]:true
集合 set2 的内容:[222, a, ]]]]
集合set2是否为空:true
iterator = 222
iterator = a
iterator =]]]
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 实现原理

Java中Set的概念和数学中的集合(set)一致,都表示一个集内可以存放的元素是不能重 复的。
前面我们会发现,Set中很多实现类和Map中的一些实现类的使用上非常的相似。而且 前面再讲解Map的时候,我们也提到:Map中的“键值对”,其中的“键”是不能重复的。 这个和Set中的元素不能重复一致。我们以HashSet为例来分析一下,会发现其实Set利用 的就是Map中“键”不能重复的特性来实现的。
先看看HashSet中有哪些属性:

public class HashSet extends AbstractSet implements Set, Cloneable, java.io.Serializable {static final long serialVersionUID = -5024744406713321676L;// 核心属性:HashMapprivate transient HashMap map;// 常量:present用来所有Map中“键值对”的“值”// Dummy value to associate with an Object in the backing Mapprivate static final Object PRESENT = new Object();

再结合构造函数来看看:

/*
* 构造函数就是对HashMap的构建
* Constructs a new, empty set;the backing <tt>HashMap</tt>
* default initial capacity(16) and load factor(0.75).
*/public HashSet() {map = new HashMap();
}public HashSet(int initialCapacity, float loadFactor) {map = new HashMap(initialCapacity,loadFactor);}public HashSet(int initialCapacity) {map = new HashMap(initialCapacity);
}

通过这些方法,我们可以发现,其实HashSet的实现,全部的操作都是基于HashMap来进行的。我们看看是如何通过HashMap来保证我们的HashSet的元素不重复性的:

/*
* Adds the specified element to this set if it is not already contain the specified element.
*
*/ public boolean add(Object o) {return map.put(o,PRESENT) == null;
}

看到这个操作我们可以发现HashSet的巧妙实现:就是建立一个“键值对”,“键”就是 我们要存入的对象,“值”则是一个常量。这样可以确保,我们所需要的存储的信息之是“键”。 而“键”在Map中是不能重复的,这就保证了我们存入Set中的所有的元素都不重复。而 判断是否添加元素成功,则是通过判断我们向Map中存入的“键值对”是否己经存在,如 果存在的话,那么返回值肯定是常量: PRESENT,表示添加失败。如果不存在,返回值就 为null `表示添加成功。

我们再看看其他的方法实现:

/*
* Removes the specified element from this set if it is present
* 移出操作即是对Map中的键值对进行移出,如果返回值为PRESENT常量,则表示移出成功
* @param o object to be removed from this set, if present.
* @return <tt>true</tt> if the set contained the specified element.
*
*/public boolean remove(Object o) {return map.remove(o) == PRESENT;
}public int size() {return map.size();
}public boolean contains(Object o) {return map.containsKey(o);
}public boolean isEmpty() {return map.isEmpty();
}public Iterator iterator() {return map.keySet().iterator();
}

了解了这些后,我们就不难理解,为什么HashMap中需要注意的地方,在HashSet中 也同样的需要注意。其他的Set的实现类也是差不多的原理。
至此对于Set我们就应该能够比较好的理解了。

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

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

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

我们以下面这个图表来描述一下常用的集合的实现类之间的区别:
在这里插入图片描述

2 练习

  1. 撰写一个Person class,表示一个人员的信息。令该类具备多辆Car的信息,表示一个人可以拥有的车子的数据

属性/方法:
Certificate code: 身份证对象
name: 姓名
cash: 现金
List car: 拥有的汽车,其中存放的是Car对象
boolean buycar(car):买车子
boolean sellcar(Person p):把自己全部的车子卖给别人
boolean buyCar(Car car,Person p):自动查找卖车的人p是否有买主想要买的车car,如果有就买,并返回true, 否则返回false
viod addCar(car):把某辆车送给方法的调用者。
String toString():得到人的信息

  1. 并撰写第二个 Car class

属性/方法:
String ID:ID 车牌号
cost:价格
color:颜色
Person owner:车子的拥有者
to String():得到汽车的信息
equals():比较车子是否同一俩汽车,ID相同则认为相同

  1. 在另外一个Market类里面,进行车子的买卖。并保留所有交易人员的的信息到一个HashMap中,我们可以通过身份证号来查找到对应的人员的信息。同时所有的车子种类都在市场中进 行注册,即车子的信息使用一个Set来保存

属性/方法:
HashMap people:存放交易人员的信息。Key为身份证号,value为Person对象
static boolean sellCar(Person p1 ,Car car1, Person p2):p1 将 car1 卖给 p2 。并在该方法中记录效益人的信息到 people 中。

  1. 撰写类Certificate 表示身份证

属性/方法:

Id:号码
equals():比较两个身份证是否同一个,ID相同则认为相同
hashCode():正确编写 hashCode 方法

场景:
一个叫Bob的人:身份证:310 现金:30000。
有一辆车子:ID:001,红色,价格:50000的车子;
一个叫 Tom 的人:身份证: 210 现金: 70000,
有一辆车子:颜色:白色,ID:003,价格:25000。
一个叫 King 的人:身份证: 245 现金: 60000,
有2辆车子:颜色:白色,ID:005,价格:18000。
颜色:红色,ID:045,价格:58000。

Tom买了 Bob的车子.他就拥有了 2辆汽车
King 把 ID=005 的车子买给了 Bob
最后各人的信息如何?

3 附录:排序

为了用’'集合框架"的额外部分把排序支持添加到Java 2 SDK,版本1.2,核心Java库 作了许多更改。像 String 和 Integer 类如今实现 Comparable 接口以提供自然排序 顺序。对于那些没有自然顺序的类、或者当您想要一个不同于自然顺序的顺序时,您可以实 现 Comparator 接口来定义您自己的。
为了利用排序功能, '集合框架"提供了两种使用该功能的接口: SortedSetSortedMap

Comparable 接口
在 java.lang 包中, Comparable 接口适用于一个类有自然顺序的时候。假定对象集合 是同一类型,该接口允许您把集合排序成自然顺序。
在这里插入图片描述

compareTo() 方法比较当前实例和作为参数传入的元素。如果排序过程中当前实例出现在参数前,就返回某个负值。如果当前实例出现在参数后,则返回正值。否则,返回零。这 里不要求零返回值表示元素相等。零返回值只是表示两个对象排在同一个位置。

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

在这里插入图片描述

创建您自己的类 Comparable 只是个实现 compareTo() 方法的问题。通常就是依赖几 个数据成员的自然排序。您自己的类也应该覆盖 equals() 和 hashCode() 以确保两个 相等的对象返回同一个散列码。

Comparator 接口

若一个类不能用于实现java .lang .Comparable,您可以提供自己的 java.util.Comparator 行为。如果您不喜欢缺省的 Comparable 行为,您照样可以 提供自己的 Comparator。

在这里插入图片描述

Comparatorcompare() 方法的返回值和 ComparablecompareTo() 方法的 返回值相似。在此情况下,如果排序时第一个元素出现在第二个元素之前,则返回一个负值。 如果第一个元素出现在后,那么返回一个正值。否则,返回零。与 Comparable 相似,零 返回值不表示元素相等。一个零返回值只是表示两个对象排在同一位置。由 Comparator 用户决定如何处理。如果两个不相等的元素比较的结果为零,您首先应该确信那就是您要的结果,然后记录行为。

为了演示,您会发现编写一个新的忽略大小写的Comparator,代替使用Collator进行语言环境特定、忽略大小写的比较会更容易。这样的一种实现如下所示:

class CaseInsensitiveComparator implements Comparator {
public int compare(Object element1, Object element2) {
String lowerE1 = ((String)element1).toLowerCase();
String lowerE2 = ((String)element2).toLowerCase(); 
return lowerE1.compareTo(lowerE2);
}
}

因为每个类在某些地方都建立了 Object 子类,所以这不是您实现 equals() 方法的必 要条件。实际上大多数情况下您不会去这样做。切记该equals() 方法检查的是 Comparator 实现的等同性,不是处于比较状态下的对象。

Collections类有个预定义的 Comparator 用于重用。调用Collections.reverseOrder() 返回一个 Comparator, 它对逆序实现 Comparable 接口的对象进行排序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/334994.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

sql安装联机丛书提示发生网络错误_速达软件:安装问题解答

问题Q&A在安装MSDE数据库时&#xff0c;出现安装程序倒退的情况解决方案&#xff1a;这是以前装过SQL Server数据库或MSDE数据库,没有卸载干净.解决办法重装操作系统。安装MSDE数据库时&#xff0c;系统出现“读取文件C:\program files\Microsoft sql sever\mssql\data\msd…

java分布式系统开发_从微服务到分布式系统-Java开发人员生存指南

java分布式系统开发感觉像是对微服务的炒作正在慢慢地落到实处&#xff0c;并且我们的行业开始意识到&#xff0c;根据微服务背后的体系结构范式无法通过仅在现有组件之上公开一些HTTP接口来轻松创建一个系统。 。 我们似乎确实同意必须进行服务优化的基础架构&#xff0c;文化…

链表数据结构原理图/内存结构图/内存图

单项链表内存结构图&#xff08;也叫原理图&#xff09; 单项链表内存结构图

反射与泛型 java_Java的反射 和 泛型的一个问题

aluckdog模板方法在编译过程中类型擦除&#xff1a;update(T,UpdateOperations)退化成update(Object,UpdateOperations)&#xff1b;update(Query,UpdateOperations)退化成update(Query,UpdateOperations)&#xff1b;morphia的update方法不光是一个模板方法&#xff0c;还是一…

迭代器原理图

原理图&#xff1a; Iterator<String> it Collection.iterator();it 是集合Collection的视图&#xff0c;迭代器其实就是视图&#xff08;类似数据库的 view&#xff09;&#xff0c;可以理解为一张单列多行的表&#xff0c;如下图所示&#xff1a; 有人理解成单行多…

neo4j python 算法_图论与图学习(二):图算法

选自towardsdatascience作者&#xff1a;Mal Fabien机器之心编译参与&#xff1a;熊猫图(graph)近来正逐渐变成机器学习的一大核心领域&#xff0c;比如你可以通过预测潜在的连接来理解社交网络的结构、检测欺诈、理解汽车租赁服务的消费者行为或进行实时推荐。近日&#xff0c…

cobol和java_现代化历险:策略+将COBOL转换为Java的示例

cobol和java在Keyhole Software&#xff0c;我们在很大程度上是一家现代化公司。 我们有一些顾问&#xff0c;专门研究将旧的代码迁移到新的&#xff0c;翻新的残旧代码库&#xff0c;并为大多数已经被供应商锁定的企业设计更光明的未来。 作为这些经验的一个有趣的副作用&…

递归算法 流程图_什么是算法?如何学习算法?算法入门的学习路径

什么是算法&#xff1f;有一个很著名的公式 “程序数据结构算法”。曾经跟朋友吃饭的时候我问他什么是算法&#xff0c;他说算法嘛&#xff0c;就是一套方法&#xff0c;需要的时候拿过来&#xff0c;套用就可以&#xff0c;我吐槽他&#xff0c;他说的是小学数学题的算法&…

envoy重试_具有Envoy代理的微服务模式,第二部分:超时和重试

envoy重试该博客是系列文章的一部分&#xff0c;该系列文章更深入地介绍了Envoy Proxy和Istio.io &#xff0c;以及它如何实现一种更优雅的连接和管理微服务的方式。 跟随我christianposta &#xff0c;紧跟这些博客文章的发布。 什么是Envoy代理 &#xff0c;它如何工作&…

网络研讨室_即将举行的网络研讨会:调试生产中Java的5种最佳实践

网络研讨室您的团队是否花费超过10&#xff05;的时间在生产中调试Java&#xff1f; 将新代码部署到生产中是一项艰巨的任务。 在您的本地环境中起作用的东西在生产中的作用并不相同&#xff0c;您可以通过用户来了解。 不理想吧&#xff1f; 生产中的调试是一个关键要素&…

chrome gwt1.7_快速提示:使用Chrome开发工具调试GWT应用程序

chrome gwt1.7调试是软件开发的重要方面。 拥有正确的工具可以节省大量时间和头痛。 在GWT Super Dev模式之前&#xff0c;经典的Dev模式允许使用JVM调试。 开发人员可以在其IDE中设置断点&#xff0c;并使用调试模式来跟踪错误和错误。 现在&#xff0c;在超级开发模式下&…

Servlet 运行原理

文章目录Servlet 如何运行演示 Servlet 运行原理Servlet 如何运行 用户向浏览器地址栏输入&#xff1a;http://ip:port/helloweb/sayHello?namezs 浏览器使用 ip:port(端口号)连接服务器 浏览器将请求数据按照 http 协议打成一个数据包(请求数据包)发送给服务器 请求数据包…

java github_GitHub Research:超过50%的Java记录语句写错了

java github为什么生产日志无法帮助您找到错误的真正根本原因&#xff1f; 询问您是否使用日志文件监视您的应用程序几乎就像询问…您是否喝水。 我们都使用日志&#xff0c;但是我们如何使用它们则是一个完全不同的问题。 在下面的文章中&#xff0c;我们将对日志进行更深入…

yolov5论文_YOLOv5的妙用:学习手语,帮助听力障碍群体

编辑&#xff1a;魔王、杜伟计算机视觉可以学习美式手语&#xff0c;进而帮助听力障碍群体吗&#xff1f;数据科学家 David Lee 用一个项目给出了答案。如果听不到了&#xff0c;你会怎么办&#xff1f;如果只能用手语交流呢&#xff1f;当对方无法理解你时&#xff0c;即使像订…

python制作系统程序与html交互_python+html语音人物交互_flask后台与前端(html)交互的两种方法...

基于python flask框架搭建webflask后台与前端(html)交互的两种方法&#xff1a;方法1 使用flask-wtf 提供的表单用常见的登录为例&#xff1a;// An highlighted blockfrom flask_wtf import Formclass LoginForm(Form): # 登录表单ROLE SelectField(角色, choices[(s, 管理员…

Java 程序执行过程的内存流程图(结合类加载器 ClassLoader 讲解)

Student s new Student(); s.play(); Student s2 new Student();以上代码的执行流程如下&#xff1a; JVM 作为操作系统的一个迚程在系统中执行&#xff0c;那么系统会为 JVM 分配一块内存空间&#xff0c;这块内存空 间被 JVM 分为 3 大块(栈区、堆区、方法区) 一般而言&a…

虚拟内存越大越好吗_手机的运行内存真的是越大越好吗?6GB和8GB到底又该如何选择?...

许多人买手机&#xff0c;除了看处理器、外观以外&#xff0c;关注最多的莫过于手机的运行内存了。选择一个合适的运行内存几乎关系到整个手机使用寿命和命脉。那么我们现在买手机选择多大的运行内存合适呢&#xff1f;真的是越大越好吗&#xff1f;下面我们就来一起看看吧。选…

显微镜自动聚焦原理是什么_什么是共聚焦显微镜?你了解过共聚焦显微镜吗?...

更出色的表面分析ZEISS Smartproof 5产品表面粗糙度质量控制ZEISS Smartproof 5是一款集成式转盘共聚焦显微镜&#xff0c;依托孔径关联技术将传统共聚焦显微镜的高分辨率与转盘系统的高速采集相结合&#xff0c;能够高速、准确地采集表面3D数据。二维测量&#xff1a;距离、高…

Java 程序执行过程的内存流程图(手写稿)

操作系统会分配一定的内存空间给JVM&#xff0c;空间大小可以在JVM里面设置&#xff0c;JVM会将内存分为三个区域&#xff1a;栈、堆、方法区。

dc/os_DC / OS中具有Java和数据库应用程序的服务发现

dc/os该博客将展示一个简单的Java应用程序如何使用DC / OS中的服务发现与数据库进行对话。 为什么要进行服务发现&#xff1f; 应用程序通常由多个组件组成&#xff0c;例如应用程序服务器&#xff0c;数据库&#xff0c;Web服务器&#xff0c;缓存和消息传递服务器。 通常&am…