写在前面:
每一个不曾起舞的日子,都是对生命的辜负。
希望看到这里的每一个人都能努力学习,不负韶华,成就更好的自己。
以下仅是个人学习过程中的一些想法与感悟,Java知识博大精深,作为初学者,个人能力有限,哪里写的不够清楚、明白,还请各位不吝指正,欢迎交流与讨论。如果有朋友因此了解了一些知识或对Java有了更深层次的理解,从而进行更进一步的学习,那么这篇文章的意义也就达到了。
目录
一、设计对象并使用
1.1设计类、创建对象并使用
1.2定义类的几个补充注意事项
二、对象在内存中的运行机制
2.1多个对象的内存图
2.2两个变量指向同一个对象地址
三、面向对象编程训练:模拟购物车模块
四、构造器、this关键字
五、封装
六、JavaBean
七、面向对象综合案例
面向对象
什么是面向对象编程?
面向:找、拿
对象:东西
面向对象编程:找或拿东西过来编程。
面向对象编程符合人类思维习惯,编程更简单,更好理解。
面向对象学习什么?
对于大多数业务需求来说,是没有现成对象供以使用的,需要学习自己设计对象并使用,前提是需要掌握面向对象的语法。
在设计对象之前,需要先设计类,再通过类设计对象并使用。
类是什么?
对象是真实存在是具体实例。
类是实物对象设计图,是对象共同特征的描述。
结论:在java中,必须先设计类,才能创建对象并使用。
如何设计类?
类的内容一般由成员变量、成员方法、构造器、代码块、内部类五部分组成。
public class 类名 {
}
例如对于汽车这一种实物的类:
public class Car {
// 属性(成员变量)
String name;
double price;
// 行为(方法)
public void start() {
}
public void run() {
}
}
如何得到类的对象?
类名 对象名 = new 类名();
如Car c = new Car();
如何使用对象?
访问属性:对象名.成员变量
访问行为:对象名.方法名(...)
示例代码如下:
汽车类
public class Car {
/**
* 成员变量
*/
String name;
double price;
/**
* 方法
*/
public void start() {
System.out.println(name + "启动了");
}
public void run() {
System.out.println("价格是" + price + "的" + name + "正在行驶");
}
}
测试类
public class Test1 {
public static void main(String[] args) {
// 创建汽车对象ss
Car c = new Car();
Car d = new Car();
c.name = "奔驰"; // 为c对象的name属性赋值
c.price = 1000000.0;
System.out.println(c.name);
System.out.println(c.price);
c.start(); // 奔驰启动了 c对象的name属性已经赋值,name为"奔驰"
d.start(); // null启动了 d对象的name属性没有赋值,调用d.name属性时name为默认值"null"
}
}
1.类名首字母建议大写,且有意义,不能是关键字,满足“驼峰模式”。
2.一个java文件中可以定义多个class类,但只能有一个public类,且public类的类名必须为文件名。实际开发中建议一个文件定义一个class类。
3.成员变量的完整定义格式是:修饰符 数据类型 变量名称( = 初始化值);一般无需指定初始化值,存在默认值,在使用时根据具体对象进行具体赋值。
默认值规则:
byte short char int long 0
double float 0.0
boolean false
String、类、接口、数组等引用类型 null
char默认值为0,但0在ASCⅡ码中没有对应值,因此char型变量默认值无法输出
示例代码如下图所示,定义了一个公共的汽车类Car类,然后再Test类中通过Car类实例化了多个Car对象。
其内存执行机制如下:
Step1:首先执行Test类,把.java类文件编译为.class文件,将其中全部内容加载到方法区中。main方法是程序执行的入口,因此首先执行main方法,将main方法提取到栈内存中运行,开始执行main方法中的代码。
Step2:执行main方法第一行代码,第一行代码涉及到Car类,因此将Car类编译为.class文件,将其中全部内容加载到方法区中。
定义一个引用类型的变量c1,由于c1是局部变量,因此会在栈内存中开辟一块区域用以存放c1。
然后在堆内存中开辟一块内存空间,new出一个Car对象,产生该对象的地址,在这块内存空间内又划分为若干个小区域,用于存放Car类的各种属性信息(name、price)和各种成员方法的引用地址,在使用时通过成员方法的引用地址访问加载在方法区中的成员方法。
注:new出来的对象中的成员变量是存放在该对象中的,该对象是存放在堆内存中的,因此new出来的成员变量也是存放在堆内存中的。
之后再将new出来的Car对象的地址赋值给c1变量。
注:c1变量中存储的实际上是在堆内存中new出来的Car对象的地址。
Step3:执行main方法第二、三行代码,根据存储在c1变量中的堆内存中new出来的Car对象的地址,找到该对象的成员变量name和price,分别将其赋值为”奔驰”和”39.78”。
执行main方法第四、五行代码,根据存储在c1变量中的堆内存中new出来的Car对象的地址,找到该对象的成员变量name和price,并将其变量值分别打印出来。
Step4:执行main方法第六行代码,根据存储在c1变量中的堆内存中new出来的Car对象的地址,找到该对象的成员方法start()的引用地址,再通过该引用地址找到在方法区中的具体方法,并执行。
将start()方法提取到栈内存中执行。由于start()方法是局部变量c1调用的,因此在加载name属性时,获取的是局部变量c1中存储地址指向的new出来的Car对象的name属性值,因此打印值为”奔驰启动了”而不是”null启动了”。
同理,执行main方法第七行代码,打印”价格是:39.78的奔驰跑得快”
Step5:第8-14行代码同第1-7行代码,具体过程不再赘述。
注:Car c1= new Car();与Car c2= new Car();都是存放的在堆内存中new出来的Car对象的地址,不同的是在这个过程中Car对象new了两次,第二次new出来的对象的地址,与第一次的地址不同,二者互相没有任何影响。
将存储某个对象地址值的变量赋值给另一变量时,被赋值的变量中存储的数据是该对象的地址值,即两个变量指向同一个对象地址,此时调用二者任意一个变量对该new出来的对象的具体内容做任意改变时,另一个变量在调用时同样会受到影响。
示例代码如下:
学生类
public class Student {
String name;
char sex;
String hobby;
public void study() {
System.out.println("姓名:" + name + ",性别:" + sex + ",爱好:" + hobby + "开始学习了");
}
}
测试类
public class Test {
public static void main(String[] args) {
// 创建学生对象
Student stu1 = new Student();
stu1.name = "小明";
stu1.sex = '男';
stu1.hobby = "睡觉";
stu1.study(); // 姓名:小明,性别:男,爱好:睡觉开始学习了
// 把stu1变量赋值给一个Student类型的变量stu2
Student stu2 = stu1; // 将存储在stu1中的new出来的Student对象的地址赋值给stu2
System.out.println(stu1); // com.itheima.memory.Student@1540e19d
System.out.println(stu2); // com.itheima.memory.Student@1540e19d 二者指向同一个Student对象的地址,地址相同
stu2.hobby = "爱提问";
stu2.study(); // 姓名:小明,性别:男,爱好:爱提问开始学习了
}
}
补充知识:垃圾回收
当堆内存中的对象,没有被任何变量引用(指向)时,会被判定为内存中的“垃圾”。
如Student stu = new Student(); // 创建实例化一个Student对象
stu = null; // 将Student类型变量stu中存放的地址值赋为空
此时堆内存中的Student对象没有被任何变量引用(指向),被判定为内存“垃圾”。
Java存在自动垃圾回收器,会定期进行清理。
1.需求分析、框架搭建
需求:模拟购物车模块的功能,需要实现添加商品到购物车中去,同时需要提供修改商品的购买数量,结算商品价格功能。
分析:
购物车中的每个商品都是一个对象,需要定义一个商品类。
购物车本身也是一个对象:可以使用数组对象代表它。
完成界面架构,让用户选择操作的功能。
2.添加商品到购物车、查看购物车信息
需求:让用户输入商品信息,并加入到购物车中去,且可立即查看当前购物车信息。
分析:
需要让用户录入商品信息,创建商品对象封装商品信息。
并把商品对象加入到购物车数组中去。
查询购物车信息,就是遍历购物车数组中的每个商品对象。
3.修改购买数量
需求:让用户输入商品id,找出对应的商品修改其购买数量
分析:
定义方法能够根据用户输入的id去购物车数组中查看是否存在该商品对象。
存在返回该商品对象的地址,不存在返回null。
判断返回的对象地址是否存在,存在修改其购买数量,不存在就继续。
4.结算金额
需求:当用户输入了pay命令后,需要展示全部购买的商品信息和总金额。
分析:定义求和变量,遍历购物车数组中的全部商品,累加其单价*购买数量。
示例代码如下:
商品类
public class Goods {
int id; // 编号,用于唯一确定该商品
String name; // 商品名称
double price; // 商品价格
int buyNumber; // 购买数量
}
主调程序
public class ShopCarTest {
public static void main(String[] args) {
// 1.定义商品类,用于后期创建商品对象
// 2.定义购物车对象 使用一个数组对象表示
Goods[] shopCar = new Goods[100]; // Goods类型的数组,里面存放Goods对象,具体值是各个Goods的地址 类本身是一个引用类型
// 3.搭建操作架构
// 添加死循环,执行完当前命令后可继续选择另一命令,继续执行
while (true) {
System.out.println("请在如下命令中选择一种执行:");
System.out.println("添加商品到购物车:add");
System.out.println("查询购物车商品:query");
System.out.println("修改购物车商品:update");
System.out.println("结算购买商品的金额:pay");
// 接收输入
Scanner sc = new Scanner(System.in);
System.out.println("请选择:");
String command = sc.next();
switch (command) {
case "add":
// 添加商品到购物车
addGoods(shopCar, sc);
break;
case "query":
// 查询购物车商品
queryGoods(shopCar);
break;
case "update":
// 修改购物车商品
updateGoods(shopCar, sc);
break;
case "pay":
// 结算购买商品的金额
payGoods(shopCar);
break;
default:
// 输入无效命令
System.out.println("输入错误!");
}
}
}
/**
* create by: 全聚德在逃烤鸭、
* description:将商品添加到购物车
* create time: 2022/4/4 0004 19:20
*
* @param shopCar
* @param sc
* @return void
*/
private static void addGoods(Goods[] shopCar, Scanner sc) {
// 录入用户输入的购买商品的信息
System.out.println("请输入购买商品的编号(不重复)");
int id = sc.nextInt();
System.out.println("请输入购买商品的名称");
String name = sc.next();
System.out.println("请输入购买商品的数量");
int buyNumber = sc.nextInt();
System.out.println("请输入购买商品的价格");
double price = sc.nextDouble();
// 把这些购买商品的信息封装成一个商品对象
Goods goods = new Goods();
goods.id = id;
goods.name = name;
goods.buyNumber = buyNumber;
goods.price = price;
// 把这个商品对象添加到购物车数组中去
for (int i = 0; i < shopCar.length; i++) {
// 判断第i个索引位置是否有内容,若无,则将该Goods对象的地址添加到数组中的第i个索引位置,跳出该循环,否则查找下一个索引
if (shopCar[i] == null) {
shopCar[i] = goods; // 将实例化的Goods对象的地址赋值给shopCar数组中的第i个索引
break; // 已经找到,无需继续循环
}
}
System.out.println("商品" + goods.name + "添加成功!"); // 添加成功后给出相应提示
queryGoods(shopCar);
}
/**
* create by: 全聚德在逃烤鸭、
* description: 查询购物车中的商品对象信息并展示出来
* create time: 2022/4/4 0004 20:11
*
* @param shopCar
* @return void
*/
private static void queryGoods(Goods[] shopCar) {
System.out.println("***********查询购物车信息如下***********");
System.out.println("商品编号\t\t商品名称\t\t购买数目\t\t商品价格");
// 定义for循环,遍历数组中所有的Goods对象,若不为空,则将此Goods类型变量中存储的地址对应的对象的各种成员属性展示出来
for (int i = 0; i < shopCar.length; i++) {
if (shopCar[i] != null) {
System.out.println(shopCar[i].id + "\t\t" + shopCar[i].name + "\t\t" + shopCar[i].buyNumber + "\t\t" + shopCar[i].price + "\t\t");
} else {
break; // 因为购物车中商品添加是从第0个索引顺序添加,所以当第i个索引位置存储的值为null时,说明i及后续索引位置值均为null,无需继续遍历,跳出循环
}
}
}
/**
* create by: 全聚德在逃烤鸭、
* description: 修改购买数量
* create time: 2022/4/4 0004 20:30
*
* @param shopCar
* @param sc
* @return void
*/
private static void updateGoods(Goods[] shopCar, Scanner sc) {
// 定义while循环,修改成功或手动取消后跳出,否则循环执行
OUT:
while (true) {
// 让用户输入要修改商品的id
System.out.println("请输入要查询的id");
int id = sc.nextInt();
// 调用根据id查询出要修改的商品对象的方法,查询出要修改的商品对象
Goods goods = getGoodsById(shopCar, id);
// 如果Goods类型的goods变量中存储的地址值不为null,则说明找到该商品,进行下一步操作,否则说明没有找到, 给出相应提示并要求用户重新输入
if (goods != null) {
System.out.println("请输入" + goods.name + "修改后的商品购买数目:");
int buyNumber = sc.nextInt();
goods.buyNumber = buyNumber; // 将该商品的购买数目更新为输入的修改后的商品购买数目
System.out.println("商品购买数目修改成功!"); // 修改成功后给出相应提示
queryGoods(shopCar); // 查询是否修改成功
break; // 修改完毕,跳出该while循环
} else {
System.out.println("很抱歉,您输入的id有误,请重新输入。");
// 定义while循环,当用户选择在进行请选择继续修改/取消修改输入错误时,重复执行
while (true) {
System.out.println("请选择继续修改:1\t\t取消修改:2");
int userChoice = sc.nextInt();
switch (userChoice) {
case 1:
System.out.println("您选择继续修改");
continue OUT; // 用户选择继续修改,跳转到OUT标签标记的while循环处,重新要求用户输入id重新查询
case 2:
System.out.println("您选择取消修改");
return; // 用户选择取消修改,结束当前方法,退回到主调方法
default:
System.out.println("输入错误,请重新输入");
}
}
}
}
}
/**
* create by: 全聚德在逃烤鸭、
* description: 根据id查询出要修改的商品对象
* create time: 2022/4/4 0004 20:36
*
* @param shopCar
* @param id
* @return com.itheima.demo.Goods
*/
public static Goods getGoodsById(Goods[] shopCar, int id) {
// 遍历每一个索引,找到数组中地址存储在Goods类型变量中的Goods对象的成员属性id是否与输入的id相同,若相同,返回该Goods对象,否则返回null
for (int i = 0; i < shopCar.length; i++) {
// Goods对象若不为空,则继续查看其id是否符合
if (shopCar[i] != null) {
// 若id符合,则将该Goods对象返回
if (shopCar[i].id == id) {
return shopCar[i];
}
} else {
break; // 因为购物车中商品添加是从第0个索引顺序添加,所以当第i个索引位置存储的值为null时,说明i及后续索引位置值均为null,无需继续遍历,跳出循环
}
}
return null; // 遍历完所有Goods类型变量存储的地址值不为空的对应的Goods对象,没有符合要求的id,返回null
}
/**
* create by: 全聚德在逃烤鸭、
* description: 计算购物车中的商品总金额
* create time: 2022/4/4 0004 21:43
*
* @param shopCar
* @return void
*/
private static void payGoods(Goods[] shopCar) {
queryGoods(shopCar);
// 定义一个求和变量,用于存储累加金额
double money = 0;
// 遍历购物车数组中的全部存储的地址值不为null的Goods类型变量对应的商品对象,累加各个商品对象中的单价*购买数目
for (int i = 0; i < shopCar.length; i++) {
// 若Goods类型的变量中存储的Goods对象的地址值不为空,则将此Goods类型变量中存储的地址对应的对象的商品购买数量属性与商品单价属性相乘,并累加到求和变量中去
if (shopCar[i] != null) {
int buyNumber = shopCar[i].buyNumber;
double price = shopCar[i].price;
money += (buyNumber * price); // 累加到求和变量money中
} else {
break; // 因为购物车中商品添加是从第0个索引顺序添加,所以当第i个索引位置存储的值为null时,说明i及后续索引位置值均为null,无需继续遍历,跳出循环
}
}
System.out.println("购物车中的商品总金额为" + money); // 循环结束后,将商品总金额输出
}
}
注:Goods[] shopCar= new Goods[100]; // Goods类型的数组,里面存放Goods对象,具体值是各个Goods对象的地址
※※※类本身是一个引用类型,Goods是自定义的引用类型
构造器的作用?
用于初始化一个类的对象,并返回对象的地址。
讲人话就是可以得到一个对象的地址,通过地址访问该对象。
构造器的格式?
示例代码如下:
public class Car {
// 无参构造器
public Car() {
...
}
// 有参数构造器
public Car(String a, double b) {
...
}
}
调用构造器得到对象的格式:
类 变量名称 = new 构造器;
如Car car = new Car(); Car c2 = new Car (“奔驰”, 39.8);
构造器的分类和作用:
无参数构造器(默认存在的):初始化的对象时,成员变量的数据均采用默认值。
有参数构造器:在初始化对象的时候,同时可以接收参数,为对象的成员变量进行赋值。
示例代码如下:
汽车类
public class Car {
String name;
double price;
/**
* 无参构造器
*/
public Car() {
System.out.println("无参构造器被调用了");
}
/**
* 有参构造器
*/
public Car(String n, double p) {
System.out.println("有参构造器被调用了");
name = n;
price = p;
}
}
测试类
public class Test {
public static void main(String[] args) {
// 通过调用构造器得到对象
Car c1 = new Car(); // 调用无参构造器
c1.name = "宝马";
c1.price = 38.9;
System.out.println(c1.name);
System.out.println(c1.price);
Car c2 = new Car("奔驰", 38.8); // 调用有参构造器
System.out.println(c2.name);
System.out.println(c2.price);
}
}
程序运行结果如下:
无参构造器被调用了
宝马
38.9
有参构造器被调用了
奔驰
38.8
构造器的注意事项:
任何类定义出来,默认自带无参数构造器,写不写都有。
一旦定义了有参数构造器,无参数构造器就没有了,如果还想用无参数构造器,此时需要自己写一个无参数构造器。
this关键字
可以出现在构造器、方法中,代表当前对象的地址,可以用于访问当前对象的成员变量、成员方法(this.name意为当前地址所对应的对象的成员变量name)。
面向对象的三大特征:封装、继承、多态。
封装:告诉我们,如何正确的设计对象的属性和方法。
※※※封装的原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为。
例如:人画圆这个行为,方法是定义在圆这个对象中,而不是定义在人这个对象中。针对圆这个对象,其中封装的成员变量有半径这个属性,只有得知半径,才能画出这个圆,画圆行为是半径属性的相关行为,因此画圆这个方法需要封装在圆这个对象中,人画圆的时候只需要调用圆中封装的画圆方法即可,除去人之外,小猫、小狗画圆行为同样是调用圆的画圆方法来完成的。
个人理解:
面向对象封装行为(方法)的原则:
主谓宾:将谓语动词这个行为(方法)封装到宾语对象中。
主谓:将谓语动词这个行为(方法)封装到主语对象中。
如何进行更好的封装?
面向对象的三大特征之一,合理隐藏,合理暴露。
①一般会把成员变量使用private隐藏起来,对外就不能直接访问了。
②提供public修饰的getter和setter方法暴露其取值和赋值。在setter方法中对传入的参数值进行校验,合格之后再赋值给对象中的变量,避免了问题数据的注入,提高了程序的安全性
示例代码如下:
学生类
public class Student {
// 定义成员变量,使用private修饰,该成员变量只能在此Class类中访问
private int age;
/**
* 提供成套的getter和setter方法暴露其取值和赋值方法
*/
public void setAge(int age) {
// 在setter方法中对传入的参数值进行校验,合格之后再赋值给对象中的变量
if (age >= 0 & age <= 200) {
this.age = age;
} else {
System.out.println("年龄值为" + age + ",数据有误!");
}
}
public int getAge() {
return age;
}
}
测试类:
public class Test {
public static void main(String[] args) {
Student stu = new Student();
// stu.age; // 报错,private修饰符定义的属性只能在本类中使用,在其他的类中获取不到
stu.setAge(20); // 调用setter方法,将对象中的成员变量age赋值为20
System.out.println(stu.getAge()); // 20 调用getter方法,获取对象中的成员变量age的值
Student stu2 = new Student();
stu2.setAge(-1); // 调用setter方法时,超出范围,返回对应提示
System.out.println(stu2.getAge()); // 0 数据赋值失败,在获取时Age仍然为默认值0,避免了问题数据的注入,提高了程序的安全性
}
}
封装的好处:
加强了程序代码的安全性。
适当的封装可以提升开发效率,同时可以让程序更容易理解与维护。
JavaBean是在现实生活中有对应具体个体(学生类、汽车类、用户类)的类,也可以称为实体类(测试类Test不属于JavaBean),其对象可以用在程序中封装数据。
标准JavaBean须满足如下要求:
①成员变量使用 private 修饰。
②提供每一个成员变量对应的 setXxx() / getXxx()。
③必须提供一个无参构造器,有参构造器可写可不写。
示例代码如下:
用户类
public class User {
// 成员变量使用私有private修饰符
private String name;
private double height;
private double salary;
/**
* 提供无参构造器,有参构造器可写可不写
*/
public User() {
}
public User(String name, double height, double salary) {
this.name = name;
this.height = height;
this.salary = salary;
}
/**
* 必须为成员变量提供成套的getter和setter方法
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
测试类
public class Test {
public static void main(String[] args) {
// 通过无参构造器创建一个对象封装一个用户信息
User user = new User();
user.setName("张三");
user.setHeight(185.3);
user.setSalary(5.5);
System.out.println(user.getName()); // 张三
System.out.println(user.getHeight()); // 185.3
System.out.println(user.getSalary()); // 5.5
// 通过有参构造器创建一个对象封装一个用户信息
User user2 = new User("李四", 180.5, 3.8);
System.out.println(user2.getName()); // 李四
System.out.println(user2.getHeight()); // 180.5
System.out.println(user2.getSalary()); // 3.8
}
}
成员变量、局部变量区别
成员变量和局部变量的区别
区别 |
成员变量 |
局部变量 |
类中位置 |
类中,方法外 |
常见于方法中 |
初始化值 |
初始化时选择默认值 |
在使用之前需要进行赋值 |
内存位置 |
堆内存(知识点 0-1) |
栈内存 |
生命周期 |
随着对象的创建而存在,随着对象的消失而消失 |
随着方法的调用而存在,随着方法的运行结束而消失 |
作用域 |
在所属的大括号中 |
需求:使用面向对象编程,模仿电影信息的展示。
分析:
①一部电影是一个java对象,需要先设计电影类,再创建电影对象。
②三部电影对象可以采用数组存储起来。
③依次遍历数组中的每个电影对象,取出其信息进行展示。
示例代码如下:
电影类
public class Movie {
// 定义成员变量
private String name;
private String actor;
private double score;
// 定义无参构造器
public Movie() {
}
// 定义有参构造器
public Movie(String name, String actor, double score) {
this.name = name;
this.actor = actor;
this.score = score;
}
// 创建getter、setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
}
测试类
public class Test {
public static void main(String[] args) {
// 1.创建电影类
// 2.创建对象,封装电影信息 3.定义一个电影类型的数组,存储3个电影对象
Movie[] movies = new Movie[3];
movies[0] = new Movie("长津湖", "吴京", 9.7);
movies[1] = new Movie("我和我的父辈", "吴京", 9.6);
movies[2] = new Movie("扑水少年", "王川", 9.5);
// 4.遍历数组中的每个电影对象,获取对象中的各个成员属性并展示出来
System.out.println("电影名\t\t主演\t\t评分");
for (int i = 0; i < movies.length; i++) {
System.out.println(movies[i].getName() + "\t\t" + movies[i].getActor() + "\t\t" + movies[i].getScore());
}
}
}
程序运行结果如下:
电影名 主演 评分
长津湖 吴京 9.7
我和我的父辈 吴京 9.6
扑水少年 王川 9.5
程序在内存中的运行机制如下图所示。
结论:数组变量中存储的元素不是数组本身,而是数组在堆内存中的地址。数组中开辟的三块小区域中各自存储的元素不是对象本身,而是堆内存中各个对象的地址。
写在最后:
感谢读完!
纵然缓慢,驰而不息!加油!