当前位置: 首页 > news >正文

Java LinkedList

LinkedList 一个双向链表。
本身是基于链表进行封装的列表, 所以具备了链表的特性: 变更简单, 容量是无限的, 不必像数组提前声明容量等。
同时 LinkedList 支持存储包括 null 在内的所有数据类型。

1 链表

了解 LinkedList 之前, 我们需要先了解一下双向链的特点

  1. 单链表, 双链表, 循环链表的定义, 可以看一下这个
  2. 链表内存是散乱的, 每一个元素存储本身内存地址的同时还存储下一个元素的地址
  3. 链表具备了增删快, 查找慢的特点
  4. LinkedList 是基于双向链表设计的, 所以具备了链表的特点

2 LinkedList 中每个节点的定义

private static class Node<E> {/** 当前节点的内容 */E item;/** 下一个节点 */Node<E> next;/** 上一个节点 */Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {// 当前节点的数据 = elementthis.item = element;// 当前节点的后置节点 = nextthis.next = next;// 当前节点的前置节点 = prevthis.prev = prev;}
}

Node 节点中除了指向下一个节点的 next, 还包含指向上一个节点的 prev, 所以 LinkedList 是一个双向的链表。

3 LinkedList 中的几个属性

public LinkedList<E> {transient int size = 0;transient Node<E> first;transient Node<E> last;
}

3.1 size

表示当前链表中的节点个数。

3.2 first

整个双向链表的头节点

3.3 last

整个双向链表的尾节点

重要的就这个几个属性了

4 LinkedList 的构造方法

public LinkedList<E> {// 构造函数 1: 无参构造函数public LinkedList() {}// 构造函数 2: 给定一个 Collection 的构造public LinkedList(Collection<? extends E> c) {// 省略}
}

4.1 无参的构造函数

public LinkedList() {
}

就是单纯的声明 LinkedList 的实例, 没有其他的逻辑了

4.2 给定一个 Collection 的构造

public LinkedList(Collection<? extends E> c) {// 调用自身无参的构造函数this();// 将集合 c 添加到当前的链表中addAll(c);
}/*** 将集合对象全部添加到当前链表中*/
public boolean addAll(Collection<? extends E> c) {// 调用 addAll 重写方法在 size 的位置添加数据return addAll(size, c);
}/*** 指定位置的批量插入*/
public boolean addAll(int index, Collection<? extends E> c) {// 判断 0 <= index <= size, 不满足抛出异常checkPositionIndex(index);// 集合转为数组Object[] a = c.toArray();int numNew = a.length;// 需要插入的集合没有数据, 直接结束if (numNew == 0)return false;// pred: 新增的元素的将要插入的位置的前一个节点  // succ: 新增的元素的将要插入的位置的节点// 如果新增的元素的节点位置为 a, 那么 pred 就是 a 的前一位的节点, succ 就是 a 节点Node<E> pred, succ;// 需要添加的元素刚好追加在已有的后面的if (index == size) {succ = null;pred = last;} else {// 获取指定位置的节点succ = node(index);pred = succ.prev;}    for (Object o : a) {E e = (E) o;Node<E> newNode = new Node<>(pred, e, null);// 第一次创建时, 那么第一个节点就是 first节点if (pred == null)first = newNode;else// 将 pred 的下一个节点指向新创建的节点pred.next = newNode;  // 将 pred 改为当前节点, 方便下一个元素操作pred = newNode;    }   // succ 是为空的, 表示直接在已有的数据后面追加元素的话, 所以将最后节点设置为新增元素的最后一个元素的节点if (succ == null) {last = pred;} else {// 原有数据后半部分拼接到现在链表的后半部分pred.next = succ;succ.prev = pred;} // 当前的长度size += numNew;// 修改次数加1 modCount++;return true;
}/*** 校验 index 的值满足:  0 <= index <= size*/
private void checkPositionIndex(int index) {if (!isPositionIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}/** 判断 index 是否大于等于 0 并且小于等于 size*/
private boolean isPositionIndex(int index) {return index >= 0 && index <= size;
}/*** 获取指定位置的节点*/
Node<E> node(int index) {// 此次做了一个小优化: 当要查找的位置小于现有数据的一半, 从前往后找, 大于的话, 从后面开始找// size >> 1 相当于 size / 2if (index < (size >> 1)) {// 从前向后找Node<E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;} else {// 从后向前找Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}

从代码可知

  1. LinkedList 是基于双向链表实现了
  2. LinkedList 的数据是通过 first(节点) 和 last(节点) 和 size 三个共同维护的
  3. LinkedList 内部的数据通过泛型, 保持了自己的类型, 没有转为 Object。

5 LinkedList 几个常用的方法

5.1.1 添加数据

直接在链表末尾添加元素

// 直接添加一个元素
public boolean add(E e) {// 默认新增到链表的末尾linkLast(e);return true;
}/*** 将入参的元素添加到链表的末尾*/
void linkLast(E e) {// 获取链表的尾结点final Node<E> l = last;// 将数据封装为 Node 节点final Node<E> newNode = new Node<>(l, e, null);// 链表的尾结点等于新的节点last = newNode;// 如果链表一开始的尾结点为空, 表示链表一开始没有数据if (l == null)// 链表的头节点也等于当前的新节点first = newNode;else// 一开始的为节点有值, 将原来的尾结点的下一个节点设置为当前新的节点l.next = newNode;    // 链表个数 + 1size++;// 修改次数 + 1modCount++    
}

指定位置的插入

public void add(int index, E element) {// 判断 0 <= index <= size, 不满足抛出异常checkPositionIndex(index);// index == size 要插入的位置刚好在末尾if (index == size)linkLast(element);else // 插入到某个节点的前面linkBefore(element, node(index));    
}/*** 将元素插入到某个节点的前面*/
void linkBefore(E e, Node<E> succ) {// 获取某个节点的前置节点final Node<E> pred = succ.prev;final Node<E> newNode = new Node<>(pred, e, succ);// 设置插入位置的节点的前置节点为需要插入的新节点succ.prev = newNode;// 插入位置的节点的前置节点为空, 表示没有头节点if (pred == null)first = newNode;else// 需要插入的位置的前置节点的下一个节点为新节点pred.next = newNode;    // 个数加 1size++;// 修改次数 + 1modCount++;        
}

在头部插入

public void addFirst(E e) {linkFirst(e);
}/*** 在链表的头节点前新增元素*/ 
private void linkFirst(E e) {// 获取头节点final Node<E> f = first;final Node<E> newNode = new Node<>(null, e, f);// 头节点等于新节点first = newNode;// 原本的头节点为空, 表示没有数据if (f == null)// 设置尾节点为新节点last = newNode;else// 设置原本的头节点的前置节点为当前的节点f.prev = newNode;size++;modCount++;        
}

还有前面看到的

  1. 插入一个 Collection 的 addAll(Collection c)
  2. 指定位置的插入一个 Collection 的 addAll(int index, Collection c)

5.1.2 删除数据

直接删除 (默认删除第一个元素)

public E remove() {return removeFirst();
}/*** 删除链表第一个元素*/
public E removeFirst() {final Node<E> f = first;// 头节点为空if (f == null)throw new NoSuchElementException();return unlinkFirst(f);
}/*** 删除指定的节点*/
private E unlinkFirst(Node<E> f) {// 获取删除节点的数据final E element = f.item;// 找到删除元素的下一个元素final Node<E> next = f.next;// 清空删除元素的信息f.item = null;f.next = null;// 将头节点重新设置为删除元素的下一个元素first = next;// 如果原本删除元素就是没有后续元素时, 将最后的元素设置为 nullif (next == null)last = null;else// 将新的第一个元素的前置节点设置为 nullnext.prev = null;  // 元素个数 - 1    size--;// 修改次数 + 1modCount++;// 返回数据return element;          
}

指定位置的删除

public E remove(int index) {// 检测删除的位置是否正确checkElementIndex(index);return unlink(node(index));
}private void checkElementIndex(int index) {// 如果不满足  0 <= index < size, 就抛出异常if (!isElementIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}private boolean isElementIndex(int index) {return index >= 0 && index < size;
}E unlink(Node<E> x) {final E element = x.item;// 得到将要移除的元素的后置节点final Node<E> next = x.next;// 得到将要移除的元素的前置节点final Node<E> prev = x.prev;// 如果前置节点为空, 说明移除的为头节点, 重新设置头节点为删除节点的后置节点if (prev == null) {first = next;} else {// 将前置节点的下一个节点设置为后置节点prev.next = next;// 设置删除节点前置节点为 nullx.prev = null;}// 如果后置节点为空, 说明移除的为尾节点, 重新设置尾节点为删除节点的前置节点if (next == null) {last = prev;} else {// 将后置节点的前置节点设置为前置节点next.prev = prev;// 设置删除节点后置节点为 nullx.next = null;}// 将删除节点的数据设置为 nullx.item = null;size--;modCount++;return element;
}

指定删除元素

public boolean remove(Object o) {// 需要删除的节点为 nullif (o == null) {// 找到第一个为 null 的元素, 进行删除for (Node<E> x = first; x != null; x = x.next) {if (x.item == null) {unlink(x);return true;}}} else {for (Node<E> x = first; x != null; x = x.next) {// 找到节点的数据等于要删除的节点if (o.equals(x.item)) {unlink(x);return true;}}}
}

除了上面几个还有

  1. 删除第一个 removeFirst()
  2. 删除最后一个 removeLast()
  3. 删除第一个符合的元素 removeFirstOccurrence(Object o)
  4. 删除最后一个符合的元素 removeLastOccurrence(Object o)

5.1.3 获取数据

获取指定位置的数据

public E get(int index) {// 确保 0 <= index < sizecheckElementIndex(index);// 定位到对应位置的节点的数据return node(index).item;
}

获取第一个的数据

public E getFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return f.item;
}

获取最一个的数据

public E getLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return l.item;
}

5.1.4 更新数据

更新指定位置节点的属性值

public E set(int index, E element) {// 确保 0 <= index < sizecheckElementIndex(index);// 获取指定位置的节点数据Node<E> x = node(index);// 获取指定节点是旧数据E oldVal = x.item;// 更新指定节点的数据x.item = element;// 返回旧数据return oldVal;
}

6 LinkedList 补充

1. LinkedList 实现了 Serializable 接口, 但是他的属性都是设置为 transient ?

LinkedList 重写了序列化方法 writeObject 和反序列化方法 readObject。 在序列化中, 重新通过遍历所有节点, 把所有节点数据写入到 ObjectOutputStream。

2. LinkedList 支持 fail-fast 机制 ?

通过继承关系可以指定 LinkedList 也是实现了 AbstractList, 具备了 modCount, 在修改中也会增加 modCount 的值, 所以 LinkedList 也支持 fail-fast 机制。

3. LinkedList不是一个线程安全的集合 ?

LinkedList 是线程不安全的, 如果需要保证线程的安全性, 可以考虑使用 Collections.synchronizedList(Lise l) 函数返回一个线程安全的 LinkedList 类。

4. 不要用 for 遍历 LinkedList ?

遍历的代码

List<String> list2 = new LinkedList<>();
list.add("1");
list.add("2");
list.add("3");for (int i = 0; i < list2.size(); i++) {String item = list2.get(i);System.out.println(item);
}

源码中涉及的代码:

public class LinkedList {public E get(int index) {checkElementIndex(index);return node(index).item;}Node<E> node(int index) {if (index < (size >> 1)) {Node<E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;} else {Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}}
}

如上: 我们通过 for 遍历 LinkedList,

