c++怎么将两个类的方法集合成一个类的方法_Java之集合(下)

c28e6d6643071da27dde37c24aa63f42.gif 0e9542a620125a541e48afe70c5c326e.png ecfffff31ae850b48351e53a36835079.gif文章前言 a27b7ebb0d81ff5d7fc3493d569072c9.gif c28e6d6643071da27dde37c24aa63f42.gif 0e9542a620125a541e48afe70c5c326e.png

在集合(上)中我们详细的讲解了List的实现类的一些重要原理,Set的实现类还没有讲,但是我们简要的看下Set下的两个主要实现类的代码:

f925f6ddae8559d4fc0d07c4a3df083d.png

1abfdc064d68e2bcf8ba9386f011e5de.png

我们会看到Set的底层是Map的实现类,所以我们讲完Map再来看Set会变得简单些。

Map

对于Map,得先知道它的一些特点:Map中数据是以"key-value"的形式存在的,一个key对应一个value,俗称键值。就如同我们的饮料一样,一种饮料对应一个价格。

而对于需要了解的Map结构,先看看以下的"简单"关系图:

27311ef763855afed2eda9e7a0fd6bb0.png

对于Map的常见接口主要有Map和TreeMap两个,而常用的实现类就看HashMap、Propertied、TreeMap这三个就行了。接下来还是一样,先看下常见接口的常用方法:

Map

7510b61e8760395e6b6d5db1932fa004.png

清空集合的所有元素。

92742ceb64993ee7ecb5df8ef1ec3355.png

判断集合中是否包含某个键或值。(底层调用equals方法,自定义类需要重写)

bcae9d38399b16e37b01effc98211f2f.png

通过指定的键获取键包含的值。

db08d161adec3fd7ec1f7d1a9abf30c3.png

判断集合是否为空。

c947fe23071c0f97a221d8d924aeda50.png

往集合中添加键值。

72dec6b2ebd6a907f3bad44c0c1dadbe.png

通过指定键来删除对应的键值。

decd10d9e525ff1d28bce20667354240.png

返回集合中键值的个数。

c6d3c8dbd388d5b258e8face7d30fbaf.png

5000b88ed2f7027e7dc3fd1b3ac2f1e6.png

这两个方法都返回了一个Set集合,之后遍历Map时候讲。

Map中还有一个重要的内部类:

2a8e619131c10f865b305cda1244ca9b.png

这个是Map的实现类存取键值的重要的父类。

SortedMap

5356523b61b2f655738a4abcbf0d49b5.png

重点就看这个comparator方法,之后到TreeMap讲。

Map的实现类

和Collection集合一样,我们要了解几个实现类的一些底层原理,而且Map的原理会更复杂:

HashMap 71ca84e33142a618428c60c3a23c7e68.gif

作为Map中最具有代表性的实现类,HashMap常用方法基本都在Map里面了。而HashMap底层的结构实际上是由数组加单向链表(红黑树)组成的。这也解释了为什么Map是无序的,而存储键值使用HashMap的Node内部类。那怎么定位到指定的数组,链表又怎么和数组连接起来构成一个HashMap呢?先看以下代码:

872222532fa1d04ba860a3c16cb2cab5.png

我们新建一个HashMap集合,然后在里面添加键值,这时候会根据键的哈希值(hashcode)来确定键值添加到数组的位置。这也是Map无序的体现了,因为根据哈希值的不同,每个键值不知道会存储到数组的哪个地方。而键的哈希值是有可能相同的,于是就会添加到数组的同一个索引下,而数组的一个索引只能存放一个元素,多个键值在同一个数组的索引下就变成了链表。当有着同一个哈希值的键值添加进链表的时候会对链表中的所有的键进行比较(equals),如果相同就覆盖已有键的值。这就是Map不可重复的由来了(所以上述的代码键为3的键值的值其实是只有乐虎而已)。于是乎就形成了大致如下的结构:

d40db93f023b01c4a56486dc61b10e89.png

