哈希 | unordered_set + unordered_map 的模拟实现(上)

目录

什么是 unordered_set + unordered_map ?

unordered_set :

unordered_map :

哈希

哈希表:

哈希冲突:

如何解决哈希冲突:

闭散列:

负载因子:

闭散列的模拟实现:

为什么需要标记状态?

为什么需要 HashFunc?

查找 Find:

插入 Insert:

删除 Erase:

 开散列:

 结点的实现:

开散列的模拟实现: 

析构函数:

 查找 Find:

插入 Insert:

开散列如何考虑哈希冲突的情况呢?

开散列如何扩容呢?

删除Erase:

什么是 unordered_set + unordered_map ?

unordered_set :

unordered_set - C++ Reference (cplusplus.com)

1、unordered_set 是每个数据只出现 1 次(去重),且不排序的容器

2、unordered_set 中的数据不可以修改,但是可以插入和删除

3、unordered_set 虽然数据无序,但是每个数据是根据 哈希值 存储的,哈希值可以帮助我们快速找到数据

4、unordered_set 的查找比 set 快,但在遍历上比 set 效率低

5、unordered_set 至少是正向迭代器(没有反向迭代器)

unordered_map :

unordered_map - C++ Reference (cplusplus.com)

1、unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与
其对应的value。
2、在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3、在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内
找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4、unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭
代方面效率较低。
5、unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问
value。
6、它的迭代器至少是前向迭代器。

哈希

数组的存储中,假设 值为 i 的数据存储在下标为 i-1 的位置中,比如值为 4 ,则存储在 3 位置,当数据为 1 2 3 4 5 7 9 时,我们最多只需要开大小为 9 的数组就可以存储所有的数据,当数据为 1 2 100  999 1000  2000  2999  3000 时,我们至少需要开大小为 3000 的数据才可以存储所有的数据,且由于数据不集中,导致数组的空间严重浪费。我们可以采用哈希来解决这个问题。 

哈希表:

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一 一映射的关系,那么在查找时通过该函数可以很快找到该元素。我们称这个函数为哈希函数。
在该结构中:

1、插入元素 : 根据待插入元素的 key ,以哈希函数计算出该元素的存储位置并按此位置进行存放

2、搜索元素 :对元素的 key 进行同样的计算,把求得的函数值当做元素的存储位置,在结构中根据此位置取出元素 和 key 比较,若关键码相等,则搜索成功

按照哈希法构造出来的结构称为 哈希表(Hash Table)

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity(capacity为存储元素底层空间总的大小)

用该方法进行搜索不必进行多次关键码的比较,只需要根据哈希函数计算出存储位置,直接到存储位置查找,因此搜索的速度比较快。
问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

哈希冲突:

假设两个数据元素的关键字 4 和 44,虽然 4 和 44 是不同的值,但有 Hash( 4 ) == Hash( 44 ),即不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

如何解决哈希冲突:

闭散列:

当发生哈希冲突时,如果哈希表未被填满,说明哈希表中仍存在空位,我们可以把 key 存储在发生冲突位置的下一个空位置,比如存储 44 时,4 和 44 发生哈希冲突了,且位置 5 6 7 都不为空,位置 8 为空,那么把 44 存储在位置 8

当存储 44 时,如果哈希表全部被填满了,此时需要扩容,同时说明 44 发生了很多次哈希冲突,把所有位置都探测了一遍,这样会导致效率低,有什么方法可以减少哈希冲突的次数?

负载因子:

负载因子 = 填入表中的元素个数 / 哈希表的长度

当哈希表的长度固定时,

1、当负载因子越大时,说明哈希表中的元素越多,发生哈希冲突的可能性越大

2、当负载因子越小时,说明哈希表中的元素越少,发生哈希冲突的可能性越小

所以可以通过负载因子决定是否扩容,而不是在哈希表完全填满时才扩容。应该严格限制负载因子为 0.7 ~ 0.8 以下,才可以提高插入和查找的效率。

闭散列的模拟实现:

为什么需要标记状态?

在实现之前,有一个问题值得注意:

在查找 key 时,我们根据哈希函数计算出 key 应该存储在位置 Hash(key)中,

1、当位置 Hash(key)为空时,我们认为哈希表中不存在 key,结束查找

2、当位置 Hash(key)不为空,且该位置的数据 =  key 时,结束查找

