JavaSE——集合5:Set(HashSet的底层原理)(重要!!!)

目录

一、Set接口基本介绍

二、Set接口的常用方法

三、Set接口实现类——HashSet

四、HashSet(HashMap底层原理:重要!!!)

(一)第一次添加元素

(二)第二次添加不同的元素

(三)添加重复的元素

1.仍旧走到了putVal(hash(key), key, value, false, true);方法

2.判断计算出的索引位置,是否为null,不为null,继续跳过下一个if,进入else中的第1个if

​3.添加失败的源码解读: 

4.关于扩容的源码解读 

5.转成红黑树源码解读

​五、练习题

1.练习1

2.练习2


一、Set接口基本介绍

  1. 无序(添加和取出的顺序不一致),没有索引
  2. 不允许重复元素,所以最多包含一个null
  3. JDK API中Set接口的实现类有:

二、Set接口的常用方法

  1. 和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
  2. set 接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null
  3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
  4. 注意:取出的顺序虽然不是添加的顺序,但是它是固定的
  5. Set接口的遍历方式:迭代器、增强for,不能使用索引的方式来获取
public static void main(String[] args) {Set set = new HashSet();set.add("john");set.add("lucy");set.add("john");set.add("jack");set.add("marry");set.add(null);set.add(null);System.out.println("set=" + set);// set=[null, marry, john, lucy, jack]set.remove(null);// 遍历// 方式1: 使用迭代器Iterator iterator = set.iterator();while (iterator.hasNext()) {Object obj = iterator.next();System.out.println(obj);}// marry// john// lucy// jack// 方式2:增强forfor (Object o : set) {System.out.println("o=" + o);}// o=marry// o=john// o=lucy// o=jack// set 接口对象,不能通过索引来获取
}

三、Set接口实现类——HashSet

  1. HashSet实现了Set接口
  2. HashSet底层是HashMap,而HashMap底层是数组+链表+红黑树
  3. HashSet可以存放null值,但是只能有一个null
  4. HashSet不保证元素是有序的,取决于hash后,再确定索引的结果
  5. 不能有重复元素/对象 
public static void main(String[] args) {HashSet set = new HashSet();//说明//1. 在执行add方法后,会返回一个boolean值//2. 如果添加成功,返回 true, 否则返回false//3. 可以通过 remove 指定删除哪个对象System.out.println(set.add("john"));//TSystem.out.println(set.add("lucy"));//TSystem.out.println(set.add("john"));//FSystem.out.println(set.add("jack"));//TSystem.out.println(set.add("Rose"));//Tset.remove("john");System.out.println("set=" + set);// set=[Rose, lucy, jack]set = new HashSet();System.out.println("set=" + set);// set=[]//4 Hashset 不能添加相同的元素/数据?set.add("lucy");// 添加成功set.add("lucy");// 添加失败set.add(new Dog("tom"));// 添加成功set.add(new Dog("tom"));// 添加成功System.out.println("set=" + set);// set=[Dog{name='tom'}, Dog{name='tom'}, lucy]// 看源码,做分析,即 add 到底发生了什么?=> 底层机制set.add(new String("hello"));// 添加成功set.add(new String("hello"));// 添加失败System.out.println("set=" + set);// set=[Dog{name='tom'}, Dog{name='tom'}, hello, lucy]
}class Dog {private String name;public Dog(String name) {this.name = name;}@Overridepublic String toString() {return "Dog{" +"name='" + name + '\'' +'}';}
}

四、HashSet(HashMap底层原理:重要!!!)

(一)第一次添加元素