然后JDK8的新特性:为了加快检索效率加了一个红黑树。当数组的一个链表中的键值超过8个之后就会转换为红黑树,当数组的一个链表中的键值少于6个之后就会恢复成链表。并且遵循左小右大原则(比当前节点小的元素放在节点左边,反之右边)。

6f93e85e0b307fd50b35596536a2d9d5.png

大致上了解了HashMap的一些原理之后,我们就看看源代码是如何实现的:

HashMap的常量和属性

807e1775c1fa96d4a0cafa7a43c8d4b4.png

ee7d08d5787d2f0f131f7c6d625f0476.png

f9b581b0b3522ea0165222379b2366d5.png

上图截取了常见的常量及属性:

DEFAULT_INITIAL_CAPACITY表示默认初始容量;

MAXIMUM_CAPACITY表示最大容量;

DEFAULT_LOAD_FACTOR表示默认加载因子(扩容讲解);

TREEIFY_THRESHOLD表示转变为红黑树的临界值;

UNTREEIFY_THRESHOLD表示还原为链表的临界值;

在属性中用一个table的数组加一个节点类来表示Map的数据结构,用entrySet来缓存键值(用于entrySet方法);size和modCount和Collection一样来表示键值个数和集合被修改的次数;loadFactor表示加载因子(没有赋值就是上述的DEFAULT_LOAD_FACTOR)。threshold表示阈值,等于容量*加载因子。

静态内部类——Node

HashMap的实现链表的节点类:

3b624fe5dc5d71c4e049214e23438f38.png

可以看到它是实现了Map的内部类Map.Entry。

HashMap常用构造方法

eac30f13369dd706ea883d2950a2a778.png

如果是无参构造,加载因子为默认值,且其他属性都是默认值。

如果传入一个容量和加载因子,那么会进行判断,都合法(容量>=0且如果容量大于最大值,容量变为最大值;加载因子是数字且>0)就进行赋值。

如果传入一个容量,和上述一样,就是加载因子是默认值。

扩容机制

扩容一般发生在添加键值的时候,所以我们看put方法:

a5fcb44edafddc20b87895f13d6aad11.png

底层又调用了putVal方法,值得注意的是它先用一个hash方法获得了一个哈希值:

f265c902c659579569808babd33896d5.png

hash方法中对哈希值的处理主要是为了减少哈希碰撞(简要理解哈希碰撞就是对key转换后的哈希值相同,链表就是来解决哈希碰撞的)。底层调用了key对象的hashCode方法。如果key为null,返回0;否则进行一些特殊运算转换为哈希值。再看putVal方法方法:

206f33e72d923bf5d39afb77d1719e0d.png

看起来很复杂,但大体上就是如下图:

d96d1d2250197010306ec4cfe8eb056e.png

主要看最后的元素个数大于阈值,扩容的if语句,这才是重点。通过源代码我们可以发现,通过key的哈希值确定数组下标的表达式为:

(数组长度-1) & key的哈希值

HashMap在元素个数达到阈值(容量*加载因子)的时候使用resize方法扩容。而因为HashMap底层结构复杂,扩容也复杂,这里就截取一部分说明:

7f6645de3f711dcacb0427ced967c14e.png

我们从这可以看出,当oldCap进行左移一位运算没有超过最大容量,并且oldCap已经大于默认容量时,新容量就变成原容量的两倍。也就是说,如果初始化HashMap用无参构造的话,初始容量为16,加载因子为默认加载因子0.75,当元素个数>16*0.75=12的时候,就会扩容成容量为32的集合。

JDK8前的HashMap

以上说的都是JDK8后的HashMap,那之前的HashMap有什么不同呢?JDK8前的HashMap不是用节点Node存储键值的,而是用Entry来储存;并且没有红黑树结构。只有数组+链表;发生哈希碰撞时在链表的头部插入(JDK8后从链表的尾部插入)。由于没有红黑树,我们在寻找存于某个链表中的键值时,最糟的时间复杂度为O(n),但红黑树因为左小右大的特性,最糟的时间复杂度为O(logn)。【n表示元素个数】

