目录
一、Java发展历史
Java诞生至今已经不仅仅是一门语言,背后所涵盖的是一个庞大的技术体系。学习一门语言就要了解它的发展历史,这样才能为后面的学习打好整体的知识体系。
- 1991年4月,Java之父James Gosling带领绿色计划(Green Project)项目启动,定位于消费电子产品(机顶盒、冰箱、收音机)运行架构的Oak语言诞生,这也是Java的前身,但是市场反响一般。
- 1995年5月23日,随着互联网浪潮在1995年兴起,Oak迅速蜕,Java语言诞生,在SunWorld大会上发布Java1.0,第一次提出Write Once,Run Anywhere的口号。
- 1996年1月23日,JDK1.0发布,纯解释型的Java虚拟机(Sun Classic VM)、Applet、AWT等诞生。
- 1996年4月,十个最主要的操作系统和计算机供应商声明将在其产品中嵌入Java技术,
- 1996年5月底,Sun于美国旧金山举行了首届JavaOne大会,从此JavaOne成为全世界数百万Java开发者每年一度的技术盛会。
- 1996年9月,已有大约8.3万个网页应用了Java技术来制作。
- 1997年2月19日,Sun公司发布了JDK1.1,代表技术:JAR文件格式、JDBC、JavaBeans、RMI等,Java语法也进行了增强,内部类(Inner Class)和反射(Reflection)出现。
- 1998年12月4日,JDK1.2发布,这是一个里程碑式的重要版本,工程代号为Playground(竞技场),这个版本代表性技术非常多,如EJB、JavaPlug-in、Swing、JavaIDL等,还有使用极为频繁的Collections体系工具类等,并且这个版本中Java虚拟机第一次内置了JIT(Just In Time)即时编译器,后续还发布了JDK1.2.1和JDK1.2.2两个小版本升级。在JDK1.2中曾经共存过ClassicVM、HotSpotVM、ExactVM三个虚拟机,其中HotSpot是作为附加程序提供。也是在这个版本中Sun开始拆分对应的Java产品线,Sun在这个版本中把Java技术体系拆分为三个方向:
- 分别是面向桌面应用开发的J2SE(Java 2 Platform,Standard Edition)
- 面向企业级开发的J2EE(Java 2 Platform,Enterprise Edition)
- 面向手机等移动终端开发的J2ME(Java 2 Platform,Micro Edition)
- 1999年4月27日,HotSpot虚拟机诞生,HotSpot最初由Longview Techno-logies公司研发,后来在1997年被Sun公司收购,后来它成为JDK 1.3及之后所有JDK版本的默认Java虚拟机。
- 2000年5月8日,工程代号为Kestrel(美洲红隼)的JDK 1.3发布。JDK 1.3的改进主要体现在Java类库上(如数学运算和新的Timer API等),此外一直作为扩展服务的JNDI服务也开始作为一项平台级服务被提供,还有基于CORBA IIOP协议来实现通信段RMI也出现了。
- 2001年5月17日,JDK1.3的修订版JDK1.3.1发布,工程代号Ladybird(瓢虫)。从JDK1.3开始,Sun公司维持着稳定的开发节奏,大概每个两年发布一个主要版本,期间发布的各个修订版本以昆虫作为工程代号。
- 2002年2月13日,JDK 1.4发布,工程代号为Merlin(灰背隼)。JDK 1.4是标志着Java真正走向成熟的一个版本。Compaq、SAS、Fujitsu、Symbian、IBM等一众大公司参与功能规划,甚至实现自己的独立发行版本。哪怕在二十年后的今天一些主流功能也可以在JDK1.4上运行,这个版本的主要代表技术包含正则表达式、异常链、NIO、日志类、XML解析器和XSLT转换器等等。
- 2002年9月16日,工程代号为Grasshopper(蚱蜢)的JDK1.4.1修订版发布。在这一年前后微软平台的.NET Framework发布,至此Java平台和.NET平台的竞争开始拉开了序幕。但似乎Java的开源策略更胜一筹,终于在2014 年 11 月 12 日,微软正式宣布了.NET Core 的开源。
- 2003年6月26日,工程代号为Mantis(螳螂)的JDK1.4.2修订版发布。
- 2004年9月30日,JDK 5发布,工程代号为Tiger(老虎),Sun公司从这个版本开始放弃JDK 1.x的命名方式,将产品版本号修改成了JDK x,JDK1.2以来Java语言在语法上的改动都不大,该版本在语法易用性上做出了非常大的改进,如自动装箱、泛型、动态注解、枚举、可变长参数、foreach等。此外改进了Java的内存模型(Java Memory Model,JMM)、提供了java.util.concurrent并发包(由Doug Lea大师带Java进入了并发时代)等,JDK 5是官方声明可以支持Windows 9x操作系统的最后一个版本。
- 2006年11月13日,JavaOne大会上,Sun公司宣布计划要把Java开源,随后在一年多的时间内,陆续的将JDK各部分在GPL V2协议下公开源码,随后并建立了OpenJDK组织对这些源码进行独立管理,除了极少部分的产权代码,OpenJDK几乎拥有了SunJDK 7中的全部代码。
- 2006年12月11日,JDK6发布,工程代号为Mustang(野马)。在这个版本中,Sun公司终结了从JDK 1.2开始已经有八年历史的J2EE、J2SE、J2ME的产品线命名方式,启用Java EE 6、Java SE 6、Java ME 6的新命名来代替。在JDK 6中提供了众多改进,如通过Mozilla JavaScript Rhino引擎提供初步动态语言支持,提供编译器注解处理器(Annotation Processor这也是Lombok的原理,通过注解生成模板代码)和微型HTTP服务器API,以及对虚拟机内部锁、同垃圾收集、类加载机制等方面进行了大量优化改动。在JDK 6发布以后由于代码的复杂化,Java开源、开发JavaFx、世界经济危机以及Oracle对Sun的收购提案等原因,Sun公司内忧外患自顾不暇,原本稳定的开发进度也受到了很大的影响,使得JDK 6的生命周期也持续了很久,一共发布了211个更新补丁,最终版本为Java SE 6 Update 211,于2018年10月18日发布。
- 2009年2月19日,工程代号为Dolphin(海豚)的JDK 7发布,这是其第一个里程碑版本,按照规划,共有十个里程碑版本发布,最后一个里程碑版本发布与2010年9月9日,由于各种原因JDK 7没有按照原计划完成。JDK 7开发阶段Sun公司在技术竞争和商业竞争中都深陷泥潭,已经无力推动开发进展,为了尽快完成JDK 7的发布,因此裁掉了大部分原定的功能,对于原定的Lambdax项目、Jigsaw项目、动态语言支持、Gabage-First垃圾收集器、Coin项目只匆匆交付了处于Experimental状态的G1垃圾收集器(直到2012年4月的Update 4中才开始正式商用),其余项目延期到JDK 8中。Oracle从JDK 7开始进行接手,迅速展现出了其极具商业化的处世风格,面对Java中使用最广泛的Java SE免费产品线,定义了一套新的商业版产品Java SE Support ,此外JDK 7计划维护到2022年,已经面向付费用户发布了211个补丁,最新版本为JDK 7 Update 211。
- 2009年4月20日,Oracle宣布正式以74亿美元的价格收SUN公司,一代巨头由此没落,Java商标正式划归Oracle所有,Java语言本身并不属于哪间公司所有,它由JCP组织进行管理。此外Oracle还收购了BEA公司,JavaEE服务器Weblogic就是该公司的产品。
- 2011年7月28日,JDK7发布,做出的改进:提供新的G1收集器、加强对非Java语言的调用支持、可并行的类加载架构等。
- 2014年3月18日,Oracle公司发布JDK 8,从此使用JEP(JDK Enhancement Proposals)来定义和管理纳入新版JDK发布范围的功能特性,JDK 8中实现了JDK7中定义并且未完成的功能,其中也有被延期到JDK 9的Jigsaw模块化功能。
- JEP 126 Lambda函数表达式支持
- JEP 104 内置Nashorn JavaScript引擎
- JEP 150 新的时间日期API
- JEP 122 移除HotSpt永久代
- 2017年9月21日,Oracle公司发表 JDK 9,这其中较为显著的功能就是在JDK 7时期已经规划的Jigsaw模块化功能,为何屡次发布都未能如约而至,前期可能是技术埋坑,后面遭到了以IBM和RedHat联合的等JCP委员抵抗,否决模块化提案,这种做法无疑是革了模块化支持本身就比较好的IBM的命,面对如此抵抗,Oracle丝毫没有退让,宁愿摒弃JSR独立研发,也要推行Jigsaw的发展,如果成真,想必Java也会面临如Python2和Python3的巨大不同,好在最终达成一直协议,Java还是那个完整的Java。JDK 9还提供了JS Shell、JLink、JHSDB等增强。JDK 9之后,Java也以更加敏捷的交付方式如期而至,每年的3月和9月发布一个版本,美6个月发布一个版本,每三年一个LTS版本,目的是避免众多功能交付在一个捆绑JDK的风险,并且提出了孵化器模块(Incubator)和预览特性(Preview)两个概念。也是在这个版本中CMS开始被废弃。
- 2018年3月20日,Oracle公司发布JDK 10,主要对内部进行重构,统一源仓库,统一垃圾收集器接口,统一即时编译器接口(引入Graal几时编译器,这里也埋下一个伏笔),这些改进对于用户并不明显,但对后期的版本打下了良好的基础。
- 2018年3月,同样发生了比较重要的一件事,Oracle正式宣布Java EE成为历史名称,曾经拥有着无数光辉的Java EE产品线(至今仍使用较为广泛的JDBC、JMS、Servlet等组件)被Oracle扫地出门,全部打包赠送给Eclipse基金会,并且不能使用Java商标,并且更名为Jakarta EE。
- 2018年10月,自1996年以来每年一度的已经举办了22年的JavaOne大会,没有人预测到这也是最后一届,同年6月Java Mission Control的开发团队也被Oracle解散。
- 2018年9月25日,JDK 11发布,这也是一个LTS(long-term support)版本,包含17个JEP,同时被引入的还有ZGC这样革命性的垃圾收集器,好比G1的诞生,比G1又更具神秘感。同时Oracle也调整了JDK的授权许可证,把以前的商业许可证授权给OpenJDK,官方宣布同时发布两个JDK,一个是Oracle OpenJDK,一个是OracleJDK,共享大部分源码近乎一致,个人均可免费使用,OpenJDK有半年的更新支持,OracleJDK商用必须付费,且支持三年的更新。因此Java收费纯属谣言,商业用户如果想使用最新的版本支持就必须付费。
- 2019年3月20日,JDK 12发布,RedHat接手了OpenJDK 8和OpenJDk 11的管理和维护权。在JDK 12中包含了8个JEP,主要有Switch表达式和JMH测试套件,最引人注目的就是Shenandoah垃圾收集器,作为首个在JDK 7以后非Oracle开发的垃圾收集器,其目标与ZGC一致,这种竞争关系,立马得到了Oracle的抵制,在OracleJDK中剔除代码,因此Oracle JDK和OpenJDK的隔离性又如历史在JDK9中开始重演,至于后续Java的发展,以及迎接的挑战也是任重而道远,这取决于Oracle的产品线的定制和JCP的决策了。
- 2019年在9月17日,JDK 13发布,这个版本主要通过改善Java SE平台和JDK的性能,稳定性和安全性来提高开发人员的生产力。共包含了5个JEPs和一个Unicode 12.1的支持总共6大主要新特性。
- 2020年3月17日,JDK 14发布,这个版本主要是对JDK历史版本的一些增强,也引入了一些新增的功能.
- 2020年9月15日,JDK 15发布,按照规划路线,JDK 14也停止更新,JDK 15虽然不是LTS版本,但也引入了一些开创性的功能和对早期版本功能的一些优化。
- 2021年3月16日,JDK 16发布,这部分依旧是一些功能的优化升级。
- 2021年9月14日,JDK17发布,这也是在JDK 11之后的下一个LTS版本,JDK 17也是Java六个月发布节奏下的最新的长期支持(LTS)发布,主打安全、性能、稳定为特性,并且官方计划支持到2029年9月。在这个版本中包含了14个JEPs更新。
二、JDK与JRE
Java development kit的缩写,表示是Java开发工具,是用于进行Java开发的利器,只有安装了JDK,配置好了环境变量和path才可以运行成功。
jdk主要包含三个部分:
- JVM(Java运行时环境)
- Java的基础类库(这个类库的数量还是相当可观的)
- Java的开发工具(它们都是辅助你更好地使用Java的利器jre--运行环境)
1、Windows系统下载安装JDK
- 下载JDK
官网下载地址 下载后,点击安装即可。 - 配置环境变量
- 测试
win + R,输入cmd,打开命令行窗口;输入如何所示,表示Java开发环境配置成功,此时就可以愉快的运行Java代码了
2、Linux系统下载安装JDK
- 下载JDK
jdk1.8 - 解压文件
tar -xvf jdk-8u65-linux-x64.tar.gz
- 添加环境变量
[root@iZm5e2he0vxbiefgvrp3raZ local]# vim /etc/profile
export JAVA_HOME=/usr/local/jdk1.8.0_181 export JRE_HOME=/usr/local/jdk1.8.0_181/jre export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
- 验证环境变量是否生效
[root@iZm5e2he0vxbiefgvrp3raZ local]# source /etc/profile
-
检查安装是否成功
[root@iZm5e2he0vxbiefgvrp3raZ local]# java -version
到此环境配置完成。
3、JRE(运行时环境)
JRE是Java Runtime Environment的缩写,顾名思义是java运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的,还有所有的Java类库的class文件,都在lib目录下,并且都打包成了jar。
4、JVM(java)
- Java源文件,通过编译器,能够生产相应的.Class文件,也就是字节码文件,而字节码文件又通过Java虚以机中的解释器,编译成特定机器上的机器码。
- 每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
三、Java基本特性
-
面向对象
Java 是一种面向对象的语言,它对对象中的类、对象、继承、封装、多态、接口、包等均有很好的支持。为了简单起见,Java 只支持类之间的单继承,但是可以使用接口来实现多继承。使用 Java 语言开发程序,需要采用面向对象的思想设计程序和编写代码。 -
平台无关性(可移植性)
平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。
Java 语言使用 Java 虚拟机机制屏蔽了具体平台的相关信息,使得 Java 语言编译的程序只需生成虚拟机上的目标代码,就可以在多种平台上不加修改地运行。
-
简单性
Java 语言的语法与 C 语言和 C++ 语言很相近,使得很多程序员学起来很容易。对 Java 来说,它舍弃了很多 C++ 中难以理解的特性,如操作符的重载和多继承等,而且 Java 语言不使用指针,加入了垃圾回收机制,解决了程序员需要管理内存的问题,使编程变得更加简单。 -
解释执行
Java 程序在 Java 平台运行时会被编译成字节码文件,然后可以在有 Java 环境的操作系统上运行。在运行文件时,Java 的解释器对这些字节码进行解释执行,执行过程中需要加入的类在连接阶段被载入到运行环境中。 -
多线程
Java 语言是多线程的,这也是 Java 语言的一大特性,它必须由 Thread 类和它的子类来创建。Java 支持多个线程同时执行,并提供多线程之间的同步机制。任何一个线程都有自己的 run() 方法,要执行的方法就写在 run() 方法体内。 -
分布式
Java 语言支持 Internet 应用的开发,在 Java 的基本应用编程接口中就有一个网络应用编程接口,它提供了网络应用编程的类库,包括 URL、URLConnection、Socket 等。Java 的 RIM 机制也是开发分布式应用的重要手段。 -
健壮性
Java 的强类型机制、异常处理、垃圾回收机制等都是 Java 健壮性的重要保证。对指针的丢弃是 Java 的一大进步。另外,Java 的异常机制也是健壮性的一大体现。 -
高性能
Java 的高性能主要是相对其他高级脚本语言来说的,随着 JIT(Just in Time)的发展,Java 的运行速度也越来越高。 -
安全性
Java 通常被用在网络环境中,为此,Java 提供了一个安全机制以防止恶意代码的攻击。除了 Java 语言具有许多的安全特性以外,Java 还对通过网络下载的类增加一个安全防范机制,分配不同的名字空间以防替代本地的同名类,并包含安全管理机制。
Java 语言的众多特性使其在众多的编程语言中占有较大的市场份额,Java 语言对对象的支持和强大的 API 使得编程工作变得更加容易和快捷,大大降低了程序的开发成本。Java 的“一次编写,到处执行”正是它吸引众多商家和编程人员的一大优势。
四、Java对象的四种关系
1、依赖
依赖关系指的是类与类存在相互的依赖,一个类的定义依赖于另外一个类的定义。例如,一个人(Person)可以买车(Car)和房子(House),Person类依赖于Car类和House类的定义。.与关联不同,Person类里并没有Car和House类型的属性,Car和House的实例是以参数的方式传入到buy()方法中去的。一般而言,依赖关系在Java语言中体现为局域变量、方法的形参,或者对静态方法的调用
2、关联
关联关系是类与类之间的联接,它使一个类知道另一个类的属性和方法。关联可以是双向的,也可以是单向的。在Java语言中,关联关系一般使用成员变量来实现。
3、聚合
聚合关系是关联关系的一种,是强的关联关系。聚合是整体和个体之间的关系。例如,汽车类与引擎类、轮胎类,以及其它的零件类之间的关系便整体和个体的关系。与关联关系一样,聚合关系也是通过实例变量实现的。但是关联关系所涉及的两个类是处在同一层次上的,而在聚合关系中,两个类是处在不平等层次上的,一个代表整体,另一个代表部分。
4、组合
组合关系是关联关系的一种,是比聚合关系强的关系。它要求普通的聚合关系中代表整体的对象负责代表部分对象的生命周期,组合关系是不能共享的。代表整体的对象需要负责保持部分对象和存活,在一些情况下将负责代表部分的对象湮灭掉。代表整体的对象可以将代表部分的对象传递给另一个对象,由后者负责此对象的生命周期。
五、流程控制语句
1、顺序结构
按照代码书写从上到下,从左到右执行
2、分支结构
- if
格式:if(逻辑表达式){语句体} - if…else
格式:if(逻辑表达式){语句体1}else{语句体2} - if…else if…else
格式:格式:if(逻辑表达式1){语句体1}else if(逻辑表达式2){语句体2}else{语句体3} - switch
case:关键字,情况
break:关键字,退出循环
default:关键字,默认
格式:格式:switch(表达式){case常量1:语句体1:break,case常量2:语句体2;break;default:语句体3}
3、循环结构
- for:格式:for(初始化语句;逻辑表达式;步进表达式){循环语句体}
- while:格式:初始化变量;while(逻辑表达式){循环语句体;步进表达式}
- do…while:格式:do{循环语句体,步进表达式}while(逻辑表达式):,其特点是至少执行一次
- 死循环:格式1:for(;;){循语句体}、格式2:while(true){循环语句体}
- 嵌套循环
格式:for(初始化语句1;逻指表达式1;步进表达式1){
for(初始化语句2;逻辑表达式2;步进表达式2){
循环语句体:
}
} - 跳转语句
- continue:结束本次循环继续下次循环
- break:结束当前所在层次的循环
- return:结束所在方法
- System.exit(O::关停虑拟机
六、方法
1、概述
具有特定功能的代码段
2、格式
修饰符 返回值类型 方法名称(参数列表){方法体;return语句}
3、优点
- 提高代码复用性
- 提高了代码的封装性,大括号中的内容括号外无法访问也就无法修改,保证了数据的安全
- 简化了代码的设计思维难度
4、调用
- 格式:方法名称实际参数:
- 直接调用:直接书写方法名你调用方法,让方法执行
- 输出调用:调用有返回值的方法,将具体返回的结果进行输出
- 赋值调用:调用有返回值的方法,将返回结果赋值给对应关型的变量,做后续处理
5、形参和实参
- 形式参数:方法用于向外界声明,执行功的时候,需要几个哪那些类型的数据
- 实际参数:方法真正要被调用执行的时候,传递给方法的实实在在的数据
6、方法的重载
概述:在同一个类中,方法名称相同,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,与返回值类型无关,重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
优点:可以使用同一个方法名,减轻记忆压力
注意:所有方法必须在同一个类中;所有方法都具有同一个名字
7、方法的重写
概述:重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 - 构造方法无法被重写
8、注意
- 方法不能嵌套定义,可以嵌套调用
- 形参可有可无,实参对应形参,可有可无
七、数组
1、概述
用于存储相同数据类型的容器
2、格式
格式:数据类型[] 数组名称=new 数据类型[数组容量]:
3、特点
可以同时存储很多数据类型相同的数据,通过索引找到具体数据
4、初始化
- 动态初始化:数据类型[] 数组名称=new 数据类型[数组容量]:
- 静态初始化:
- 数据类型[] 数组名称=new 数据类型[]{元素1,元素2,……,元素n}:
- 简写:数据类型[] 数组名称={元素1,元素2,……,元素n}:
5、异常
数组索引越界异常
产生的原因:访问的索引超出了已有索引的范围
Exception in thread "main"java.lang.ArraylndexOutOfBoundsException:3
避免方式:不去访方问超出范围的索引
空指针异常
产生原因:当地址为null时,即不指向任何数据,如果此时仍然要通过null来进行操作,就会出现空指针异常
Exception in thread "main"java.lang.NullPointerException
避免方式:在访问数据之前,先判断是否存在地址
6、数据的遍历
概述:一个一个遍历元素,所有的元素都访问一遍
for (int i 0;i<arr.length;i++){
System.out.print(arr[i]+ "")
}
7、数组的最值
概述:给定一个数组,获取数组中的最大值或最小值
int max arr[0];
for (int i 0;i<arr.length;i++)(
if(max <=arr[i]){
max=arr[i];
}
}
8、数组的反转
概念:给定一个数组,让原来的顺序颠倒
for (int i=0,j=arr.length-1;i<j;i++,j--){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
9、三维数组
概述:就是数组套数组,一个数组中装很多个数组,每一个数组中又有多个元素
格式:
int[][] arr = new int[3][4];
int[][] arr = new int[3][];
int[][] arr = {{},{1},{1,2},{3,4,5,6},null}:
三维数组的遍历:
for (int i=0;i<arr.length;i++){
for (int j=0;j<arr[i].length;j++){
System.out.print(arr[i]]+ "");
}
System.out.println();
}
八、常用API
1、Object
- 概述:所有类的总父类,所有类型默认即成Object类
- toString方法
- getClass() 获取到当前对象所属的类型
- hashCode() 根据对象的情况,计算出对象的哈希码值
- toString() 返回对象的字符串表示形式
- 重写原则:不返回地址值,返回属性值
- equals方法
- 用于比较两个对象是否相等
- Object中比较地址值,重写后比较属性值
- ==和equals的区别
- ==是运算符,equals是方法
- 比较的内容不同:==既可以比较基本数据类型,也可以是比较引用数据类型;equals 只能比较引用数据类型
- 比较规则不同:==比较基本数据类型,比较的是值;比较引用数据类型,比较的是地址。equals 重写前比较地址,重写后比较值
2、Scanner
- 概述:扫描器,扫描基本数据类型和简单文本
- 构造方法:
- Scanner(File source) 扫描指定的文件
- Scanner(String source) 扫描指定的字符串
- Scanner(InputStream source) 扫描指定的输入流,System.in 就是一个输入流,标准输入流,默认关联到键盘
- 录入基本数据类型(方法)
- nextByte()
- nextShort()
- nextInt()
- nextLong()
- nextFloat()
- nextDouble()
- nextBoolean()
- 注意:无nextChar()方法
- 录入字符串
- next() 可以将空格作为分割标记
- nextLine() 通过换行来作为结束标记
- 注意问题
- 当我们使用了——空格作为分隔和结束标志后,在紧接着使用nextLine() 方法,nextLine()会直接被消耗掉
- 解决办法:所有录入的内容都是用nextLine方法
3、String
- 概述:字符串,不需要导包,字符串字面值常量,存储在方法取得的常量池中;String类型表示的字符串本身是一个常量,创建后就无法修改;String不可变原因在于String没有提供任何的set方法能够修改字符串本身,而且该类由final修饰,是一个不可变类
- 构造方法
String() 创建一个空字符串
String(byte[] arr,int offset,int length) 将byte数组的-部分解析为字符串
String(char[] arr,int offset,int length) 将char数组的一部分解析为字符串
String(String str) 返回参数字符串的副本
String(StringBuffer buffer) 分配一个新的字符串,它包含字符串缓冲区参数中当前包
含的字符序列
String(StringBuilder builder) 分配一个新的字符串,它包含字符串生成器参数中当前包
含的字符序列
- 判断功能
equals(Object obj) 判断调用者字符串和参数字符串内容是否相等
equalslgnoreCase(String anotherString) 忽略大小写判断调用者和参数字符串内容是
否相等
contains(String str) 判断参数字符串是否为调用者字符串的字符串
startWith(String str) 判断调用者是否为参数字符串开头
endWith(String str) 判断调用者是否为参数字符串结尾
isEmpty() 判断调用者是香为空事
- 获取功能
length() 返回调用者字符串的长度
char(int index) 从调用者字符串上获取到指定索引的字符返回
substring(int beginlndex) 从指定索I开始,截取调用者字符串的一部分,知道字符串
结尾
substring(int beginlndex,int endlndex) 截取调用者字符串的任意一部分,范围:前包
后不包【indexOf重载】indexOf(int ch)顺序查找指定字符第一次出现的索引
【indexOf重载】indexOf(int ch,int fromlndex)从指定位置开始l顺序查战指定字符的索
【indexOf重载】indexOf(String str)顺序查找指定字符串第一次出现的索引
【indexOf重载】indexOf(String str int fromlndex)从指定位置开始l顺序查找指定字符串的索引【lastdexOf重载】:和indexOf相反
- 转换功能
byte[] getBytes() 将调用字符串转为byte数组
char[] toCharArray() 将调用者字符串转为对应的字符数组
toUpperCase() 将字符串转为纯大写
toLowerCase() 将字符串转为纯小写
concati(String str) 将参数字符串追加到调用者字符串之后(拼串)
valueOf 重载:都是静态方法,用于将其它数摆类型转换为String类型
4、Math
- 两个常量
E:自然对数的底数:2.718
P1:圆周率:3.1415926
- 常用方法
abs(数字类型) 求绝对值
cbrt(double d) 开立方
sqrt(double d) 开平方
ceil(double d) 向上取整
floor(double d) 向下取整
max(int a,int b) 求最大值
min(int a,int b) 求最小值
pow(int a,int b) 求a的b次方
random() 返回[0.0,1.0)的随机数
round(double d) 四舍五入,负数部分可以看成“五舍六入”
5、System
- 概述:用于描述系统资源的类型,不能创建对象
- 常用字段
System.out 标准输出流,默认关联显示器
System.in 标准输入流,默认关联键盘
System.err 措误打印流默认关联到显示器,用于打印错误的信息
- 常用方法
gc() 运行垃圾回收器
currentTimeMills() 获取时间的毫秒值
6、StringBuilder
- 概述:是一个可变字符序列,提供了修改字符串的成员方法
- 底层:采用的一个字符数组
- String和StringBuilder
String 是不可变字符序列,没有提供任何的修改其本身的方法,
StringBuilder 是可变字符序列,提供了修改自身的额方法
String 本身长度也不可变,
StringBuilder 长度可变,通常我们将StringBuilder当做是一个长度可变的容器,用于
存储字符
- 构造方法
StringBuilder() 创建一个字符串生成器,初始容量为16
StringBuilder(int capacity) 创建一个字符串生成器,初始容量有capacity指定
StringBuilder(String str)) 创建一个字符串生成器,初始化参数字符串。初始容量为16
+字符串长度【获取容积】capacity() 返回字符串生成器的容量
【获取容积】length() 返回字符串缓冲区中的字符个数
- 添加功能
append(任意类型) ,长度不够<<1+2(扩大两倍+2)
insert(int index,任意数据类型) 可以将任意数据类型插入到指定位置
- 删除功能
deleteCharAt(int index) 删除字符串缓冲区中指定索引上的字符串
delete(int start,int end) 删除字符串缓冲区上一段字符串
- 替换和反转
replace(int start,int end,String str) 使用指定字符串去替换字符串缓冲区中指定字符串
reverse() 反转字符串缓冲区
- StringBuilder和StringBuffer
- 基本类型包装类
概述:提供了操作基本数据类型的一些方法,以及将基本数据类型和其他数据进行转换的方法
byte-->Byte
short-->Short
int--<Integer
long-->Long
float-->Float
double-->Double
boolean-->Boolean
char-->Character
自动拆装箱(JDK5)
7、正则表达式
- 概述:其实正则表达式就是一个字符串,其作用不仅表示一个字符换,还可以表示一类字符串,表示一类字符串的格式或者规则;代码简单,逻辑复杂;坏处就是比较难写。
- 字符类:使用String中的matches() 方法来判断,表示单个字符,使用符号[]
- 预定义字符类:
提前在正则表达式中进行了定义,表示单个位置
预定义字符中的\不是转义字符,就是普通反斜杠
预定义字符中的【.】表示任意字符
- 数量词
表示多个位置的字符
模数量词:X?、X*、X+
精确数量词:{n}、{n,}、{n,m}
8、Date类
- 概述:该类的对象用于表示一个特定的瞬间,精确到毫秒值
- 构造方法:
Date() 表示创建对象刹那的时间
Date(long date) 表示传入毫秒值的时间,是从1970年1月1日0时0分0秒起开始计算
- 成员方法
getTime() 获取Date对象表示的时间的毫秒值
setTime(long time) 将调用者对象的毫秒值设置为指定值
- DateFormat
九、Java中8种基本数据类型
1、byte
byte为字节类型。1字节,8位,最大存储数据量是255,存放的数据范围是-128~127之间。其对应得包装类为Byte。
2、short
short为字节类型。2字节,16位,最大数据存储量是65536,数据范围是-32768~32767之间。其对应得包装类为Short。
3、int
int为整型。4字节,32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。其对应得包装类为Integer。
4、long
long为长整型。8字节,64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。其对应得包装类为Long
5、float
float为单精度浮点型。4字节,32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。其对应得包装类为Float
6、double
double为双精度浮点数。8字节,64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。其对应得包装类为Double
7、char
char为字符类型。2字节,16位,存储Unicode码,用单引号赋值。其对应得包装类为Character
8、boolean
boolean为布尔类型。只有true和false两个取值。其对应得包装类为Boolean。
💡 Tips:一个Byte(字节)等于8个bit(位),bit是最小的单位,1B(字节)=8bit(位)。一般情况下,一个汉字是两个字节,英文与数字是一个字节。
十、异常
1、什么是异常?
异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是:
💡 Tips,异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.
2、异常体系
异常机制其实是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable
,其下有两个子类:java.lang.Error
与java.lang.Exception
,平常所说的异常指java.lang.Exception
。
- Throwable体系:
Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。
- Throwable中的常用方法:
public void printStackTrace()
:打印异常的详细信息。 包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
public String getMessage()
:获取发生异常的原因。 提示给用户的时候,就提示错误原因。public String toString()
:获取异常的类型和异常描述信息(不用)。
3、异常分类
我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。 异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?
- 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
- 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)
4、异常捕获以及处理的常用语法
Java异常处理的五个关键字:try、catch、finally、throw、throws
- try-catch
try {
// 可能发生异常的语句
} catch(Exception e) {
// 处理异常语句
}
在上面的语法中,我们要把可能引发异常的语句封装在try语句块中,用于捕获可能发生的异常。catch语句里的( )中,要传入与try语句里匹配的异常类,指明catch语句可以处理的异常类型。
也就是说,如果此时try语句块中发生了某个异常,那么这个相应的异常对象就会被拋出,产生异常的这行代码之后的其余代码就不会再被执行。然后catch语句就开始执行,把try中拋出的异常对象进行捕获并处理。
当然,如果try语句块中没有发生异常,那么try里的代码就会正常执行结束,而后面的catch就会被跳过。
这里我们需要注意:try...catch与if...else是不一样的,try后面的花括号{ }不可以省略,即便try中只有一行代码。同样的,catch的花括号 { } 也不可以省略。另外,try语句块中声明的变量属于局部变量,它只在try中有效,其它地方不能访问该变量。
📌注意:如果需要catch多个异常,可以使用多重catch语句
try {
// 可能会发生异常的语句
} catch(ExceptionType e) {
// 处理异常语句
} catch(ExceptionType e) {
// 处理异常语句
} catch(ExceptionType e) {
// 处理异常语句
...
}
- try-catch-finally
IO流属于一种比较消耗物理资源的API,使用完之后应该把IO流进行关闭,否则就可能会导致物理资源被过多的消耗。那么我们该在哪里关闭IO流呢?有的人会说,我们可以在try语句块执行完正常的功能后关闭IO流。但是大家要知道,try语句块和catch语句块有可能因为异常并没有被完全执行,那么try里打开的这些物理资源到底要在哪里回收呢?
为了确保这些物理资源一定可以被回收,异常处理机制给我们提供了finally代码块,且Java 7之后又提供了自动资源管理(Automatic Resource Management)技术,更是可以优雅地解决资源回收的问题。
try {
// 可能会发生异常的语句
} catch(ExceptionType e) {
// 处理异常语句
} finally {
// 执行清理代码块,这里的代码肯定会被执行到,除非极特殊的情况发生
}
上面的代码中,无论是否发生了异常(除极特殊的情况外,比如提前调用了System.exit()退出虚拟机的方法),finally语句块中的代码都会被执行。另外,finally语句也可以直接和try语句配合使用,其语法格式如下:
try {
// 逻辑代码块
} finally {
// 清理代码块
}
我们在使用try-catch-finally语句时要注意以下几点:
- 在异常处理的语法结构中,只有try是必需的。如果没有try代码块,则不能有后面的catch和finally;
- 虽然catch块和finally块是可选的,但也不能只有try块,catch块和finally块至少要出现其中之一,也可以同时出现;
- 可以有多个catch块,捕获父类异常的catch块,必须位于捕获子类异常的后面;
- 多个catch块必须位于try块之后,finally块必须位于所有的catch块之后;
- 只有finally与try语句块的语法格式,这种情况会导致异常的丢失,所以并不常见;
- 通常情况下,我们不应该在finally代码块中使用return或throw等会导致方法终止的语句,否则这将会导致try和catch代码块中的return和throw语句失效。
尤其是try-catch-finally与return的结合,是我们面试时的一个考点哦。有些面试官会贱贱地问你try-catch-finally中如果有return,会发生什么,请大家自行做个实验吧。
- 抛出异常throw
在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。 在java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢?
- 创建一个异常对象。封装一些提示信息(信息可以自己编写)。
- 需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字throw就可以完成。throw 异常对象。 throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。
使用格式:
throw new 异常类名(参数);
例如:
throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
📌注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。 那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用throws声明处理。
3、throw和throws区别
- throws:抛出 状态,形容状态
当发生问题之后怎么办,没有正在抛,不是动作,是消极的处理方案 方法体外。用在方法声明后面,跟的是异常类名,可以跟多个异常类名,用逗号隔开。表示抛出异常,由该方法调用者来处理。throws表示出现异常的一种可能性,并不一定会发生这些异常。
- throw:抛 动词
原来有异常,但没发现,没毛病,加throw就举报了,就有异常被发现,(打小报告)正在抛异常,方法体内。在方法体内,跟的是异常对象名,只能输出一个异常对象名。表示抛出异常,由方法体内的语句处理。throw则是抛出了异常,执行throw则一定抛出了某种异常。
4、自定义异常
项目里面一般都是使用自定义异常继承RuntimeException(运行时异常),主要还是给前端返回友好的错误提示,全局自定义异常也是很常用的
//自定义异常类
public class StockException extends RuntimeException{
public StockException(){
super();
}
public StockException(String mes ,Throwable cause , boolean str ,boolean str1 ){
super(mes , cause , str ,str1) ;
}
public StockException(String mes ,Throwable cause){
super( mes , cause);
}
public StockException(String mes){
super( mes );
}
public StockException(Throwable cause){
super( cause);
}
}
//测试自定义异常
public static void main(String[] args) {
try {
submitOrder(); //提交订单方法
} catch (Exception e) {
String mes = e.getMessage();
System.out.println("提示用户:"+mes);
// TODO: handle exception
}
}
//提交订单处理方法
public static void submitOrder(){
int stock = 0; //库存数量
if(stock == 0){
throw new StockException("库存不足"); //抛出自定义异常
}
}
十一、集合
1、Collection(单列)
Collection单列集合下又细分为List、Set两种。
List集合允许重复,其实现类有ArrayList、LinkedList、Vector;ArrayList在jdk底层维护bject类型的可变数组,LinkedList底层实现了双向链表和双端队列,可以添加任意元素(元素可以重复),包含null,线程不安全,没有实现同步。Vector是一个可变数组,相比这ArrayList而言,安全,但效率不高
名字 | 底层结构 | 版本 | 线程安全(同步)效率 | 扩容备份 |
---|---|---|---|---|
ArrayList | 可变数组 | jdk1.2 | 不安全,效率高 | 如果使用有参构造器按照1.5倍扩容,如果是无参构造器。1.第一次扩容10;2.从第二次开始按照1.5倍 |
Vector | 可变数组Object[] | jdk1.0 | 安全,效率不高 | 如果是无参,默认10,满后,按照2倍扩容。如果是指定大小创建Vector,则每次按照2倍扩容. |
Set是无序(添加和取出的顺序不一致),没有索引 不允许重复元素,所以最多包含一个null,JDK API中Set接口的实现类有:EnumSet、HashSet、TreeSet、LinkedHashSet、CopyOrWriteArraySet等。HashSet地城采用的是
2、Map(双列)
- Map接口的特点
- Map与Collection并列存在。用于保存具有映射关系的数据。
- Map中key和value可以是任何引用类型的数据,会封装HashMap$Node对象中
- Map中的key不允许重复,hash散列,
原因和HashSet一样,前面分析过源码
- Map的value可以重复
- Map的key可以为null,value也可以为null,注意key为null,只能有一个,value为null可以有多个
- 常用String类作为Map的key
- key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
- HashMap数据存储分析:
HashMap中两个内部类,用于储存Hashmap数据中的key、value
KeySet内部类
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
Values内部类
final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
-
Map接口的常用方法
- Map的六大遍历方式
- 取出Map的keySet,使用增强for
- 取出Map的keySet,使用迭代器
- 通过EntrySet的增强for循环
- 通过EntrySet的迭代器
- 通过把所有的value取出,类型为Values
- 通过EntrySet取出所有的 k-v
- Map的小结
- Map接口的常用实现类:HashMap、HashTable和Properties
- HashMap是Map接口使用频率最高的实现类
- HashMap是以key-value对的方式来存储数据的
- key不能重复,但是值可以重复,允许使用null键和null值
- 如果添加相同的key,则会覆盖原来的key-value,等同于修改。(key不会替换,value会替换)
- 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(jdk1.8的hashMap底层:数组+链表+红黑树)
- HashMap没有实现同步,因为是线程不安全的,方法没有做同步互斥的操作,没有使用synchronized
- Map接口的实现类HashMap
HashMap底层机制及源码剖析:
- HashMap底层维护了Node类型的数组table,默认为null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75。
- 当添加key-val时,通过key的哈希值得到在tablef的索引,然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素, 继续判断该元素的key是否和准备加入的key是否相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果 添加时发现容量不够,则需要扩容。
- 第1次添加,则需要扩容table容量为16,临界值(threshold)为12.
- 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍,即24,依次类推
- 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
- HashMap源码分析
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("java", 10);//ok
map.put("php", 10);//ok
map.put("java", 20);//替换 value
System.out.println("map=" + map);
/*老韩解读 HashMap 的源码+图解
1. 执行构造器 new HashMap() '
初始化加载因子 loadfactor = 0.75
HashMap$Node[] table = null
2. 执行 put 调用 hash 方法,计算 key 的 hash 值 (h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) {//K = "java" value = 10
return putVal(hash(key), key, value, false, true);
}
3. 执行 putVal
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 数组为 null, 或者 length =0 , 就扩容到 16
if ((tab = table) == null || (n = tab.length) == 0)
//取出 hash 值对应的 table 的索引位置的 Node, 如果为 null, 就直接把加入的 k-v
//, 创建成一个 Node ,加入该位置即可
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;;//辅助变量
// 如果 table 的索引位置的 key 的 hash 相同和新的 key 的 hash 值相同,
// 并 满足(table 现有的结点的 key 和准备添加的 key 是同一个对象 || equals 返回真)
// 就认为不能加入新的 k-v
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) //如果当前的 table 的已有的 Node 是红黑树,就按照红黑树的方式处 理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果找到的结点,后面是链表,就循环比较
for (int binCount = 0; ; ++binCount) {//死循环
if ((e = p.next) == null) {//如果整个链表,没有和他相同,就加到该链表的最后
p.next = newNode(hash, key, value, null);
//加入后,判断当前链表的个数,是否已经到 8 个,到 8 个,后
//就调用 treeifyBin 方法进行红黑树的转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果在循环比较过程中,发现有相同,就 break,就只是替换 value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;//替换,key 对应 value
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//每增加一个 Node ,就 size++
if (++size > threshold)//如 size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
4. 关于树化(转成红黑树)
//如果 table 为 null ,或者大小还没有到 64,暂时不树化,而是进行扩容
//否则才会真正的树化 -> 剪枝
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();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
*/
}
到此第一阶段就写到这,关注+收藏。我会持续更新后续内容