3、当位置 Hash(key)不为空,且该位置的数据和 key 不相等时,说明 key 发生过哈希冲突,++Hash(key),一直往后找,直到 Hash(key)为空 或者 位置 Hash(key)的值 = key 时,结束查找

假设数据为 5 15 4 6 11 8 ,在插入 15 时,5 和 15 发生了冲突,所以 15 存储在位置 6 ,在插入 6 时,15 和 6 发生哈希冲突了,所以 6 存储在 位置 7 ,在全部数据插入完之后,如果删除 15,则位置 6 空了,如果此时查找 6 ,按照哈希函数,6 应该存储在位置 6 ,而此时位置 6 为空,根据查找的思路,我们会认为哈希表中没有 6 这个数据,并不会继续往后找,因为程序不知道存储 6 时发生了哈希冲突,6 被存储到位置 7 了

所以每个位置除了存储数据之外,我们需要标记每个位置的状态,来避免这个问题:

1、如果位置 Hash(key)的状态为 EXIST,且该位置的值 = key,结束查找

2、如果位置 Hash(key)的状态为 DELETE,则需要往后找

3、如果位置 Hash(key)的状态为 EMPTY,则该位置确实为空,结束查找

    enum State
	{
		EMPTY,//当前位置为空
		EXIST,//当前位置已经被占了
		DELETE//当前位置被删除
	};
	
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

    template<class K,class V,class Hash=HashFunc<K>>
    class HashTable
    {
    public:
		HashTable(size_t size = 10)
		{
			_tables.resize(size);//先开一定的size
		}
    private:
	    vector<HashData<K, V>> _tables;
	    size_t _n = 0;//存储哈希表元素的个数
    };
为什么需要 HashFunc?

在哈希函数中,我们认为 key 不是字符型,而是 整型、浮点型等可以进行计算的类型,从而计算出哈希值,如果 key 为 string 类型,那么无法计算哈希值了,因为字符串无法进行加减乘除,怎么解决这个问题呢?

我们写个仿函数,让 string 可以转换为可以计算的类型:

由于字符串是用 ASCII 存储的,我们可以利用 ASCII 来计算哈希值。为了辨别众多的字符串,我们不能只用字符串的第一个字符的 ASCII 来计算哈希值,这样会加剧哈希冲突,我们可以把字符串的所有字符的 ASCII 进行相加,相加得到的和来计算哈希值。

这样会导致一个问题:对于类似 "abcd" "acdb" "aadd" 这样的字符串,ASCII 相加的结果相等,没办法做很好的区分,可以在每次相加之后乘以一个权值,减少和相等的概率,这里用 131 来作为权值。

    template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& str)
		{
			size_t sum = 0;
			for (auto e : str)
			{
				sum += e;
				sum *= 131;
			}
			return sum;
		}
	};
查找 Find:

哈希表是由 vector 实现的,在计算哈希值时,可以对 capacity 取模吗?

size_t hashi = hs(key) % _tables.capacity();

不可以,假设 vector 为 nums,用下标遍历 nums 时,一般用 i<nums.size()来判断 for循环是否结束,而不是用 i<nums.capacity() 来进行判断

for(size_t i=0;i<nums.size();i++)

意味着,如果我们对 capacity 进行取模,哈希值可能会超过 size 的范围,即越界了,那我们在遍历哈希表时就无法访问超出 size 范围的值。

        HashData<K,V>* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();//不用自己写size 函数,因为vector有自带的size

			while (_tables[hashi]._state !=EMPTY )
			{//值可能hashi位置,也可能在hashi的后面

				if (_tables[hashi]._kv.first == key
					&& _tables[hashi]._state==EXIST)
				{//值相等且值存在
					return &_tables[hashi];
				}
				++hashi;
				hashi %= _tables.size();//取模,如果++hashi之后,hashi越界了,会重新回到0开始找
			}
			return nullptr;//值在哈希表中不存在
		}
插入 Insert:

如果在插入前,哈希表的负载因子>= 0.7了,此时需要扩容,扩容不是仅扩大 vector 的空间,仅扩大空间,扩容后再计算哈希值时,所有的关系都错乱了,因为 size 已经不是原来的 size 了,所以不仅需要扩大空间,还需要把原本哈希表里的数据重新映射到新的哈希表中

我们在扩容时,可以再次调用 Insert 函数来把旧表的数据映射到新表。因为扩容后,空间已经变大了,再次调用 Insert 函数时,负载因子已经小于 0.7 了,不会再次进入扩容函数,不会出现死循环,而是把旧表的数据映射到新表中。