其他

经过上述我们知道了HashMap在进行元素的一些操作时都会调用key元素的hashCode和equals方法,所以如果是我们自定义的类,需要重写hashCode和equals方法。而重写hashCode这里就不再讲解,编译器可以自动生成(两者一般一起生成)。

equals之前提过是Object的方法,hashCode也是,但当时我们没有说,现在我们简要看看:

c8fdf3d1435b907bed7bded9b444bf20.png

看常规协定中的前两点我们可以知道equals和hashCode

方法一般是一起重写的,而且如果equals方法比较两个对象返回true,那么两个对象的hashCode返回值也是一样的

在HashMap中允许key和value为null,但因为key为null的时候hash(key)返回0,任何数与0进行&(与)运算都为0,所以key为null的键值只会存储在数组下标为0的位置。所以在HashMap中key为null的键值只有一个,但value为null的键值可以多个。

因为HashMap的无序不可重复,所以又有同一个链表/红黑树中的key的哈希值是一样的,同一个链表/红黑树下的key调用equals方法进行比较一定返回false

HashMap的底层是数组加链表的这种结构也被称为哈希表/散列表。之所以要重写hashCode是因为如果哈希值如果变成了定值,那么哈希表就会变成一个纯单向链表/红黑树;哈希值如果都不一样,那么哈希表就会变成一个纯数组。而没有重写hashCode的话哈希表就会变成一个纯数组。以上两种情况我们都称为散列分布不均匀

我们在看HashMap的常量和属性中,对于初始化容量中的注释我们需要知道:

The default initial capacity - MUST be a power of two.

默认初始化的容量-必须是2的幂。这是不仅为了提高效率,而且是为了达到散列分布均匀。

Hashtable

Hashtable大致上都和HashMap一致,这里重点讲解两者的区别:

b155ae2878a9d6452eed91efdbdf9a0f.png

底层不是Node数组,而是Entry数组。

19c37ad82190971faff6abb3de4c1209.png

默认初始容量为11,默认加载因子为0.75

cd1765d032d774be4134017cbc3dd5be.png

扩容机制用rehash方法,新容量是原容量的两倍+1.

5a1a01ef7edc58bcee9716f9d4f356ca.png

看注释的@exception那里,会发现在Hashtable中不能向key或value储存null,会报空指针异常。而且所有供外部使用的方法都有synchronized关键字修饰。也即是说Hashtable是线程安全的,而HashMap是非线程安全的。所以Hashtable的效率比HashMap低。并且Hashtable的哈希值直接由key的hashCode方法确定

Properties

作为Hashtable的子类,Hashtable有的它都有了,直接看它与Hashtable的不同:

30efc1048a1b3eebf7fa33363c96be4b.png

从该方法我们可以知道Properties是专门用来储存String类型的key和value。因此也称为属性类。我们可以通过以上的setProperty来设置键值。

97a821f3a6bd062c562a2959068ce53b.png

用getProperty来通过key获取value。如果我们在设置键值的时候调用put方法传入非字符串value的时候,那么调用getProperty就会把value转换为null。如果调用put方法传入非字符串key的时候那getProperty就不能用了。不过是一般是没有人用Properties来放非字符串类型数据的。

TreeMap

对于TreeMap,也是一个挺重要的部分。作为SortedMap下的实现类,不仅实现了无序不可重复,而且实现了自动排序。其底层是一个红黑树

鉴于红黑树出现那么多次,现在简单介绍下红黑树的一些特点。先看看简单的二叉查找树:

72129c9cbeaa25cb1d691aa32b9b67eb.gif

二叉查找树的特点就是只有左小右大的原则进行排序,因此可能会出现以下的情况:

c5cfd09c2ce65391794993e69e81bb08.png