1. 执行 HashSet()public HashSet() {map = new HashMap<>();}2. 执行 add()// private static final Object PRESENT = new Object();// PRESENT 是一个静态常量,类型是Object,用于在map中进行占位的public boolean add(E e) { // e = "john"return map.put(e, PRESENT)==null;}3.执行 put()// key = "john" value = PRESENT 共享public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}4.执行hash(key)该方法会key对象的hashCode进行运算,得到新的hash值注意:static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}5.回到 putVal(hash(key), key, value, false, true);final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i; // 定义了辅助变量// table 就是 HashMap 的一个数组,类型是 Node[],初始长度为null// if 语句表示如果当前table 是null, 或者 大小=0// 进行第一次扩容,到16个空间 (在6中有讲解)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length; // 16// (1) 根据key,即传入的对象,得到新的hash// (2) i = (n - 1) & hash:计算该key应该存放到table表的哪个索引位置,// (3) p = tab[i = (n - 1) & hash] 把这个位置的对象,赋给 p// (4) 判断p 所在的索引位置是否为null// (4.1) 如果p 为null, 表示还没有存放元素, 就创建一个Node(key="john",value=PRESENT)// (4.2) 将key存放在该位置 tab[i] = newNode(hash, key, value, null)if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);// newNode()方法:// Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {// return new Node<>(hash, key, value, next);// }else{......}++modCount; // 计算插入的次数// size 就是每加入一个结点Node(k,v,h,next), size++if (++size > threshold) // 查看添加次数是否到达临界值12resize(); // 扩容afterNodeInsertion(evict);// void afterNodeInsertion(boolean evict) { } 是一个空方法,// 是为了让HashMap的子类,例如LinkedHashMap来实现return null; // 返回null就是存放成功的意思}6.第一次扩容的resize()方法:
因为数组长度为0,所以进入到最后一个else
数组扩容到16,临界值为12=0.75*16
else { // static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16newCap = DEFAULT_INITIAL_CAPACITY; // 16// static final float DEFAULT_LOAD_FACTOR = 0.75f;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 临界值:12}
}

(二)第二次添加不同的元素

        前面的都一样,只是这次数组长度不为null,是16,所以不会扩容;通过hash(key)方法计算出新的hash值,再根据新的hash值计算存放的索引位置,赋值给p,判断p所在的位置是否为null,为null,就创建新的结点存放进去。

存放成功后,再次返回null给putVal()方法

(三)添加重复的元素

1.仍旧走到了putVal(hash(key), key, value, false, true);方法

先判断数组长度是否为null,不为null,进入到下一个if

2.判断计算出的索引位置,是否为null,不为null,继续跳过下一个if,进入else中的第1个if

3.添加重复值的源码解读: 

else {// 一个开发技巧提示: 在需要局部变量(辅助变量)时候,再创建Node<K,V> e; K k;// 如果当前索引位置,对应的链表的第一个元素,和准备添加的key的hash值一样// 并且满足 下面两个条件之一:// (1) 准备加入的key 和 p 指向的Node 结点的 key 是同一个对象// (2) p 指向的Node 结点的 key 的equals() 和准备加入的key比较后相同// 就不能加入if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;​// 再判断 p 是不是一颗红黑树:// 如果是一颗红黑树,就调用 putTreeVal 来进行添加else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {// 如果table对应索引位置,已经是一个链表, 就使用for循环比较// (1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后//     注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点,//     就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)//     注意:在转成红黑树之前,要调用treeifyBin()进行判断, 判断条件://     final void treeifyBin(Node<K,V>[] tab, int hash) {//         int n, index; Node<K,V> e;//         if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //             resize();// static final int MIN_TREEIFY_CAPACITY = 64;// 如果链表>=7,但是整个数组的长度没有到64,那么不会转成红黑树,// 会执行resize()方法进行扩容//     只有上面条件不成立时(即链表=8 && 数组长度>=64),才进行转成红黑树// (2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接breakfor (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 添加重复值走下面的代码:// 注意:这里返回的oldValue是PRESENTif (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}// 添加成功走下面的代码:++modCount;// 每新增一个节点就会记录一次,当超过临界值12就会扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

在 HashSet 中,添加重复值时:

  • value 不会更新:

        HashSet 底层是使用 HashMap 来实现的,每个元素作为 HashMap 的键,值是一个固定的常量(PRESENT)。无论你往 HashSet 中添加多少次相同的元素,底层 HashMap 中该元素对应的 value 始终是 PRESENT。

        因此,当添加重复的元素时,value 不会更新,因为值永远是 PRESENT,不会发生变化。

  • key 也不会更新:

        在 HashSet 中,key 是唯一的(即元素是唯一的)。当你第一次添加某个元素时,HashMap 会将该元素作为 key 插入。

        当尝试添加重复的元素时,HashMap 会发现该 key 已经存在,于是不会再重新插入,也不会更新 key。

        所以,key 也不会更新,因为 HashMap 保证键的唯一性,重复添加相同的 key 不会导致键的覆盖或更新。

总结:

  • value:永远是 PRESENT,不会被更新。
  • key:如果 key 已经存在(即元素已经在 HashSet 中),则不会被重新插入或更新。
  • 因此,在 HashSet 中,添加重复的元素不会影响已有的键或值,元素的唯一性得到了保证。 

  1. 先获取元素的哈希值(hashCode方法)
  2. 对哈希值进行运算,得出一个索引值,即为要存放在哈希表中的位置号
  3. 如果该位置上没有其他元素,则直接存放;如果该位置上已经有其他元素,只需要进行equals判断,如果相等,则不再添加。如果不相等但计算后的hash值相同,则以链表的方式添加。
  4. 注意:传入的对象要看是否重写了equals方法,如果重写了,则判断内容相等,不再添加;如果没有重写,则判断地址值,重复的元素可以相加。
  1. HashSet底层是HashMap
  2. 添加一个元素时,先得到hash值,会转成索引值
  3. 找到存储数据表table,看这个索引位置是否已经存放有元素
  4. 如果没有,则直接加入
  5. 如果有,调用equals比较,如果相同,就放弃添加;如果不相同,创建新的结点,添加到最后
  6. 在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)