在插入时,如果哈希值所在的位置已经被占了(EXIST),需要继续往后走,直到找到位置的状态为 EMPTY / DELETE 时,才可以插入。

        bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;//值在哈希表已经存在了
			
			Hash hs;
			if (_n*10 / _tables.size() >=7)
			{//如果没有*10,比如7/10,整型和整型相除结果小于1,那么商为0,*10可以避免这种尴尬
				//超出负载因子,扩容
				HashTable<K,V,Hash> newHt(2 * _tables.size());
				for (auto& e:_tables)
				{
					if (e._state == EXIST)
					{
						//把旧表的数据重新映射到新表中
						newHt.Insert(e._kv);//插入到新表中
					}
				}
				_tables.swap(newHt._tables);//只需要交换vector
			}
			size_t hashi = hs(kv.first) % _tables.size();
			
			while (_tables[hashi]._state == EXIST)
			{//如果 hashi 位置已经被占了
				++hashi;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;//哈希表个数++
			return true;					
		}
删除 Erase:

复用 Find 来确定 key 是否在哈希表中:

1、当哈希表中不存在 key 时,返回假

2、当哈希表中存在 key 时,把 key 所在的位置的状态设为 DELETE,把哈希表的数据的个数 -1,随后返回真

        bool Erase(const K& key)
		{
			//auto ret = Find(key);
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				//值在哈希表中存在
				--_n;
				ret->_state = DELETE;
				return true;
			}
			else
			{
				return false;
			}
		}

 开散列:

开散列法又叫链地址法(开链法),首先对关键码集合用哈希函数计算哈希值,具有相同哈希值的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。即原本每个位置存储的是数据,现在每个位置存储的是链表。

 结点的实现:

    template<class K,class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode* _next;//指向下一个结点

		HashNode(const pair<K,V>& kv)
			:_kv(kv)
			,_next(nullptr)
		{ }
	};

开散列的模拟实现: 

析构函数:

如果不写析构函数,vector 默认的析构函数不会释放链表,而链表的结点是手动开辟的,这会导致内存泄漏,所以需要写析构函数,处理链表。

    template<class K,class V,class Hash= HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable(size_t size = 10)
		{
			_tables.resize(size,nullptr);
			_n = 0;
		}
        ~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;//把链表置空
			}
		}
    private:
		vector<Node*> _tables;//指针数组
		size_t _n;//数据的个数
	};
 查找 Find:

和闭散列不同的是,我们用 key 计算出哈希值后,即找到了对应的哈希桶,我们需要遍历哈希桶(即遍历链表)来确定 key 是否存在

        Node* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}
插入 Insert:

在开散列的插入中,我们先开辟一个结点,根据 key value 的哈希值找到要插入的桶,在对应的链表中实现头插。

开散列如何考虑哈希冲突的情况呢?

假设桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,
再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可
以给哈希表增容。

开散列如何扩容呢?

我们开一个新表,然后遍历旧表,把旧表的结点的 key value 根据哈希函数重新映射到新表中,我们可以像闭散列一样在扩容时再次调用 Insert ,来把旧表的结点映射到新表吗?