  1. 第一次 get(0), node(0), 就是 first 节点
  2. 第二次 get(1), node(1), 就是 first -> node1
  3. 第三次 get(2), node(2), 就是 first -> node1 -> node2

每次通过 get() 就是需要从第一个节点一直找到对应的位置, 才能找到需要的节点。
所以遍历 LinkedList 可以使用 foreach (foreach 循环的原理就是迭代器) 或者迭代器。

5. LinkedList 还有其他的作用吗

LinkedList 实现了 Deque 接口, 所以 LinkedList 可以作为双端队列, 同时 LinkedList 的双向链表的特点, 还可以作为 Stack 使用, 但是 LinkedList 的这 2 个功能,
如果没有什么特殊的要求的话, 都可以使用 ArrayDeque 替代, 后者的性能更好。

相关文章:

Java LinkedList

LinkedList 一个双向链表。 本身是基于链表进行封装的列表, 所以具备了链表的特性: 变更简单, 容量是无限的, 不必像数组提前声明容量等。 同时 LinkedList 支持存储包括 null 在内的所有数据类型。 1 链表 了解 LinkedList 之前, 我们需要先了解一下双向链的特点 单链表, 双…...

【单片机学习笔记】STC8H1K08参考手册学习笔记

STC8H1K08参考手册学习笔记 STC8H系列芯片STC8H1K08开发环境串口烧录 STC8H系列芯片 STC8H 系列单片机是不需要外部晶振和外部复位的单片机&#xff0c;是以超强抗干扰/超低价/高速/低功耗为目标的 8051 单片机,在相同的工作频率下,STC8H 系列单片机比传统的 8051约快12 倍速度…...

RevCol:可逆的柱状神经网络

文章目录 摘要1、简介2、方法2.1、Multi-LeVEl ReVERsible Unit2.2、可逆列架构2.2.1、MACRo设计2.2.2、MicRo 设计2.3、中间监督3、实验部分3.1、图像分类3.2、目标检测3.3、语义分割3.4、与SOTA基础模型的系统级比较3.5、更多分析实验3.5.1、可逆列架构的性能提升3.5.2、可逆…...

HCIA-RS基础-RIP路由协议

前言&#xff1a; RIP路由协议是一种常用的距离矢量路由协议&#xff0c;广泛应用于小规模网络中。本文将详细介绍RIP路由协议的两个版本&#xff1a;RIPv1和RIPv2&#xff0c;并介绍RIP的常用配置命令。通过学习本文&#xff0c;您将能够掌握RIP协议的基本原理、RIPv1和RIPv2的…...

虚拟化逻辑架构: LBR 网桥基础管理

目录 一、理论 1.Linux Bridge 二、实验 1.LBR 网桥管理 三、问题 1.Linux虚拟交换机如何增删 一、理论 1.Linux Bridge Linux Bridge&#xff08;网桥&#xff09;是用纯软件实现的虚拟交换机&#xff0c;有着和物理交换机相同的功能&#xff0c;例如二层交换&#…...

【Spring之AOP底层源码解析,持续更新中~~~】

文章目录 一、动态代理1.1、ProxyFactory1.2、Advice的分类1.3、Advisor的理解 二、创建代理对象的方式2.1、ProxyFactoryBean2.2、BeanNameAutoProxyCreator2.3、DefaultAdvisorAutoProxyCreator 三、Spring AOP的理解3.1、AOP中的概念3.2、Advice在Spring AOP中对应API3.3、T…...

C语言:有一篇文章,共三行文字,每行有80个字符。要求分别统计出单词个数、空格数。

分析&#xff1a; #include<stdio.h>&#xff1a;这是一个预处理指令&#xff0c;将stdio.h头文件包含到程序中&#xff0c;以便使用输入输出函数。 int main()&#xff1a;这是程序的主函数&#xff0c;是程序执行的入口点。 char a[3][80];&#xff1a;定义了一个二维…...

【数据结构与算法篇】一文详解数据结构之二叉树

树的介绍及二叉树的C实现 树的概念相关术语树的表示 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一 个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c; 也就是说它是根朝上&#xff0c;而叶朝…...

Windows主机信息收集命令

一.常用信息搜集 whoami # 查看当前用户 net user # 查看所有用户 query user # 查看当前在线用户 ipconfig /all # 查看当前主机的主机名/IP/DNS等信息 route print # 查看路由表信息 netstat -ano # 查看端口开放情况 arp -a # 查看arp解析情况 tasklist /svc # 查看进…...

「go module」一文总结 go mod 入门使用

文章目录 什么是 Go Modules为什么要使用 Modules怎么使用前置条件项目初始化如何安装/管理依赖&#xff1f;依赖安装 go get版本选择方式 替换版本 replace间接依赖 && go mod tidy远程代理 总结 什么是 Go Modules Module 是 Go 的依赖管理工具。 核心概念 Module…...

48. 旋转图像 --力扣 --JAVA

题目 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 解题思路 顺时针旋转90度 上下翻转 对角线翻转&#xff1b;两次两层循环…...

Java中的jvm——面试题+答案(Java虚拟机更深层次的概念和原理,包括字节码、代理、内存管理、并发等)——第17期

什么是即时编译&#xff08;JIT Compilation&#xff09;&#xff1f; 答案&#xff1a; 即时编译是一种在运行时将字节码转换为本地机器代码的技术&#xff0c;以提高程序的执行速度。JVM中的JIT编译器负责执行这个过程。 什么是Java字节码&#xff1f;为什么Java使用字节码…...

docker打包前端镜像

文章目录 一、构建镜像二、查看本地镜像三、启动容器四、查看启动的容器五、保存镜像六、读取镜像七、创建镜像八、最后 docker官网 一、构建镜像 -t是给镜像命名&#xff0c;.(点)是基于当前目录的Dockerfile来构建镜像 docker build -t image_web .二、查看本地镜像 docke…...

深入理解数据结构:链表

文章目录 &#x1f330;导语&#x1f330;链表的定义及基本结构&#x1f330;单链表&#x1f955;单链表特点 &#x1f330;双向链表&#x1f955;双链表特点 &#x1f330;循环链表&#x1f955;循环链表特点 &#x1f330;链表的操作&#x1f346;链表的插入&#x1fad8;链头…...

7:kotlin 数组 (Arrays)

数组是一种数据结构&#xff0c;它保存固定数量的相同类型或其子类型的值。kotlin中最常见的数组类型是对象类型数组&#xff0c;数组由array类表示。 什么时候使用 当你在kotlin中有特殊的底层需求需要满足时&#xff0c;可以使用数组。例如&#xff0c;如果你有超出常规应用…...

mysql 变量和配置详解

MySQL 中还有一些特殊的全局变量&#xff0c;如 log_bin、tmpdir、version、datadir&#xff0c;在 MySQL 服务实例运行期间它们的值不能动态修改&#xff0c;也就是不能使用 SET 命令进行重新设置&#xff0c;这种变量称为静态变量。数据库管理员可以使用前面提到的修改源代码…...

算法基础之合并集合

合并集合 核心思想:并查集: 1.将两个集合合并2.询问两个元素是否在一个集合当中 基本原理:每个集合用一棵树表示 树根的编号就是整个集合的编号 每个节点存储其父节点&#xff0c;p[x]表示x的父节点 #include<iostream>using namespace std;const int N100010;int p[N];…...

在使用微信或者支付宝支付的时候,为什么微信支付或者支付宝支付的异步通知商户支付结果要进行验签?

在使用微信支付或支付宝支付等第三方支付平台时&#xff0c;异步通知是一种常见的机制&#xff0c;用于通知商户支付结果或交易状态的变化。验签&#xff08;Signature Verification&#xff09;是为了确保异步通知的安全性和完整性而进行的重要步骤。以下是为什么要进行验签的…...

带你用uniapp从零开发一个仿小米商场_5. 公共样式编写,

先前引入了公共样式,但公共样式文件里面还没有编写内容 在这里我将一一讲解公共样式内应该有什么样式,和为什么 首先给page添加高度和背景色,当然这个背景色可以在app.vue内添加 page{/* 设置page高,让每个页面的最小高度为整个视窗的高 */min-height: 100vh; /* 统一字体大小…...

2019年全国硕士研究生入学统一考试管理类专业学位联考英语(二)试题

文章目录 2019年考研英语二真题SectionⅠ Use of EnglishSection II Reading ComprehensionText 121——细节信息题22——细节信息题23——细节信息题24——细节信息题25——词义题 Text 226——细节信息题27——细节信息题28——细节信息题29——细节信息题30——态度题 Text …...

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合

强化学习&#xff08;Reinforcement Learning, RL&#xff09;是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程&#xff0c;然后使用强化学习的Actor-Critic机制&#xff08;中文译作“知行互动”机制&#xff09;&#xff0c;逐步迭代求解…...

工业安全零事故的智能守护者:一体化AI智能安防平台

前言&#xff1a; 通过AI视觉技术&#xff0c;为船厂提供全面的安全监控解决方案&#xff0c;涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面&#xff0c;能够实现对应负责人反馈机制&#xff0c;并最终实现数据的统计报表。提升船厂…...

Java 8 Stream API 入门到实践详解

一、告别 for 循环&#xff01; 传统痛点&#xff1a; Java 8 之前&#xff0c;集合操作离不开冗长的 for 循环和匿名类。例如&#xff0c;过滤列表中的偶数&#xff1a; List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql

智慧工地管理云平台系统&#xff0c;智慧工地全套源码&#xff0c;java版智慧工地源码&#xff0c;支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求&#xff0c;提供“平台网络终端”的整体解决方案&#xff0c;提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

Java - Mysql数据类型对应

Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...

多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验

一、多模态商品数据接口的技术架构 &#xff08;一&#xff09;多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如&#xff0c;当用户上传一张“蓝色连衣裙”的图片时&#xff0c;接口可自动提取图像中的颜色&#xff08;RGB值&…...

SpringCloudGateway 自定义局部过滤器

场景&#xff1a; 将所有请求转化为同一路径请求&#xff08;方便穿网配置&#xff09;在请求头内标识原来路径&#xff0c;然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…...

C++八股 —— 单例模式

文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全&#xff08;Thread Safety&#xff09; 线程安全是指在多线程环境下&#xff0c;某个函数、类或代码片段能够被多个线程同时调用时&#xff0c;仍能保证数据的一致性和逻辑的正确性&#xf…...

Android第十三次面试总结(四大 组件基础)

Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成&#xff0c;用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机&#xff1a; ​onCreate()​​ ​调用时机​&#xff1a;Activity 首次创建时调用。​…...