这时候说它是二叉查找树也不像,更像一个链表。这时候,红黑树就可以极大程度的解决这个问题:

0f2298dbf242274a82bc51e4384b5da7.gif

从以上图片就可以知道红黑树的每个节点不是红就是黑的;根节点总是黑色的;如果节点是红色的,那么字节点一定是黑色(反之不一定);为了保证树的平衡性,先根据左小右大的原则进行排序后,会对树进行检测其平衡性,如果不平衡,会进行一系列的旋转,改色等操作来保持平衡。感兴趣的读者可以进入

https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

这个网站,里面模拟了很多数据结构。

6df1187d3bddc0643dd917ccf268c3ad.png 

接下来我们就慢慢看TreeMap这个实现类。

基本属性

d3dcd14946cfc30a4b7733851e3682b1.png

comparator,比较器。上面的SortedMap的comparator

方法可以返回它。不过重点不是这个,是等下怎么实现这个比较器。root是红黑树的根。size红黑树的节点个数。modCount统计TreeMap结构改变次数。

Entry静态内部类

2f446c626eebc06e1f2c5d2db1245e1a.png

可以看到在TreeMap中,一个Entry有着六个属性。分别对应着我们上述的结构。left表示左节点,right表示右节点,parent表示上层节点,color表示当前节点是红还是黑(黑为true,红为false)。

构造方法

567f4237b5ad98c1348581febac3ee7d.png

无参的构造方法是没有比较器的,我们可以通过在构造方法中传入比较器使TreeMap集合有比较器,那么有比较器和没有比较器有什么区别呢?我们先看put方法。

排序机制

在讲解排序机制前,我们先看看TreeMap能不能添加自定义的类:

3d93835626a98117a01312edfe4c5952.png

我们会发现居然报了类型转换异常,而且是不能转换为Comparable类异常。

77bdb796d0ead7ab1ef9b3d7643a2efb.png

但是添加进去String类或者Integer为什么就可以了呢?其实很简单,它们底层都实现了上述的Comparable接口。

306b69f0d660e06ac4aca0def41aae62.png

934e2c1f7fe0bb87d0b7b800072d48e7.png

接下来我们就通过put的源代码来弄清楚为什么会报错,并了解TreeMap底层是如何实现排序的:

9afed6cf7293c58ccce5a20acd207c54.png

这里就不像HashMap那样可以看下简单的扩容和数组下标计算方法就行,这里设计比较的机制,所以我们需要将整个代码都看一遍。里面也有一个重要方法compare:

64f07f956cc582e7ee0ceae8b179af33.png

看上图put方法的第14行+第17行和第26行+第30行,可以发现当TreeMap中有比较器的时候会调用比较器的compare方法来确定key的位置,而当TreeMap中没有比较器的时候会先把key强制转换为Comparable类,并使用Comparable类的compareTo方法来确定key的位置。所以到这里我们大概就可以总结出TreeMap实现排序的两种方式:

1、添加进去的key对应的类必须实现Comparable接口。

2、自己写一个类实现Comparator接口。在构造TreeMap的时候传进去

那里面的排序规则怎么写呢?我们先看有比较器和没有比较器put方法是怎么添加的我们再来确定怎么写规则:

8ad21e76d694c266bb7cab34ec2d1048.png

可以看出就是红黑树的结构,这时候我们就开始思考要怎么写排序比较好了。

1

先从第一种开始:假如我有一个猫类,确定它的大小想要根据年龄的大小来排,我们就可以像以下的格式写:

8ec774090e75c198dbfec189023804d8.png

仔细思考下,是不是就和源代码的排序规则对上了。不同的规则实现方式也有所不同,比如:判断猫类对象的大小我们想先根据年龄的大小排,但如果年龄相同,想根据它的体重排,那么代码就可以改成如下的形式:

d8bb3b7c53a5cd8b897d43816cf12f7d.png

当然如果比较规则中需要比较的有字符串类型的,可以直接调用String类实现的compareTo方法就可以。