不可以,在开散列的插入中,需要开辟结点,假设旧表有 1000 个结点,那么新表也需要开辟 1000 个结点,把旧表的结点全部映射到新表后,旧表的结点就全部释放掉了,这样会造成浪费,因为我们不必要开新表的 1000 个结点,只需要把旧表的结点插入到新表即可

        bool Insert(const pair<K,V>& kv)
		{
			if (Find(kv.first))
			{
				//值存在,不必插入
				return false;
			}
			
			Hash hs;
			//扩容
			if (_n == _tables.size())
			{
				//HashTable<K, V> newht(2 * _tables.size());
				vector<HashNode<K, V>*> newht(2 * _tables.size(), nullptr);
				//只需要创建新表vector,不需要重新创建新的哈希表
				
				for (size_t i = 0;i < _tables.size();i++)
				{//遍历旧表

					Node* cur = _tables[i];
					while (cur)
					{
						size_t hashi = hs(cur->_kv.first) % newht.size();
						//插入新表
						Node* next = cur->_next;
						cur->_next = newht[hashi];
						newht[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;//把旧表置空
				}
				_tables.swap(newht);
			}

			//空间充足
			Node* newnode = new Node(kv);
			size_t hashi = hs(kv.first) % _tables.size();
			//头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}
删除Erase:

我们根据 key 的哈希值计算出 key 对应的桶,接着用 cur 遍历桶对应的链表,在删除时,由于是单链表,所以需要 prev 标记 cur 结点的前一个结点,并把 prev 初始化为空,假设 cur 是要删除的结点:

1、当 prev 为空,则链表为头删,把链表的头节点交给 cur 的下一个结点

2、当 prev 不为空,则 prev 的下一个结点指向 cur 的下一个结点,随后把 cur 结点释放即可

3、当 cur 为空,即遍历了整个链表都没有匹配的 key value 时,说明哈希表中不存在我们要删除的节点

        bool Erase(const K& key)
		{	
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev)
					{
						prev->_next = cur->_next;
					}
					else
					{
						//头删
						_tables[hashi] = cur->_next;
					}

					delete cur;
					--_n;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

html公众号页面实现点击按钮跳转到导航

实现效果&#xff1a; 点击导航自动跳转到&#xff1a; html页面代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>跳转导航</title><meta name"keywords" conten…

【学习笔记十五】批次管理和容量管理

一、批次管理 1.配置 SAP EWM 特定参数 激活仓库的批次管理 2.ERP端物料需要启用批次管理 3.EWM物料需要启用批次管理 一般是ERP启用批次管理&#xff0c;相关的配置也会传输到EWM系统 4.建立批次主数据 5.创建采购订单并创建内向交货单&#xff0c;维护批次 6.维护产品主数…

【Canvas技法】绘制正三角形、切角正三角形、圆角正三角形

【图例】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>绘制正三角形、切角正三角形、圆角正三角形</title><style …

计算机网络—传输层UDP协议:原理、应用

​ &#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;2月のセプテンバー 1:21━━━━━━️&#x1f49f;──────── 5:21 &#x1f504; ◀️ ⏸ ▶️ ☰ &am…

leetcode.45题:跳跃游戏II

Leetcode.45题&#xff1a;跳跃游戏II /* 题意的理解&#xff1a; nums[0] 只能跳 1 ~ nums[0]步 依次类推&#xff1a;从nums[0] - nums[n - 1] 最少需要多少步数 nums 2 3 1 1 4 nums[0] 2,初始只能跳 1/2步&#xff0c;如跳1步&#xff0c;达到nums[1] 而nums[1] 3,顾第二…

网络篇04 | 应用层 mqtt(物联网)

网络篇04 | 应用层 mqtt&#xff08;物联网&#xff09; 1. MQTT协议介绍1.1 MQTT简介1.2 MQTT协议设计规范1.3 MQTT协议主要特性 2 MQTT协议原理2.1 MQTT协议实现方式2.2 发布/订阅、主题、会话2.3 MQTT协议中的方法 3. MQTT协议数据包结构3.1 固定头&#xff08;Fixed header…

uboot操作指令2

文章目录 一、FAT 格式文件系统操作命令1.fatinfo 命令2.fatls 命令3.fstype 命令4.fatload命令-将EMMC数据复制到DRAM中4.fatwrite命令-将DRAM数据复制到EMMC中 二、Boot操作指令1.bootz2.boot命令 一、FAT 格式文件系统操作命令 &#x1f4a6; 有时候需要在 uboot 中对 SD 卡…

MYSQL08_页的概述、内部结构、文件头、文件尾、最大最小记录、页目录、区段表

文章目录 ①. 页的概述、大小②. 页的内部结构③. 第一部分 - 文件头④. 第一部分 - 文件尾⑤. 第二部分 - 空闲、用户记录、最大最小⑥. 第三部分 - 页目录⑦. 第三部分 - 页面头部⑧. 从数据页角度看B树⑨. 区、段和表、碎片区 ①. 页的概述、大小 ①. 数据库的存储结构&…

云原生:10分钟了解一下Kubernetes架构

Kubernetes&#xff0c;作为当今容器编排技术的事实标准&#xff0c;以其强大的功能和灵活的架构设计&#xff0c;在全球范围内得到了广泛的应用和认可。本文将深入简出地探讨Kubernetes的核心架构&#xff0c;帮助大家了解Kubernetes&#xff0c;为今后的高效的学习打下良好的…

计算机网络 Cisco虚拟局域网划分

一、实验内容 1、分别把交换机命名为SWA、SWB 2、划分虚拟局域网 valn &#xff0c;并将端口静态划分到 vlan 中 划分vlan 方法一&#xff1a;在全局模式下划分vlan&#xff0c;在SWA交换机上创建三个vlan&#xff0c;分别为vlan2&#xff0c;vlan3&#xff0c;vlan4。 方…

1.初识Docker与容器

初识Docker与容器 文章目录 初识Docker与容器1、什么是DockerDocker架构 2、为什么使用DockerDocker容器虚拟化的好处Docker与虚拟机比较Docker为什么快 1、什么是Docker Docker是基于Go语言实现的开源容器项目。Docker是为解决了运行环境和配置问题的软件容器&#xff0c;方便…

24.4.11-13C语言学习笔记|函数、部分结构体【未完待续】

巴拉巴拉~~~哭死&#xff0c;学习啊啊啊啊&#xff0c;学校课好多&#xff0c;只能半夜学了 4.2函数名--特殊的地址&#xff1a; void fun(int a){ int aa1&#xff1b; printf("%d"&#xff0c;a); return a&#xff1b; } 指针函数&#xff1f;&#xff1f; void …

(五)C++自制植物大战僵尸游戏LoadingScene的实现讲解

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/xjvbb 一、类介绍 游戏启动后就会立即切换到游戏加载场景中。只有游戏资源文件加载完成后&#xff0c;才能进入游戏。Loadingscene类继承Cocos2d-x中的Scene父类&#xff0c;表明Loadingscene是一个场景类。切换到Loadi…

Mathorcup 甲骨文识别

本资源主要包含第2-4问&#xff0c;第一问直接使用传统图像处理即可&#xff0c;需要有很多步骤&#xff0c;这一步大家自己写就行。 2 第2问&#xff0c;甲骨文识别 2.1 先处理源文件 原文件有jpg和json文件&#xff0c;都在一个文件夹下&#xff0c;需要对json文件进行处理…

大数据存储解决方案和处理流程——解读大数据架构(四)

文章目录 前言数据存储解决方案数据集市运营数据存储&#xff08;Operational Data Store&#xff09;数据中心 数据处理数据虚拟化和数据联合虚拟化作为 ETL 或数据移动的替代品数据目录数据市场 前言 在数字时代&#xff0c;数据已成为公司的命脉。但是&#xff0c;仅仅拥有…

读《AI营销画布》品牌企业成长的逻辑(四)

前言 曾几何时&#xff0c;为了销售和品牌这两个扯的一世不可开交&#xff0c;也因为这个在企业里&#xff0c;形成了二个主张派&#xff0c;一派是以为销售为目标&#xff1b;一派是以品牌为目标。最后&#xff0c;&#xff0c;&#xff0c;&#xff0c;也就形成了不同的意见&…

c# .net 香橙派 Orangepi GPIO高低电平、上升沿触发\下降沿触发 监听回调方法

c# .net 香橙派GPIO高低电平、上升沿触发\下降沿触发 监听回调方法 通过gpio readall 查看 gpio编码 这里用orangepi zero3 ,gpio= 70为例 当gpio 70 输入高电平时,触发回调 c# .net 代码 方法1: Nuget 包 System.Device.Gpio ,微软官方库对香橙派支持越来越好了,用得…

学习JavaEE的日子 Day38 网络编程

Day38 网络编程(了解即可) 1. 计算机网络 2. 网络编程 实现多台计算机之间实现数据的共享和传递&#xff0c;网络应用程序主要组成为&#xff1a;网络编程IO流多线程 3. 网络模型 两台计算机之间的通信是根据什么规则来走的(OSI & TCP/IP) 此处简单了解该模型就行《TCP/IP…

Windows瘦客户机系统默认英文?一招改成中文界面

前言 最近发现有很多小伙伴给电脑安装了Windows瘦客户机系统&#xff0c;但开机之后发现系统是英文的&#xff0c;看都看不懂。 今天就给小伙伴们带来更改Windows Thin系统语言的办法。 首先&#xff0c;咱们都知道&#xff0c;更改系统显示语言基本上都是在系统设置或者控制…

Java——类和对象

目录 一.类定义和使用 1.简单认识类 2.类的定义格式 3.注意事项 二.课堂练习 1.定义一个狗类 2.定义一个学生类 3.注意事项&#xff1a; 三.类的实例化 1.什么是实例化 2.注意事项 3.类和对象的说明 四.this引用 1.为什么要有this引用 2.什么是this引用 五.对…
最新文章