4.关于扩容的源码解读 

  1. HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75=12
  2. 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
  3. 注意:table数组使用到了临界值12的意思是,每添加一个元素,size就会++一次,加到12的时候就会扩容,而不是数组索引使用了12位或者使用到了12这个索引!!!!!
  4. 在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHLOD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制

代码示例:

HashSet hashSet = new HashSet();for(int i = 1; i <= 100; i++) {hashSet.add(i);// 1,2,3,4,5...100}

存放到临界值之前:

存放到临界值之后,进行数组扩容:

数组2倍扩容部分源码:

5.转成红黑树源码解读

设置代码,让元素加到一条链表上

public static void main(String[] args) {HashSet hashSet = new HashSet();for(int i = 1; i <= 12; i++) {hashSet.add(new A(i));//}}
}class A {private int n;public A(int n) {this.n = n;}@Overridepublic int hashCode() {return 100;}
}

HashSet初始大小为null

添加第1个元素落到索引为4的位置上:

添加第2个元素,落入链表的下一个结点:

扩容数组到64后,继续添加

添加前:

添加后,链表变为红黑树:

五、练习题

1.练习1

        定义一个Employee类,该类包含:private成员属性name,age 要求:

  1. 创建3个Employee对象放入HashSet中
  2. 当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中

代码实现:

public class HashSetExercise {public static void main(String[] args) {HashSet hashSet = new HashSet();hashSet.add(new Employee("张三", 20));hashSet.add(new Employee("李四", 25));hashSet.add(new Employee("张三", 20));for (Object o : hashSet) {System.out.println(o);}// Employee{name='张三', age=20}// Employee{name='李四', age=25}}
}class Employee {private String name;private int age;public Employee(String name, int age) {this.name = name;this.age = age;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Employee employee = (Employee) o;return age == employee.age && Objects.equals(name, employee.name);}@Overridepublic int hashCode() {return Objects.hash(name, age);}@Overridepublic String toString() {return "Employee{" +"name='" + name + '\'' +", age=" + age +'}';}
}

2.练习2

        定义一个Employee1类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中birthday为MyDate类型(属性包括:year、month、day),要求:

  1. 创建3个Employee放入HashSet中
  2. 当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中

代码实现:

public class HashSetExercise02 {public static void main(String[] args) {HashSet hashSet = new HashSet();hashSet.add(new Employee1("张三", 2000, new MyDate(1990, 1, 14)));hashSet.add(new Employee1("李四", 2000, new MyDate(1998, 10, 28)));hashSet.add(new Employee1("张三", 2900, new MyDate(1990, 1, 14)));for (Object o : hashSet) {System.out.println(o);}// Employee1{name='张三', sal=2000.0, birthday=MyDate{year=1990, month=1, day=14}}// Employee1{name='李四', sal=2000.0, birthday=MyDate{year=1998, month=10, day=28}}}
}class Employee1 {private String name;private double sal;private MyDate birthday;public Employee1(String name, double sal, MyDate birthday) {this.name = name;this.sal = sal;this.birthday = birthday;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Employee1 employee1 = (Employee1) o;return name.equals(employee1.name) && birthday.equals(employee1.birthday);}@Overridepublic int hashCode() {return Objects.hash(name, birthday);}@Overridepublic String toString() {return "Employee1{" +"name='" + name + '\'' +", sal=" + sal +", birthday=" + birthday +'}';}
}class MyDate {private int year;private int month;private int day;public MyDate(int year, int month, int day) {this.year = year;this.month = month;this.day = day;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;MyDate myDate = (MyDate) o;return year == myDate.year && month == myDate.month && day == myDate.day;}@Overridepublic int hashCode() {return Objects.hash(year, month, day);}@Overridepublic String toString() {return "MyDate{" +"year=" + year +", month=" + month +", day=" + day +'}';}
}

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

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

相关文章

java-02 数据结构-队列

在Java中&#xff0c;队列是一种常见的数据结构&#xff0c;用于在保持顺序的同时存储和检索数据。Java提供了java.util.Queue接口&#xff0c;它的常见实现包括ArrayDeque、LinkedList和PriorityQueue等。 如果你觉得我分享的内容或者我的努力对你有帮助&#xff0c;或者你只…

PyQt5常用功能四

⽂本涂鸦 写⼀些⽂本上下居中对齐的俄罗斯Cylliric语⾔的⽂字 import sys from PyQt5.QtWidgets import QWidget, QApplication from PyQt5.QtGui import QPainter, QColor, QFont from PyQt5.QtCore import Qtclass Example(QWidget):def __init__(self):super().__init__()…

趋势(一)利用python绘制折线图

趋势&#xff08;一&#xff09;利用python绘制折线图 折线图&#xff08; Line Chart&#xff09;简介 折线图用于在连续间隔或时间跨度上显示定量数值&#xff0c;最常用来显示趋势和关系&#xff08;与其他折线组合起来&#xff09;。折线图既能直观地显示数量随时间的变化…

如何查看GB28181流媒体平台LiveGBS中对GB28181实时视频数据统计的负载信息

目录 1、负载信息2、负载信息说明3、会话列表查看 3.1、会话列表4、停止会话5、搭建GB28181视频直播平台 1、负载信息 实时展示直播、回放、播放、录像、H265、级联等使用数目 2、负载信息说明 直播&#xff1a;当前推流到平台的实时视频数目回放&#xff1a;当前推流到平台的回…

【无标题】基于情境依赖因果影响的多智能体协作强化学习

、文章探讨了大型语言模型&#xff08;LLMs&#xff09;&#xff0c;例如GPT-4&#xff0c;是否以及在何种意义上拥有知识。作者认为&#xff0c;这些模型展现了一种称为“工具性知识”的能力&#xff0c;这种知识允许它们根据输入上下文推断任务结构&#xff0c;并在此基础上进…

废水处理(一)——MDPI特刊推荐

特刊征稿 01 期刊名称&#xff1a; Removing Challenging Pollutants from Wastewater: Effective Approaches 截止时间&#xff1a; 摘要提交截止日期&#xff1a;2024年11月30日 投稿截止日期&#xff1a;2025年5月31日 目标及范围&#xff1a; 该主题是分享去除有毒物…

js操作元素的其他操作(4个案例+效果图+代码)

目录 1. 获取元素的位置和大小 案例:获取元素的位置和大小 1.代码 2.效果 2. 获取元素的可视区域 案例:获取元素的可视区域 1.代码 2.效果 3. 元素的滚动操作 案例:元素的滚动操作 1.代码 2.效果 4. 获取鼠标指针位置 案例:时刻获取鼠标位置 1.代码 2.效果 案例:拖动的小球 1.代…

万物智联创未来,第三届OpenHarmony技术大会在上海成功举办 深圳触觉智能参会

​10月12日&#xff0c;以“技术引领筑生态&#xff0c;万物智联创未来”为主题的第三届OpenHarmony技术大会&#xff08;以下简称“大会”&#xff09;在上海成功举办。本次大会由OpenHarmony项目群技术指导委员会&#xff08;TSC&#xff09;主办&#xff0c;华为、上海交通大…

阿里云物联网自有app创建之初始化SDK

文章目录 一、新建工程&#xff0c;配置gradle,导入.so文件&#xff0c;生成apk二、上传apk&#xff0c;集成安全图片&#xff0c;下载SDK三、SDK的集成四、初始化SDK 最近在研究阿里云自有app,这是自己的心得。 一、新建工程&#xff0c;配置gradle,导入.so文件&#xff0c;生…

Gin框架官方文档详解03:HTML渲染

注&#xff1a;本教程使用工作区方法管理项目&#xff0c;详见第一讲&#xff1a;创建一个简单的Gin应用。 目录 一、简单渲染二、使用不同目录下名称相同的模板三、自定义模板渲染器四、自定义分隔符五、自定义模板函数六、总结 一、简单渲染 首先&#xff0c;以03HTML渲染为…

机器学习与神经网络:诺贝尔物理学奖的新篇章

机器学习与神经网络&#xff1a;诺贝尔物理学奖的新篇章 引言 2024年诺贝尔物理学奖的颁发&#xff0c;无疑是一个历史性的转折点。这一奖项首次授予了在机器学习与神经网络领域做出杰出贡献的科学家&#xff0c;标志着人工智能技术在科学研究中的重要地位得到了前所未有的认…

3.计算机网络_端口号

端口号的由来 运输层的作用&#xff1a; 在计算机网络中&#xff0c;运输层处在用户功能的最底层、通信部分的最高层的位置&#xff0c;也就是说运输层是用户数据和实际网络通信的桥梁。因此运输层屏蔽了网络的实现部分&#xff0c;以协议的方式向用户层提供了接口&#xff…

Matlab绘图总结(进阶)

本文在前文的基础上进一步整理画图方法 MATLAB画动图_CSDN博客 1. 基础图形绘制 1.1 rectangle&#xff08;矩形&#xff0c;圆形&#xff09; 在前文中&#xff0c;讲解了如何使用rectangle&#xff0c;rectangle本意是用来画矩形的&#xff0c;其中&#xff0c;Curvature可…

【一起学Rust | 框架篇 | Tauri2.0框架】高级概念之安全特性的权限与能力

文章目录 前言一、开发前准备1. 准备项目2. 需求分析1. 监听系统热键2. 切换窗口无边框3. 切换窗口全屏 二、安装插件三、前端实现功能四、配置权限 前言 当前时间为 2024 年 9 月&#xff0c;距离Tauri 2.0 的 RC 版本发布迄今已近一个月。从 Tauri 官方渠道可以看出&#xf…

CVESearch部署、使用与原理分析

文章目录 前言1、概述2、安装与使用2.1、源码安装2.1.1、部署系统依赖组件2.1.1.1、下载安装基础组件2.1.1.2、下载安装MongoDB Community Edition 7.0 2.1.2、使用源码安装系统2.1.2.1、安装CVESearch2.1.2.2、填充MongoDB数据库2.1.2.3、填充Redis数据库 2.2、使用方法 3、测…

使用java画一条线。

package p1008;import javax.swing.*; import java.awt.*;public class LineAndTextExample extends JPanel {Overrideprotected void paintComponent(Graphics g) {super.paintComponent(g);// 设置线条粗细Graphics2D g2d (Graphics2D) g;g2d.setStroke(new BasicStroke(5))…

SpringBoot教程(二十四) | SpringBoot实现分布式定时任务之Quartz(基础)

SpringBoot教程&#xff08;二十四&#xff09; | SpringBoot实现分布式定时任务之Quartz&#xff08;基础&#xff09; 简介适用场景Quartz核心概念Quartz 存储方式Quartz 版本类型引入相关依赖开始集成方式一&#xff1a;内存方式(MEMORY)存储实现定时任务1. 定义任务类2. 定…

Broken pipe异常分析及处理

问题出现&#xff1a;生产上运行的系统业务正常&#xff0c;当在查询数据时&#xff0c;出现后台异常&#xff0c;检查后台日志出现Broken Pipe异常&#xff1b; 如图示&#xff1a; Broken Pipe定义&#xff1a;通常发生在服务器端尝试向已关闭的套接字&#xff08;客户端/端…

前端面试经验总结1(简历篇)

本文分为3部分&#xff0c;分别为第一部分简历篇&#xff0c;第二部分经典问题篇以及第三部分知识体系篇&#xff0c;都是个人面试经验及同行面试经验总结和整理。 我对于简历的理解是这样的&#xff0c;简历的作用是让看简历的人能够快速、准确地捕捉到有用信息&#xff1a; 你…

大数据存储,搜索智能化的实践分享 | OceanBase 城市交流会精彩回顾

9月21日&#xff0c;“OceanBase 城市交流会”来到了深圳&#xff0c;携手货拉拉大数据技术与产品部&#xff0c;联合举办了“走进货拉拉”的技术交流活动。货拉拉、万家数科、云集、百丽等多家企业的一线技术专家&#xff0c;就大数据存储、AI等热点话题&#xff0c;深入探讨并…