2

第二种就是实现一个比较器了,还是按以上的猫类来看,我们简单点,就根据年龄来确定大小,于是就有:

bbaff2b0afd43ce4ed79fcf499df8c7f.png

有没有读者感觉这样实现Comparator接口有点麻烦呢?确定有点,但我们可以使用匿名内部类来简化

3053ed7b9213d469cf59f19ae1e32dd4.png

是不是感觉好多了?但光说没用,我们通过Map的两种遍历方式来看上述实现比较之后的效果。

Map实现类的遍历方式 

keySet 71ca84e33142a618428c60c3a23c7e68.gif

第一种就是用Map的方法keySet

5000b88ed2f7027e7dc3fd1b3ac2f1e6.png这个方法返回一个关于key的Set集合,这时候map可以根据key获取value,因此我们就可以用foreach或iterator进行遍历:

de689e081bfa2898bdf14428891915a1.png

注意用iterator的时候需要将next指向的key先取出来,如果直接用next:

dde30358319a574dca17e7479290a260.png

因为每调用一次next方法,迭代器的指针会向后移,所以我们也发现了第一次输出的cat对象对应的value是下一个cat对象对应的value。而走第二次while的时候,代码块里又执行了两次next,加上第一次while的时候走的两次,总共有4下next,所以指针往后移了4次,当移动最后一次的时候指针没有指向元素,因此就会报NoSuchElementException异常。

entrySet 71ca84e33142a618428c60c3a23c7e68.gif

c6d3c8dbd388d5b258e8face7d30fbaf.png

这个方法返回一个Map.Entry的Set集合。而Map.Entry是Map的一个内部类:

1da0ce5c03cc078deb2295a26e4bc559.png

需要知道的是,这里返回的是一个接口,说明用了多态,其内部是Map.Entry的实现类,方法肯定也实现了,在这里我们只需要知道我们可以通过getKey和getValue方法来获取Map.Entry的key和value

0d2d1101080e493fdbb7485e18c25d13.png

也是一样用迭代器或foreach遍历。Iterator的遍历注意事项和上面的keySet一致。

81408fbfc6ec80fb5fd40a6c51720fb7.gif

相同的元素,把TreeMap改成HashMap就没有排序的效果,而且key相同覆盖value:

d846c93cd6871355e7c953c52520e36d.png

TreeMap续

看完了遍历方式,我们会发现输出TreeMap的键值的时候是根据key的从小到大规则排的,原因之一是我们在实现比较的时候就是实现左小右大,还有一点就是TreeMap在遍历的时候是采用中序遍历(左根右)。这里就不拓展了。而且后者我们是无法修改的,但前者我们可以修改,就是在实现比较的时候两者的位置调换:

882a077b3eb4a55ed3ebfdf636731632.png

这时候就实现了从大到小排了。

对于TreeMap还有一点需要知道的,在以上的代码中我们并没有发现TreeMap有调用key的equals方法,所以在TreeMap中,存放自定义类的时候可以不写equals方法,但最好还是写上。

终于! b7dd7d59e749bfac22d7d37d5903e075.png

到这Map终于是差不多完结了,这时候我们回过头看Set中的HashSet和TreeSet。不用慌,就看两者底层的add方法就能说明问题(HashSet和TreeSet的add底层差不多,直接看HashSet的):

9b925a55adfafc228887721d21004ec3.png

可以看到底层调用Map的put方法那个PRESENT其实是一个用处不大的Object类:

2a5df4b1b0221a242fbe1fe3705ca6d5.png

而put方法返回的是value的值,这里直接赋值为null。

因此,我们可以认为Set是value为null的Map。所以Set的无序不可重复更多的是依赖于Map的key无序不可重复。不过还要记得Set的常用方法是在Collection里面。

ArrayListHashMapTreeMap

自定义类

要求

最好重写equals

一定需要

重写equals和hashCode

不一定重写

equals,

但一定要实现比较。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值