1. Java集成开发环境(IDE) IntelliJ IDEA下载
- 先从官网上下载Ultimate版exe安装包(2022.3.2版):https://www.jetbrains.com/idea/download/
- 按步骤安装IDEA即可
2. Java开发工具包JDK的下载、安装与环境变量配置
- 进入Oracle下载JDK的官网:https://www.oracle.com/java/technologies/downloads/
- 下载和自己电脑适配的JDK安装包exe文件,安装即可;也可从Java archive中下载老版本JDK
- 新建
JAVA_HOME
系统变量,变量值为JDK安装路径 - 在Path系统变量中新建两个变量
%JAVA_HOME%\bin
与%JAVA_HOME%\jre\bin
- 打开cmd并输入
java -version
命令显示版本号
3. IDEA对远程Git项目的检出和提交
先确保已下载Git、IDEA已经配置好Git
- 打开Git官网https://git-scm.com/download/win,下载
64-bit Git for Windows Setup
并安装git(一路next)
创建Git账号和邮箱,打开Git Bash:git config --global --replace-all user.name "你的 git 的名称"
git config --global --replace-all uesr.email "你的 git 的邮箱"
- 打开IDEA,File->Settings->Version Control->Git->Path to Git executable:选择git安装后位于bin文件夹下的git.exe文件,点击Test按钮进行测试是否成功
3.1 IDEA对Git项目检出
- 克隆整个项目:File->New->Project from Version Control,输入URL和想要克隆到的文件夹,点击Clone即可
- 获取远程仓库中项目的某些修改:右键->Git->Fetch,点击右下角master,本地库右边若有箭头,则说明本地库和远程库不一样,则可点击本地库->Update对本地库进行更新
- 获取远程仓库中的新分支:右键->Git->Fetch,点击右下角master,点击远程库的新分支->Checkout即可
- 将分支合并到主干:在主干上右键->Git->Merge,选择要合并的分支即可,同时点击主干->Push同步到远程仓库
- 项目要发布版本:项目右键->Git->New Tag,写明Tag Name,再右键->Git->Push(勾选左下角Push Tags)即可
3.2 IDEA使用Git提交代码
- VCS->Create Git Repository->选择要提交的项目
- 选中要提交的项目,右键->Git->Commit Directory,再选择要提交的文件,并写上提交信息注释即可提交到本地仓库
- 对于同项目中的新的文件,可以先右键->Git->Add,再进行步骤(2)完成新文件的提交
- 前3步是将代码提交到本地仓库中,右键->Git->Push,再输入远程库的Name和URL,以及账户和密码即可提交到远程仓库
- 可以通过右键->Git->Branches创建新的分支(点击分支->Checkout可以切换分支),再通过步骤(3)和(4)将分支上传到Git远程仓库
4. Java基础语法
Java是一种混合型语言,先将.java文件编译成.class二进制字节码文件,然后再按行交给JVM进行解释
JDK:Java开发工具包,包含了JVM、核心类库、开发工具(如javac、java、jdb等)。
JRE:Java运行环境,包含JVM、核心类库、运行工具(如java)。
原码:最高位为符号位,0正1负,原码进行正数计算不会出现问题,负数计算的结果和预期结果相反。
反码:为了解决原码不能计算负数,负数反码:符号位不变,其余位取反;反码计算时跨0会出错,由于有-0和+0两个0。
补码:为了解决反码不能跨0,负数补码=反码+1,因此10000000规定为-128,计算机数字的存储和计算都是以补码的形式进行的。
4.1 Java数据类型
![](https://i-blog.csdnimg.cn/blog_migrate/3deccb926dacb668b913254861686c48.png)
注:(1) long类型定义变量时,数据值最后应加上L或l;float类型定义变量时,数据值最后应加上F或f
(2) 基本数据类型数据值存储在自己的空间中,赋值给其它变量时赋的是真实值
(3) 引用数据类型变量在栈空间中存储的是地址值,数据值存储在该地址指向的堆空间中,赋值给其它变量时赋的是地址值
(4) ==号对于基本数据类型比较的是数据值,而引用数据类型比较的是地址值
(5) 基本数据类型作为实参传入方法的形参时,会重新生成一个值相等的新的变量,因此变量原型不会发生改变;而引用数据类型作为实参传入方法的
形参时,由于传递的是引用数据类型的地址值,在方法中改变参数变量的同时变量原型也会发生改变(String字符串由于是不可变的,故不作考虑)。
4.2 Java的输入与输出
一、Java的输入:Scanner类
- 导包
import java.util.Scanner;
- 创建Scanner对象
Scanner scan = new Scanner(System.in);
- 接收数据
int a = scan.nextInt();
注:Scanner类有针对不同类型的接收方法,如next()和nextLine()接收字符串、nextInt()接收整数、nextDouble()接收小数等
nextLine()是遇到回车就停止接收数据,其它都是遇到空格就停止接收数据
二、Java的输出
- System.out.println(“输出的内容(输出完换行)”);
- System.out.print(“输出的内容(输出完不换行)”);
- System.out.printf(); // 与C语言的用法一致,%d(整数占位符),%f(浮点数占位符),%s(字符串占位符),%c(字符占位符)
注:只要有字符串参与的+运算,就代表连接操作;对于多个+则从左到右依次执行,但只有遇到字符串后+才开始变成连接符
4.3 Java的判断与循环
一、Java的判断语句
- if语句(可嵌套、可单条件判断、双条件判断和多条件判断)
if (关系表达式) {
关系表达式为true时要执行的语句;
} else {
关系表达式为false时要执行的语句; //当不考虑关系表达式为false时的情况可以省略else
}
- switch语句(case后的值只能是字面量且不能重复;语句体结束后必须加break,否则当匹配后因case穿透仍会执行它下面的case语句体)
switch (表达式) {
case 值1: //表达式值为值1时,则执行语句体1。注:若对于值1、值2要执行的语句体一样,则可写为case 值1,值2:相同语句体;
语句体1;
break;
case 值2: //表达式值为值2时,则执行语句体2
语句体2;
break;
...
default: //表达式值与上述的值都不匹配时,则执行语句体n+1
语句体n + 1;
break;
}
- switch语句新特性(适用于JDK12及之后的版本,用{}代替了break)
switch (表达式) {
case 值1 -> {
语句体1;
}
case 值2 -> {
语句体2;
}
...
default -> {
语句体n + 1;
}
}
注:if语句的多条件判断:一般适用于对范围的判断,如考试成绩的分段
switch语句:一般适用于可以将数据一一列举出来,如星期几
switch语句新特性:数据类型 变量名 = switch(表达式){ }; 其中语句体的赋值可以省略
二、Java的循环语句(continue跳过当前层的本次循环;break结束当前层的整个循环;可在循环语句前加上loop:,再用break loop;
语句跳出指定层的循环(一般用于多层嵌套循环),loop只是一个标记,用别的字符也可以表示,continue同样适用)
- for循环
for (初始化语句; 条件判断语句; 条件控制语句) {
循环体语句;
}
- while循环
初始化语句;
while (条件判断语句) {
循环体语句;
条件控制语句;
}
- 增强for循环(针对数组和单列集合,底层是迭代器)
for(数组/集合的数据类型 变量名 : 数组名/集合名) {
循环体语句(只适合取数据,不能更改数据);
}
注:for循环和while循环的区别(for循环中的初始化语句只在for循环中可用,出了for循环则不可用)
for循环:知道循环次数或循环范围
while循环:只知道循环结束条件
4.4 Java数组
一、一维数组的定义与初始化
- 一维数组的定义
方式一:数据类型[] 数组名
如int[] array;
方式二:数据类型 数组名[]
如int array[];该方式不推荐(不符合阿里巴巴编码规范) - 一维数组的初始化
静态初始化(明确具体数据):数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, 元素3...};
//new 数据类型[]可省略,直接跟{}
动态初始化(只明确元素个数,不明确具体值):数据类型[] 数组名 = new 数据类型[数组长度];
- 拷贝数组
(1)for循环:基本数据类型拷贝的数据值;引用数据类型拷贝的是地址值(共享数据)。
(2)clone():新数组 = 源数组.clone()。与for循环结果一样。
(3)System.arraycopy(源数组, 起始索引, 目的地数组, 起始索引, 拷贝个数):与for循环结果一样。(拷贝速度最快)
(4)新数组 = Arrays.copyOf(源数组,新数组长度):与for循环结果一样。(底层仍是用(3)实现的)
二、二维数组的定义与初始化
- 二维数组的定义
方式一:数据类型[][] 数组名
如int[][] array;
方式二:数据类型 数组名[][]
如int array[][];该方式不推荐(不符合阿里巴巴编码规范) - 二维数组的初始化
静态初始化(明确具体数据):数据类型[][] 数组名 = new 数据类型[][]{{元素1,元素2,...},{元素3,元素4,...},...};
//new 数据类型[][]可省略,直接跟{};其中二维数组包含的一维数组元素的长度可以不同,建议把每一个一维数组单独写成一行便于阅读
动态初始化(只明确元素个数,不明确具体值):数据类型[][] 数组名 = new 数据类型[m][n];
其中m表示二维数组中一维数组的个数;n表示每个一维数组中元素的个数。注意:可以不指定n的值,后期再创建一维数组,再将一维数组的地址赋值给二维数组表示的一维数组,如array2D[0] = new int[5];
array2D[0]指的是第一个一维数组的地址。也可以指定n的值,后期再将地址覆盖也可实现变长二维数组。
三、数组的长度(array.length
,该length是数组的属性,不是方法,故不加括号)
4.5 Java方法(内含对权限修饰符的描述)
一、方法的定义
修饰符 返回值类型 方法名([参数类型 参数名,...]) {
...
方法体
...
[return 返回值;]
}
修饰符:定义了方法的访问类型
- 访问权限修饰符
default
(即默认,什么也不写):在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法
private
:在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
public
:对所有类可见。使用对象:类、接口、变量、方法
protected
:对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类) - 非访问权限修饰符
static
:用来修饰静态方法和静态变量(属于类的,不属于对象,不用实例化就可以通过类名.方法名
调用,静态方法只能调用静态方法和静态变量,非静态方法都可以访问,且静态方法没有this关键字),静态变量存储在堆内存的静态区中,是该类所有对象共享的,静态变量和方法是随着类的加载而加载的,优先于对象出现的。
final
:用来修饰类、方法和变量。final 修饰的类不能够被继承;final修饰的方法不能被继承类重新定义;final修饰的变量为常量,只能被赋值一次(常量命名规则:单个单词全部大写;多个单词全部大写,单词之间用下划线隔开)(final修饰的变量若为基本类型,那么变量存储的数据值不能发生改变;final修饰的变量若为引用类型,那么变量存储的地址值不能发生改变,对象的内部可以改变)。
abstract
:用来创建抽象类(不能用来实例化对象,目的为了对该类进行扩充)和抽象方法(没有任何实现的方法,该方法的具体实现由子类提供)。
synchronized
:声明的方法同一时间只能被一个线程访问。
volatile
:修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值;当成员变量发生变化时,会强制线程将变化值回写到共享内存;保证不同线程在任何时刻看到的成员变量都是相同的值。
返回值类型:方法可能会有返回值,若无返回值则用关键字void
方法名:所定义方法的名称,小驼峰命名法(首单词的首字母小写,其后的单词的首字母都大写),如getSum
可定义为求和方法名
参数类型:形参,像是一个占位符,当方法被调用时,将实参的值传递给该参数,可有多个形参也可一个没有
方法体:定义该方法功能的代码
二、方法的重载
定义:在同一个类中,方法名相同,参数不同(参数类型、参数个数、参数顺序),与返回值无关
优点:(1)定义端(方法的提供者):使用相同的方法名(一个方法)来表示功能相同的(多个)方法
(2)调用端(方法的使用者): 在调用的时候,可以使用相同名字(一个名字)的方法实现不同的功能
(3)重载也是多态性的体现:一个内容,可以实现多个功能
当方法出现重载现象,JVM会优先调用实参和形参类型一致的那个方法。
三、可变参数
方法的形参的个数是可以发生变化的,格式:属性类型...形参名
,如int…args,就可以传入不确定个数的int类型的数据,底层仍是通过数组实现的,args就相当于数组名。但是在方法中最多只能有一个可变参数,且如果还有其它形参,则可变参数必须写在最后。
5. Java面向对象
5.1 类与对象
一、类:类是对某一类事物的描述,是抽象的、概念上的意义
- 类的定义
public class 类名 { //类名采用大驼峰命名格式,即全部单词的首字母大写;类不能用private进行修饰,即类不能是私有的
(1) 成员变量(属性,名词)
(2) 成员方法(行为,动词)
(3) 构造器
(4) 代码块
(5) 内部类 //见5.6节
}
- 构造方法
在创建对象的时候给成员变量进行初始化。构造方法名与类名完全一致,无返回值,可有多个构造方法(系统会根据传入的参数选择对应的构造器)。构造方法是在创建对象的时候由虚拟机调用
的,不能手动调用构造方法,若不写任何构造方法,虚拟机会给出一个无参构造方法,若写了一个有参构造,则必须写无参构造才可以无参实例化对象;每创建一次对象,就会调用一次构造方法。无论是否使用,建议无参构造和有参构造都写。若唯一的构造方法用private修饰了,证明该类无法被创建成对象,即无法实例化对象,则只能根据类名调用该类的静态成员和静态方法。
修饰符 类名(参数) {
方法体;
}
- 代码块用{}括起来的代码
(1)局部代码块:写在方法里用{}括起来的代码,可提前结束局部变量生命周期,提高内存利用率。
(2)构造代码块:写在类中方法外用{}括起来的代码,用以提取多个构造方法的重复代码,每次调用构造方法都会执行,并且在构造方法前执行。
(3)静态代码块:写在类中方法外用static{}
括起来的代码,随着类的加载而加载(即要使用这个类的时候),并且自动触发,只执行一次。可以在程序运行时提前完成一些只进行一次的数据初始化操作。 - 类的分类
JavaBean类:用来描述一类事物的类(无main方法),成员变量私有且都有对应set与get方法(Alt+Insert
),有参和无参构造方法,toString()方法以及其它成员方法,ptg插件可快速生成标准JavaBean。
测试类:用来检查其它类是否书写正确,带有main方法的类,是程序的入口(一个java源文件只能有一个public类,且类名与文件名相同)
工具类:帮我们做一些事情的类,类名要见名知意,构造方法私有化(防止创建该类对象),方法都定义为静态(方便调用)
二、对象:实际存在的该类事物的实例化个体
- 类对象的获取:
类名 对象名 = new 类名();
- 访问属性:
对象名.成员变量
- 访问行为:
对象名.方法名
三、封装
定义:封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。把类中的某些信息进行隐藏,从而使外部程序不能直接对这些信息进行直接的访问,只能通过类中定义的方法对这些隐藏的信息进行操作和访问。
目的:防止该类的代码和数据被外部类定义的代码随机访问;要访问该类的代码和数据,必须通过严格的接口控制。提高了程序的安全性和便利性,对象代表什么,就得封装对应的数据,并提供数据对应的行为,如get、set方法。
四、this关键字(this中存的是当前对象的地址值,对于非静态方法都有一个隐藏的当前类对象this形参,静态方法则没有)
this.属性名
:访问的是类的成员变量,用来区分成员变量和局部变量的重名问题this.方法名
:访问本类的成员方法this()
:访问本类的构造方法(可以有参数来指定对应的有参构造,只能在构造方法中且必须是第一条语句)
5.2 Java的继承
- 定义:继承就是子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法。当类与类之间,存在相同(共性)的内容,并满足子类是父类的一种,就可以考虑使用继承,来优化代码,提高代码的复用性。
- 继承的格式
public class 子类 extends 父类 { } //一个子类只能继承一个父类,但可以多层继承(父类可以还有它的父类)
- 每一个类都直接或间接的继承于Object类(若自定义的类没有继承某个父类,JVM则会自动添加一个默认的继承Object父类)。
- 构造方法不能被子类继承,子类中的所有构造方法默认先访问父类中的无参构造(子类在初始化时可能用到父类的数据,如果父类没有完成初始化,子类将无法使用父类的数据),再执行自己的。子类构造方法第一句默认都是
super();
,不写也存在,且必须是第一句。也可以用super(参数…)调用父类的有参构造。 - 成员变量(无论私有还是非私有)都可以被子类继承,但是子类对继承的私有成员变量不能直接使用。
继承中成员变量访问特点:就近原则(先在局部位置找,本类成员位置找,父类成员位置找,逐级往上,没找到就报错),通过this.成员变量名
调用本类的成员变量(不存在就会从父类中找),通过super.成员变量名
调用父类的成员变量。 - 成员方法能被添加到虚方法表中的虚方法(非private,非static,非final) 可以被子类继承,子类从父类中拷贝一份虚方法表,使子类的虚方法表指针指向新的虚方法表,如果子类中覆写了父类的虚方法,则将方法表中覆写方法的方法指针替换为子类覆写的方法指针,如果子类中有新增的虚方法,则在该子类的虚方法表中追加新增的虚方法指针。对不能继承的方法,则会往父类向上一层一层的找,找到之后对于private和final修饰的方法仍不能调用,而static修饰的方法可以调用,但是不被子类继承。
继承中成员方法访问特点:同样是就近原则,this.方法名()
调用本类的成员方法(不存在就从父类中找),super.方法名()
调用父类的成员方法。
方法的重写(覆盖):当父类的方法不能满足子类的需求时,可以在子类中对继承的方法进行重写(方法名、形参一致;访问权限>=父类;返回值类型<=父类),本质是子类覆盖了从父类继承下来虚方法表中的虚方法,只有在虚方法表中的方法才可以被重写,在重写的方法上面加上@Override
注解可以验证重写是否存在语法错误。
5.3 Java的多态
- 定义:同一个行为具有多个不同表现形式或形态的能力;同一个接口,使用不同的实例而执行不同操作。
前提:继承/实现关系;方法的重写;父类引用指向子类对象,例如:父类类型 对象名称 = new 子类类型()
。
好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理,如使用父类类型作为参数,则可以接收所有子类对象,体现多态的扩展性与便利。 - 调用成员变量的特点:编译看左边,运行也看左边。左边代表父类,右边代表子类,即无论编译还是运行,判断编译是否正确和运行时的取值都是父类的成员变量。
调用成员方法的特点:编译看左边,运行看右边。即编译时先判断父类是否存在该成员方法;运行时由于子类重写了父类方法致使虚方法表的虚方法被覆盖,且多态本质是实例化的子类对象,因此运行时执行的是子类重写的成员方法。 - 多态的优点(多态本身是一种向上转型:子类对象赋值给一个父类引用)
(1)消除类型之间的耦合关系(解耦),
(2)可扩充性:新增子类不影响现在类的特性。
(3)定义方法的时候,使用父类类型作为参数,则可以接收所有子类对象,体现多态的扩展性与便利,例如StringBuilder容器正是由于多态才可以通过append()方法添加任何数据(由于任何类都是Object的子类)。 - 多态的缺点:无法直接访问子类特有的成员,可以向下转型:使用强制类型转换的格式,将父类引用转为子类引用,再调用特有子类的成员
(1)在强制转换之前可以先用对象名 instanceof 类名
来判断该对象是否是该类的类型,再进行强制转换。
(2)JDK14之后,可用对象名 instanceof 类名 新的对象名
将判断和强转合并为一句,若是则强转后的变量名为新的对象名
,若不是则返回false。
5.4 Java的抽象类
- 抽象方法:将共性的行为(方法)抽取到父类之后,由于每一个子类执行的内容是不一样的,因此在父类中就不能确定具体的方法体,该方法就可以定义为抽象方法。定义格式:
public abstract 返回值类型 方法名(参数列表);
- 抽象类:如果一个类中存在抽象方法,那么该类就必须声明为抽象类;而一个抽象类中不一定有抽象方法。定义格式:
public abstract class 类名{}
。抽象类不能被实例化;抽象类可以有构造方法(便于子类定义构造方法super,当创建子类对象时,给属性赋值);抽象类的子类要么重写抽象类中的所有抽象方法,要么子类也是抽象类;抽象类虽然不能实例化对象,但可以定义抽象类的引用,仍可以完成抽象的引用指向子类对象,即抽象类也可以实现多态。 - 抽象方法和抽象类的意义:强制子类必须按照定义的抽象方法的格式进行重写。
5.5 Java的接口
- 接口是一种规则,是对行为的抽象。若想要某个类拥有某行为,让该类实现对应类的接口即可。
- 接口的定义用
interface
实现:public interface 接口名 {}
- 接口不能被实例化,但是可以被实现;接口和类之间是实现关系,用
implements
关键字来实现,public class 类名 implements 接口名 {}
;一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类;一个类只能继承一个父类,但可以实现多个接口。 - 接口成员变量只能是常量,默认修饰符
public static final
;接口没有构造方法;接口成员方法只能是抽象方法(JDK7之前),默认修饰符public abstract
。 - 接口和接口之间是继承关系,可以单继承,也可以多继承。若实现类实现了子接口,则需要重写所有的抽象方法。
- JDK8新特性:接口中可以定义有方法体的方法(default,static),由于在接口中每次添加新的抽象方法(接口升级),该接口的全部实现类都要再重新这个新添加的抽象方法。
(1)允许在接口中定义默认方法(default修饰且不能省略),public default 返回值类型 方法名(参数列表) {}
。默认方法不是抽象方法,因此不强制实现类重写,但是如果默认方法被实现类重写,则重写时应去掉default关键字
;如果实现多个接口有同名的默认方法,那么实现类必须对该默认方法进行重写。
(2)允许在接口中定义静态方法(static修饰且不能省略),public static 返回值类型 方法名(参数列表) {}
。接口中的静态方法只能通过接口名调用,不能通过实现类名或对象名调用。 - JDK9新特性:接口中可以定义私有方法(private),为了提取接口中默认方法和静态方法重复代码,并且只为接口服务,不被外类访问。
private 返回值类型 方法名(参数列表) {}
为默认方法提供服务;private static 返回值类型 方法名(参数列表) {}
为静态方法提供服务(静态方法只能访问静态方法和静态变量)。 - 接口的多态:定义方法的时候,使用接口作为参数,则可以接收接口所有实现类对象,接口引用指向实现类对象。 例如:
接口类型 对象名称 = new 实现类类型()
- 适配器设计模式:由于实现类要实现接口的全部抽象方法,但可能我们只需要其中的几个方法,因此可以定义一个抽象类(抽象的目的是不让实例化这个类,没有意义)实现接口的全部抽象方法并重写为空,再由某个类来继承该抽象类,就可以重写需要的那几个方法,这个抽象类即适配器类(命名规则XXXAdapter)。若某个类有父类,则可以先让这个中间适配器类继承该父类,再让这个类继承这个中间适配器类。
5.6 Java的内部类($)
- 定义:在一个类(外部类)的里面,再定义一个类(内部类)。内部类表示的事物是外部类的一部分,内部类单独出现没有任何意义,例如汽车的发动机。
- 访问特点:内部类可以直接访问外部类的成员(包括私有);外部类要访问内部类的成员,必须创建对象。在内部类的方法中对成员的访问:当成员变量重名时,
this.成员变量名
调用内部类的成员变量;外部类名.this.成员变量名
调用外部类的成员变量,这是因为在执行内部类方法的时候会默认添加一个变量外部类名.this
即外部类的地址值。 - 成员内部类:写在成员位置的内部类,属于外部类的成员。JDK16之后才可以在成员内部类里定义静态变量。
(1)成员内部类对象创建方式一:直接创建外部类名.内部类名 对象名 = new 外部类名().new 内部类名();
,相当于先创建一个外部类对象后再创建内部类对象。
(2)成员内部类对象创建方式二:当内部类用private修饰时,在外部类编写get方法,对外提供内部类对象,Object 内部类对象名 = 外部类对象.getXXX();
,由于内部类私有,外部其它类识别不了内部类,可用它的父类引用,但是这样的话对内部类特殊的成员和方法就无法使用了,因此在用内部类对象的时候直接使用外部类对象.getXXX()
来获取,不赋值给某变量。 - 静态内部类:用
static
修饰的内部类,属于成员内部类的一种。静态内部类只能访问外部类中的静态变量和静态方法,若要访问外部类的非静态成员则需要创建外部类对象。静态内部类对象创建方式:外部类名.内部类名 对象名 = new 外部类名.内部类名();
,静态内部类中的非静态成员只有先创建对象后才可以调用;静态内部类中的静态成员可直接调用外部类名.内部类名.方法名()
。 - 局部内部类:将内部类定义在方法里,类似方法里的局部变量(不能用public修饰)。
- 匿名内部类:隐藏了名字(外部类名$序号)的内部类,可以写在成员位置(此时的匿名内部类就属于一个没有名字的成员内部类),也可以写在局部位置(此时的匿名内部类就属于一个没有名字的局部内部类),
new 类名或接口名(){ 重写的方法 };
定义了一个继承某类或实现某接口并重写方法的子类对象或实现类对象,这样就可以更加方便的使用多态类名或接口名 对象名 = new 类名或接口名(){ 重写的方法 };
而不用再重新定义一个继承某类的子类或实现某接口的实现类了,尤其是那些只使用一次的子类或实现类。
5.7 Lambda表达式
函数式编程(Functional programming)是一种思想特点。函数式编程思想,忽略面向对象的复杂语法,强调做什么,而不是谁去做。Lambda表达式就是函数式思想的体现,Lambda 表达式,也可称为闭包,它允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
格式:(形参) ->{ 方法体 }
(1)参数类型可省略不写。
(2)如果只有一个参数,参数类型可省略不写,()也可省略不写。
(3)如果方法体只有一行,{}、return和分号都可省略不写,这三个需同时省略。
Lambda表达式只能用于实现接口的匿名内部类,且该接口中只能有一个抽象方法,且它是作为函数的参数出现的,可以在接口上方用@FunctionalInterface
注解来验证是否是函数式接口。可以使代码更加简洁。
5.8 Java四大核心函数式接口
- 消费型接口:
Consumer<T>
,抽象方法void accept(T t);,接收一个参数但无返回值,常用于打印等消费动作。 - 供给型接口:
Supplier<T>
,抽象方法T get();,没有输入参数但返回一个T类型数据,常用于信息的获取。 - 函数型接口:
Function<T,R>
,抽象方法R apply(T t);,对传入参数T进行操作,返回数据R,类似于一个映射函数。 - 断言型接口:
Predicate<T>
,抽象方法boolean test(T t);,判断类型为T的对象是否符合约束条件,用于条件判断。
5.9 方法引用
方法引用:把已经有的方法拿过来用,当作函数式接口中抽象方法的方法体。::
方法引用符
条件:
(1)引用处必须是函数式接口。
(2)被引用方法必须已经存在,且形参和返回值必须和抽象方法保持一致,且被引用方法功能要满足当前需求。
- 引用静态方法:
类名::静态方法名
- 引用成员方法:
对象::成员方法
,本类可用this来代替本类对象(静态方法无this和super),父类可用super来代替父类对象。 - 引用构造方法:
类名::new
,用于对象的创建,如用Stream流的map将某数据映射为对象。 - 使用类名引用成员方法:
类名::成员方法
,该方法要保证成员方法的形参和抽象方法从第二个开始的形参保持一致即可。 - 引用数组的构造方法:
数据类型[]::new
5.10 Java的8大包装类
由于Java基本数据类型int、double、char等不是对象,不符合Java面向对象编程特点,因此每个基本数据类型都设计了一个对应的类进行代表,统称为包装类(Wrapper Class),均位于java.lang
包。
可通过静态方法来创建对象,如Integer integer = Integer.valueOf(int value);
,注:-128~127范围的Integer是提前创建好的。
自动装箱
:基本数据类型会自动转换为引用数据类型(底层仍是调用上边静态方法),如Integer integer = 10;
自动拆箱
:包装类会自动转换为对应的基本数据类型,如int i = integer;(JDK5之后出现的)
常用方法:
(1)将整数转换为二进制字符串 String str = Integer.toBinaryString(int value);
(2)将整数转换为八进制字符串 String str = Integer.toOctalString(int value);
(3)将整数转换为十六进制字符串 String str = Integer.toHexString(int value);
(4)字符串按进制转换为int整数 int value = Integer.parseInt(String str, int radix); //8大包装类除了Character都有对应的强转方法
![](https://i-blog.csdnimg.cn/blog_migrate/fdb4ea67415cff2d97e31c7a6e2e8d06.png)
5.11 泛型
泛型是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。
泛型的格式:<数据类型>,注意:泛型只能支持引用数据类型,且可以传入该数据类型的子类类型,不写<>则默认是Object类型。
引入泛型的原因:对于集合来说,如果没有泛型,那么集合存储的是Object类型,可以存储全部类型对象(向上转型),但是在取出数据的时候,会无法使用子类特有的行为,虽然可以向下转型,但是每次还得判断该子类对象的类型instanceof,否则容易出现强转异常。引入泛型就可以统一数据类型,把运行时的问题提前到了编译期间,避免了强转异常。
Java泛型是伪泛型:由于泛型是JDK5引入的,为了向下兼容,这个泛型只是在添加的时候判断是否是指定的泛型类型,但在集合中存储的仍是Object类型,取出的时候会根据泛型类型向下转型。
- 泛型类: 当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类。
修饰符 class 类名<类型> {}
,类型常用T(type)、E(element)、K(key)、V(value)来代表,创建该类对象的时候,E等就会确定类型。 - 泛型方法:当方法中形参类型不确定时,可定义泛型方法。
修饰符 <E> 返回值类型 方法名(类型 变量名) {}
,当实参传入方法时,就会自动确定泛型的数据类型。 - 泛型接口:当接口中类型不确定时,可定义泛型接口。
修饰符 interface 接口名<类型> {}
,可在实现类定义时直接指定接口的类型,也可在实现类定义时延续该泛型。
注意:泛型不具备继承性,即泛型一旦指定具体数据类型,只能传递该类型的数据,如某方法的形参是指定数据类型的集合,则调用该方法只能传该种数据类型的集合,而不能是其子类数据类型的集合。但是数据具备继承性,如在父类集合种添加子类对象。
泛型的通配符:可以限定泛型种类型的范围,比如限定在某继承体系中。
(1)? :类似于E,表示任何不确定的类型,但是不用。
(2)? extends E :表示可以传递E类型或E的所有子类类型。
(3)? super E:表示可以传递E类型或E的父类类型和父类的父类等。
5.12 Java的内存管理
- 程序计数器(Program Counter Register): 程序计数器是一个较小的内存区域,它是线程私有的,它保存的是当前线程正在执行的字节码指令的地址。在多线程环境下,程序计数器能够保证线程切换后能恢复到正确的执行位置。
- Java虚拟机栈(Java Virtual Machine Stacks): Java虚拟机栈也是线程私有的,它用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法在执行时都会创建一个栈帧(Stack Frame),每个栈帧包含了方法的局部变量、操作数栈等信息。当一个方法被调用时,其对应的栈帧会被压入栈中,当方法执行完毕时,栈帧会被弹出栈。
- 本地方法栈(Native Method Stack): 本地方法栈与Java虚拟机栈类似,只不过它是为执行本地方法(Native Method)服务的。本地方法是使用其他语言(如C/C++)编写的,它们不使用Java虚拟机的字节码指令,而是直接调用底层操作系统的接口。
- Java堆(Java Heap): Java堆是Java虚拟机管理的最大的一块内存区域,它是线程共享的。Java堆用于存储对象实例和数组对象,由垃圾回收器负责管理内存的回收和分配。Java堆被划分为年轻代和老年代两部分。年轻代又被划分为Eden区和两个Survivor区(通常是一个较大的Eden区和两个较小的Survivor区),对象在年轻代中被创建和销毁,而在老年代中存活时间较长。
- 方法区(Method Area): 方法区也是线程共享的,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK8及以前的版本中,方法区是HotSpot虚拟机中的一个实现,从JDK8开始,方法区被替换为元空间(Metaspace)。
- 运行时常量池(Runtime Constant Pool): 运行时常量池是方法区的一部分,它用于存储编译期生成的各种字面量和符号引用。字面量包括字符串、数字常量等,符号引用包括类和方法的全限定名、字段的名称等。
- 直接内存(Direct Memory): 直接内存不是Java虚拟机规范中定义的一部分,但它是一块与Java堆外的内存区域,它可以通过Java的NIO(New IO)库直接操作。在使用NIO时,可以通过直接内存来减少数据在Java堆和操作系统之间的复制,提高IO操作的性能。直接内存是一种在Java堆外分配的内存空间,其生命周期不受Java垃圾回收的控制,因此可以更灵活地管理内存,并且避免频繁地将数据从Java堆复制到操作系统内核缓冲区。
6. Java的API
API(Application Programming Interface):应用程序编程接口
Java API:JDK中提供给我们使用的类,这些类将底层的代码实现封装了起来,我们不需要关心这些类是如何实现的,只要会使用即可。
Java API在线文档:JDK11API菜鸟教程中文版 JDK19API官方英文版
6.1 java.lang.Math
Math类是帮助我们进行数学计算的工具类,里面的方法都是静态的,直接用Math.方法名()
调用。
返回类型 方法名(参数)
(1)求绝对值 int abs(int a) ;
(2)向上取整 double ceil(double a); //向正无穷大方向获取距离最近的整数
(3)向下取整 double floor(double a); //向负无穷大方向获取距离最近的整数
(4)四舍五入 int round(float a);
(5)较大值 int max(int a, int b);
(6)较小值 int min(int a, int b);
(7)a的b次幂 double pow(double a, double b);
(8)a的平方根 double sqrt(double a);
(9)a的立方根 double cbrt(double a);
(10)随机值 double random(); //[0.0,1.0)
6.2 java.lang.System
System类也是一个工具类,提供了一些与系统相关的方法。
返回类型 方法名(参数)
(1)终止JVM void exit(int status); //status=0JVM正常停止;status非0JVM非正常停止
(2)系统时间毫秒 long currentTimeMillis();//时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数.
(3)数组拷贝 void arraycopy(数据源数组, 起始索引, 目的地数组, 起始索引, 拷贝个数); //基本数据类型数组类型必须一致;引用数据类型子类类型可以赋值给父类类型
6.3 java.lang.Runtime
Runtime表示当前虚拟机的运行环境。应用程序无法创建自己的此类实例,可以从getRuntime方法获得当前运行时实例对象,实例对象允许应用程序与运行应用程序的环境进行交互。
返回类型 方法名(参数)
(1)当前Runtime对象 Runtime getRuntime();
(2)停止JVM void exit(int status); //status=0JVM正常停止;status非0JVM非正常停止
(3)获取CPU线程数 int availableProcessors();
(4)JVM能获得内存大小 long maxMemory(); //byte为单位,JVM尝试使用的最大内存量
(5)JVM已获得内存大小 long totalMemory(); //JVM虚拟机中的内存总量
(6)JVM可用内存大小 long freeMemory();
(7)运行cmd命令 Process exec(String command);
6.4 java.lang.Object
Object类是Java中的顶级父类,所有的类都直接或间接的继承于Object类,Object类中方法可以被所有子类访问。
返回类型 方法名(参数)
(1)返回对象的字符串表现形式 String toString(); //在打印对象想要看到属性值,重写toString方法
(2)比较对象是否相等 boolean equals(Object obj); //比较的是两个对象的地址值,可重写equals方法比较属性值
(3)对象克隆(protected修饰) Object clone(); //把A对象的属性值完全拷贝给B对象。重写clone方法,javabean类实现Cloneable接口,最后克隆并强转
(4) 返回对象的运行时类 Class<?> getClass();
(5) 返回对象的哈希码值 int hashCode(); //默认使用地址值计算哈希值,一般会重写用属性值计算
(6) 唤醒此对象监视器上等待的单个线程 void notify();
(7) 唤醒等待此对象监视器的所有线程 void notifyAll();
(8) 使当前线程等待直到被唤醒 void wait(); //被notify或notifyAll唤醒
(9) 使当前线程等待直到被唤醒 void wait(long timeoutMillis);//被唤醒或到时间
(10)使当前线程等待直到被唤醒 void wait(long timeoutMillis, int nanos);//被唤醒或到时间
- 重写equals方法
(1)先判断this==o是否相等,就是先判断两者地址值是否相等即是否指向同一对象,若是则直接返回true;
(2)再判断o是否为空,或this和o是否属于同一类别,若o为空或不是同一类别,则直接返回false;
(3)最后将o强转为该类对象(强转是因为o可能是父类引用子类对象,因此要向下转型),再对每个属性进行比较是否相等。 - 重写hashCode方法
public int hashCode() { return Objects.hash(属性1, 属性2…); }
浅克隆:不管对象内部的属性是基本数据类型还是引用数据类型,都完全拷贝过来。Object类的clone方法是浅拷贝(拷贝的引用数据类型的地址值)
深克隆:基本数据类型拷贝,字符串在串池复用,引用数据类型会重新创建新的。可以重写clone方法,规定引用数据类型重新创建。
6.5 java.util.Objects
Objects类是一个工具类,用于操作对象或在操作前检查某些条件。包含的方法都是静态的,即可以Objects.方法名()
直接使用。
返回类型 方法名(参数)
(1)先做非空判断,再比较两个对象 boolean equals(Object a, Object b);
(2)判断对象是否为null boolean isNull(Object a); //若a为null则返回true
(3)判断对象是否不为null boolean nonNull(Object a); //若a不为null则返回true
6.6 java.math.BigInteger
BigInteger支持任意精度的整数,即该类型可以准确地表示任何大小的整数值而不会丢失任何信息。BigInteger对象一旦创建里面的数据不能发生改变,只要进行计算都会产生一个新的BigInteger对象,底层通过整型数组实现。
BigInteger构造方法
(1)public BigInteger(int numBits, Random rnd); //获取随机大整数,范围在[0~2^numBits-1]
(2)public BigInteger(String val); //将十进制字符串(必须是整数)表示形式转换为BigInteger
(3)public BigInteger(String val, int radix); //将radix进制字符串(必须与进制吻合)表示形式转换为BigInteger
BigInteger静态获取BigInteger对象方法
(4)public static BigInteger valueOf(long val); //返回一个值等于long的BigInteger对象。优化了-16~16已提前创建好BigInteger对象,多次获取不会重新创建新的
BigInteger常用方法
(5)加法:add() 减法:subtract() 乘法:multiply() 除法(商):divide() 较大值:max() 较小值:min() 次幂:pow()
(6)除法(商和余数) BigInteger[] divideAndRemainder(BigInteger val);
(7)比较是否相等(属性值,BigInteger重写了equals方法) boolean equals(Object x);
(8)转为int整数 int intValue(BigInteger val); //超出int范围则会出错,类似还有longValue()、doubleValue()
6.7 java.math.BigDecimal
BigDecimal用于小数的精确计算,可用来表示很大的小数。BigDecimal对象一旦创建里面的数据不能发生改变。底层通过byte数组实现,存储的是字符串每一个字符的ASCII对应的数值。
BigDecimal构造方法
(1)public BigDecimal(String val) //将字符串表示的数转换为BigDecimal对象
(2)public BigDecimal(double val) //将double类型转换为BigDecimal对象,结果可能非精确的,不建议使用
BigDecimal静态获取BigDecimal对象方法
(3)public static BigDecimal valueOf(double val) //该静态方法底层仍是通过(1)方法实现。优化了0~10之间的整数,不会重新new。
BigDecimal常用方法(BigInteger的方法都有)
(4)除法 public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) //scale:小数点保留的位数;RoundingMode:舍入模式,如RoundingMode.HALF_UP为四舍五入
6.8 正则表达式
正则表达式可以校验字符串是否满足规则(字符串.matches(正则表达式)
);[]
代表里面内容出现一次;|
代表或者;()
代表分组;^
代表取反;&&
代表交集;{}
代表具体次数;(?i)
表示忽略大小写(只需在想要忽略大小写的前面加上即可);
正则表达式在字符串方法中的使用
(1)判断字符串是否满足正则表达式的规则 public boolean matches(String regex)
(2)按照正则表达式的规则进行替换 public String replaceAll(String regex, String newStr) //底层是用Matcher的replaceAll方法
(3)按照正则表达式的规则切割字符串 public String[] split(String regex)
字符类(只匹配一个字符)
(1)[abc] 只能是a,b,或c
(2)[^abc] 除了a,b,c之外的任何字符
(3)[a-zA-Z] a到z A到z,包括边界
(4)[a-d[m-p]] a到d,或m到p
(5)[a-z&&[def] a-z和def的交集。为:d,e,f
(6)[a-z&&[^bc]] a-z和非bc的交集。(等同于[ad-z])
(7)[a-z&&[^m-p]] a到z和除了m到p的交集。(等同于[a-lq-z])
预定义字符(只匹配一个字符)(转移字符\将特殊字符转换为普通的字符,如\\d来实现\d功能)
(1). 任何字符 用\\.来代表只能是小数点(两次转义,第一次将后边的\转为普通字符'\',而在正则表达式中字符串"\."就表示将.转义)
(2)\d 一个数字:[0-9]
(3)\D 非数字:[^0-9]
(4)\s 一个空白字符:[ \t\n\xOB\f\r]
(5)\S 非空白字符:[^\s]
(6)\w [a-zA-Z_0-9]英文、数字、下划线
(7)\W [^\w]一个非单词字符
数量词
(1)X? X,一次或0次
(2)X* X,零次或多次
(3)X+ X,一次或多次
(4)X{n} X,正好n次
(5)X{n,} X,至少n次
(6)X{n,m} X,至少n但不超过m次
分组(从1开始,遇到一个左括号就是一组,第一个左括号是第一组,第二个左括号是第二组...)
捕获分组
(1)\\组号 代表匹配结果应该和第'组号'组的内容一摸一样(不是正则表达式一样,而是与第一组匹配的内容一样)
(2)$组号 可以在正则外部调用第'组号'组的内容,如str.replaceAll("(.)\\1+","$1")就实现了对字符串重复内容的删除
非捕获分组如(?=)、(?:)、(?!),这种括号不占用组号
正则表达式也可在文本中查找满足要求的内容(爬虫)。java.util.regex.Pattern类是编译正则表达式后创建一个匹配模式;java.util.regex.Matcher类是文本(模式)匹配器。
(1)获取正则表达式对象 Pattern p = Pattern.compile("正则表达式"); //compile方法是静态方法
(2)获取文本匹配器对象 Matcher m = p.matcher("文本");
(3)判断是否有匹配的数据 boolean b = m.find(); //若有满足规则的子串,则在底层记录子串的起始索引和结束索引+1
(4)获取匹配的子串 String s = m.group(); //按照上步记录的索引对文本进行截取进而获得子串,可以有参数,表示只要第几组()
可循环获取匹配的全部子串
while(m.find()){
String s = m.group();
}
(1)(?=) 表示匹配=号后边的正则表达式,但是不返回,只返回括号前边的内容。如java(?=8|11|17),对于java8来说返回的结果是java
(2)(?:) 表示匹配:号后边的正则表达式,并且返回。如java(?:8|11|17),对于java8来说返回的结果是java8
(3)(?!) 表示不匹配!后边的正则表达式。如java(?!8|11|17),对于java8来说则不符合匹配,对java7则返回java7
(4)贪婪爬取 java默认就是贪婪爬取,如abbbbbbb字符串对正则表达式ab+的结果就是abbbbbbb
(5)非贪婪爬取 +?或*?,如abbbbbbb字符串对正则表达式ab+?的结果就是ab
6.9 时间类
以前格林威治时间(GMT)偏差太大,目前世界标准时间(UTC)原子钟。System.currentTimeMillis();
获取当前系统时间的毫秒值。
- JDK7时间类
(1)java.util.Date
Date date = new Date(); //空参构造,当前系统的时间
Date date = new Date(long 毫秒值);
date.getTime(); //获取Date对象的毫秒值
date.setTime(); //设置Date对象的毫秒值
(2)java.text.SimpleDateFormat
SimpleDateFormat sdf = new SimpleDateFormat(); //空参构造,Java默认格式
SimpleDateFormat sdf = new SimpleDateFormat(String pattern); //使用指定格式,如yyyy年MM月dd日 HH:mm:ss
String str = sdf.format(Date date); //将Date对象格式化(日期对象->字符串)
Date date = sdf.parse(String str); //将字符串解析为日期对象(字符串->日期对象)
(3)java.util.Calendar 抽象类,不能直接创建对象
Calendar calendar = Calendar.getInstance(); //通过静态方法来创建对象,会把年、月、日、时间、星期等放到数组中,月份从0开始,星期从星期日开始
calendar.setTime(Date date); //通过日期对象来设置日历时间
Date date = calendar.getTime(); //获取日期对象
calendar.setTimeInMillis(long millis); //通过毫秒值来设置日历时间
long millis = calendar.getTimeInMillis(); //获取时间毫秒值
int n = calendar.get(int field); //获取日历的某个字段信息。1:年,2:月,3:一年的第几周,4:一个月的第几周,5:日。也可用Calendar的静态常量来表示
calendar.set(int field, int value); //更改日历某个字段信息的值。
calendar.add(int field, int amount); //增加或减少日历某个字段信息的值。
- JDK8时间类(时间日期对象都不可变,会产生新的时间日期对象,解决之前的多线程的数据安全问题)
(1)java.time.ZoneId 时区类
Set<String> zoneIds = ZoneId.getAvailableZoneIds(); //获取全部时区
ZoneId zoneId = ZoneId.systemDefault(); //获取系统默认时区
ZoneId zoneId = ZoneId.of(String zoneId); //获取指定的时区
(2)java.time.Instant 时间戳(标准时间UTC)
Instant instant = Instant.now(); //获取当前时间的Instant对象
Instant instant = Instant.ofXxxx(long time); //根据(秒/毫秒/纳秒)获取Instant对象,如ofEpochSecond是根据秒
ZonedDateTime zdt = instant.atZone(ZoneId zoneId); //指定时间戳的时区
boolean b = instant.isXxx(Instant instant); //对时间戳之间的关系进行判断,如isAfter, isBefore
Instant in = instant.minusXxx(long xxxToSubtract); //减少时间,返回的是新的Instant对象,如minusSeconds
Instant in = instant.plusXxx(long xxxToSubtract); //增加时间,返回的是新的Instant对象,如plusSeconds
(3)java.time.ZonedDateTime 具有时区的日期时间的不可变表示
now() ofXxx() minusXxx(时间) plusXxx(时间)
ZonedDateTime time = zonedDateTime.withXxx(时间) //修改时间,如withSecond(1)表示将秒设置为1
(4)java.time.format.DateTimeFormatter 时间的格式化与解析
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(String pattern); //根据格式创建对象
String str = sdf.format(时间对象); //将时间对象(Instant、ZonedDateTime)格式化为字符串
(5)java.time.LocalDate(年月日) java.time.LocalTime(时分秒) java.time.LocalDateTime(年月日时分秒)
now() of() isBefore() isAfter() getXxx() withXxx(时间) minusXxx(时间) plusXxx(时间)
localDateTime.toLocalDate() localDateTime.toLocalTime() //可以将类型强制转换,只能这个转那俩范围小的
localDate.isLeapYear(); //可用来判断是否为闰年
(6)java.time.Period(年月日) java.time.Duration(秒、纳秒) java.time.temporal.ChronoUnit(计算两个日期的时间间隔,所有单位)
Period period = Period.between(时间1,时间2); //时间2-时间1的时间间隔
period.getXxx(); //获取具体的时间间隔,如getYears
period.toTotalMonths(); //两个时间差的总月数
ChronoUnit.YEARS.between(时间1,时间2); //可计算所有类型的时间间隔
6.10 java.util.Arrays
该类包含用于操作数组的各种方法(例如排序和搜索)。 此类还包含一个静态工厂,允许将数组视为列表。几乎全是静态方法。
返回类型 方法名(参数)
(1) 把数组拼接成字符串 String toString(数组);
(2) 二分查找查找元素 int binarySearch(数组, 元素);//返回结果为(负的插入点-1)
(3) 拷贝数组 int[] copyOf(原数组, 新数组长度); //不限为整型数组
(4) 拷贝数组(指定范围) int[] copyOfRange(原数组, 起始索引, 结束索引);
(5) 填充数组 void fill(数组, 填充元素); //也可指定一段范围的填充fill(数组, 起始索引, 结束索引, 填充元素);
(6) 数组排序(升序) void sort(数组); //数据<7:插入排序;数据>7时先分治递归调用插入排序,最后合并排序。
(7) 按某种规则排序数组 void sort(数组, 排序规则); //若没有指定排序规则,则调用(6),重写的compare方法:o1-o2升序,o2-o1降序
(8) 将数组转换成List List<T> Arrays.asList(T...a);
6.11 java.util.Collections
Collections是集合的工具类,将集合转换成数组:集合对象.toArray();
和集合对象.toArray(T[] a)
,空参会返回Object类型数组,有参会返回T[]类型数组(且底层会比较传入的数组长度和集合长度大小,若数组长度大,则直接复制到数组中;若集合长度大,则会重新创建一个新的数组复制并返回)。
(1) 批量添加单列集合List和Set元素 boolean addAll(Collection<T> c, T...elements); //elements是可变形参
(2) 打乱List集合顺序 void shuffle(List<?> list);
(3) List集合排序 void sort(List<T> list);
(4) List集合按指定规则进行排序 void sort(List<T> list, Comparator<T> c);
(5) List集合二分查找元素 void binarySearch(List<T> list, T key);
(6) List集合拷贝 void copy(List<T> dest, List<T> src);
(7) List集合填充 void fill(List<T> list, T obj);
(8) 求单列集合最大值 void max(Collection<T> coll); //根据默认的自然顺序进行依次比较,T需要有比较方法
(9) List集合交换元素 void swap(List<?> list, int i, int j);
6.12 java.util.stream.Collectors
该工具类用于将Stream流收集起来,即stream流对象.collect(Collectors collector);
,Collectors提供一些用不同方法收集流的收集器。
(1) 求流的平均数 Double Collectors.averagingInt(ToIntFunction<? super T> mapper); //double和long类似
(2) 求流中元素的个数 Long Collectors.counting();
(3) 求流中最大值 Optional<T> Collectors.maxBy(Comparator<? super T> comparator); //传入比较器
(4) 求流中元素的总和 IntSummaryStatistics Collectors.summingInt(ToIntFunction<? super T> mapper); //返回一个统计数据
(5) 指定分隔符 String Collectors.joining(CharSequence delimiter); //同时可以指定前后缀
(6) 按某条件分组 Map<K,List<T>> Collectors.groupingBy(Function<? super T,? extends K> classifier);
同时该分组还可以有第二个参数,即对第一次分组后的数据再进行收集器的进一步处理
(7) 对第一个参数进行某种收集器处理 Collectors.mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream)
7. Java字符串
7.1 String
- java.lang.String类代表字符串(java.lang包是核心包,因此在使用时不需要导包),字符串的内容是不会发生改变的,它的对象在创建后不能更改(String底层是通过char字符数组实现的,由于字符数组是private私有的,且对外没有提供修改字符数组的方法,因此String类型是不可变)。java程序中的所有字符串文字(如"superm`超")都被视为String类对象。由于String类重写了toString()方法在打印的时候是属性值而不是地址值。同时String类重写了equals()方法,比较的是字符串的属性值,前提是跟字符串比较的对象仍是字符串,否则直接返回false。
- 创建String对象的方式(直接赋值:字符串存储在堆内存中的StringTable串池(字符串常量池)中,字符串不存在则创建新的,存在则复用;new:在堆中新开辟空间,通过键盘录入的字符串也是new创建的)(字符串用+拼接后就变成了另一个字符串对象)
(1) 直接赋值 String str = "superm`超";
(2) 空白构造 String str = new String();
(3) 字符串构造 String str = new String(字符串对象);
(4) 字符数组构造 String str = new String(字符数组);
(5) 字节数组构造 String str = new String(字节数组);
- 字符串的比较
(1) 完全相等 boolean result = str1.equals(str2);
(2) 忽略大小写 boolean result = str1.equalsIgnoreCase(str2);
(3) 比较大小 int result = str1.compareTo(str2); //str1<str2为-1,值相等为0,str1>str2为1
- 字符串的方法
(1) 字符串的长度 int length = str.length();
(2) 索引返回字符 char c = str.charAt(索引值);
(3) 截取字符串 String sub = str.substring(起始索引, 结束索引); //左闭右开
(4) 截取字符串 String sub = str.substring(起始索引); //从起始索引截止到末尾
(5) 值的替换 String newStr = str.replace(旧值,新值);
(6) 转为字符数组 char[] arr = str.toCharArray(); //若要修改字符串内容,可转为字符数组,也可截取字符串
(7) 字符串开头 boolean flag = str.startsWith("字符串"); //判断字符串是否以某字符串开头,若是则返回true
(8) 按值分割 String[] strArr = str.split("分割的值");
(9) 转为大写 String str1 = str.toUpperCase();
(10) 去除空格 String str1 = str.trim(); //去除字符串前边和后边的空格
- 字符串拼接(+)的底层原理
(1)拼接时没有变量,全是字符串:触发字符串的优化机制,在编译成字节码class文件时已经是拼接后的结果,若串池中存在则复用。
(2)拼接时有变量(JDK8之前):先new一个StringBuilder对象再调用append方法进行拼接,之后再调用toString()方法转换为String对象,一个+号,在堆中创建两个对象。
(3)拼接时有变量(JDK8及之后):先预估拼接后的字符串总长度(预估同样花费时间),把拼接的内容放在数组,即产生新的String对象。
注:如果有很多字符串变量拼接,不要直接+。在底层会创建多个对象,浪费时间,浪费性能。
7.2 StringBuilder
- StringBuilder可以看作是一个容器,创建之后里面的内容是可变的,可提高字符串的操作效率(字符串的拼接与反转)。
- 构造方法:(1)
StringBuilder sb = new StringBuilder();
(2)StringBuilder sb = new StringBuilder(“字符串”);
- StringBuilder常用方法
(1) 求长度 int length = sb.length();
(2) 转换为String String str = sb.toString();
(3) 反转容器内容 sb.reverse(); //对容器本身内容进行修改
(4) 末尾添加数据 sb.append(); //添加的数据可以是任意类型的字符串形式
(5) 获取sb容量大小 int capacity = sb.capacity();
(6) 在指定位置插入 sb.insert(int offset, char c)
注:StringBuilder拼接时都放在容器里,不会创建很多无用的空间,节约内存。StringBuilder默认创建一个长度为16的字节数组,若超出则扩容为原来的
容量*2+2;若扩容之后还不够就以实际长度为准。除非超过int范围21亿多就会报长度超出限制的错误。
7.3 StringBuffer
StringBuffer与StringBuilder的方法是完全一样的,StringBuffer方法是用synchronized修饰线程同步的,因此是线程安全的,而StringBuilder用于多个线程是不安全的。
7.4 StringJoiner
- StringJoiner可进一步简化StringBuilder的拼接操作,JDK8之后出现,需要导包
import java.util.StringJoiner;
- 构造方法:(1)
StringJoiner sj = new StringJoiner(间隔符号);
(2)StringJoiner sj = new StringJoiner(间隔符号, 开始符号, 结束符号);
- StringJoiner常用方法
(1) 添加数据 sj.add(); //只能添加字符串,返回对象本身
(2) 求长度 int length = sj.length();
(3) 返回字符串 String str = sj.toString(); //该返回的字符串是拼接之后的结果
8. Java集合
集合分为单列集合和双列集合(键值对)。
8.1 Collection
Collection是单列集合的祖宗接口,它的功能是全部单列集合都可以继承使用的。Interface Collection< E >。
(1) 添加对象到集合中 boolean add(E e); //添加到末尾
(2) 清空集合 void clear();
(3) 删除集合中对象 boolean remove(E e);
(4) 判断集合是否包含某对象 boolean contains(Object obj); //底层是通过equals方法来判断,故对于引用型最好重写该方法
(5) 集合是否为空 boolean isEmpty(); //底层是判断size是否为0
(6) 集合的长度 int size();
Collection的遍历方式:
- 迭代器遍历:不依赖索引,而是通过创建指针和移动指针来获取集合中每一个元素。
(1) 获取迭代器对象 Iterator<E> iterator(); //调用对象是集合,返回一个迭代器对象,默认指向当前集合的0索引
(2) 判断当前位置是否有元素 boolean hasNext(); //有元素返回true,否则返回false
(3) 获取当前位置元素并后移 E next(); //和上边的调用者都是迭代器对象
注:若hasNext()为false还调用next()会报NoSuchElementException;
迭代器遍历完毕,指针不会复位;
next()方法用一次指针就后移了,所以之前的元素就无法再次获取了;
迭代器遍历时,不能用集合的方法进行增加或删除,会出现并发异常,可通过当前迭代器的remove()方法对集合元素进行删除。
- 增强for遍历:它的底层就是迭代器实现的,可用于单列集合和数组的遍历。修改增强for循环中的第三方变量,不会改变原来的数据。
- forEach遍历:
集合.forEach(Lambda表达式)
,该Lambda表达式是实现Consumer函数式接口的accept方法,该forEach遍历底层是增强for实现的。
8.2 List
List是继承了Collection接口的接口,有序、有索引、可重复,继承了Collection接口的方法,还定义了一些有关索引操作的方法。
(1) 在集合指定位置中插入指定元素 void add(int index, E element);
(2) 删除指定索引元素并返回 E remove(int index);
(3) 修改指定索引元素并返回原来的 E set(int index, E element);
(4) 返回指定索引处的元素 E get(int index);
List的遍历在Collection的3种遍历基础上又增加了两种遍历方式:
- 列表迭代器遍历:
ListIterator<E> li = list实现类对象.listIterator();
,在列表迭代器中可以指定初始游标的位置index,同时该迭代器增加了add()方法来向集合中添加数据。 - 普通for循环遍历:由于List是有索引的,可配合size()和get()方法进行遍历。
List系列集合迭代器的底层关系:
- 首先是Collection接口中定义了iterator()抽象方法,而List接口继承了iterator(),同时由于List是有索引的,因此List接口又添加了listIterator()和listIterator(int index)两个抽象方法。
- 然后在继承List接口的抽象类AbstractList重写了iterator()方法,通过一个
实现Iterator<E>泛型接口的内部类Itr
来进行迭代遍历(定义了三个变量:游标cursor=0,上次调用时的位置lastRet=-1,和一个用于记录创建内部类时modCount值(当集合执行某操作如add都会自加1的变量)的变量expectedModCount=modCount 。主要有三个方法:(1)hasNext():通过判断cursor指针是否等于集合size来确定当前游标是否已经到末尾;(2)next():先检查expectedModCount是否等于modCount,若不相等(在执行next方法时,若调用集合的一些操作如add就会更改modCount的值,但是expectedModCount还是初始化记录的值)就会抛出并发修改异常,再通过get(cursor)获取当前游标指向的数据并返回,返回之前要把lastRet置为cursor的值,并把游标加1后移;(3)remove():本质上仍调用了集合的remove(lastRet)方法将上次元素删除,同时更改expectedModCount为当前的modCount来避免并发修改异常;)。同时重写了listIterator()方法和listIterator(int index)方法,listIterator()方法中调用了listIterator(0),两者都是通过一个继承内部类Itr并实现ListIterator<E>泛型接口的内部类ListItr
来实现的,该ListItr内部类比Itr多了hasPrevious()、previous()、add()和set()等方法,这些方法仍是通过对cursor配合get、add和set等方法来进行操作的。 - 继承了AbstractList的ArrayList类同样重写了两个内部类和迭代器方法,大体与AbstractList保持一致,不过在next()和previous()方法中不是用get方法来获取当前游标的元素,而是直接用Object[] elementData = ArrayList.this.elementData;来获取了集合全部的数据,
- AbstractSequentialList抽象类继承了AbstractList抽象类,并重写了iterator()方法,而该迭代器方法是用了继承的listIterator(),而在AbstractSequentialList类中又抽象化了listIterator()方法,因此继承了AbstractSequentialList的LinkedList类的迭代器和列表迭代器实际上都是用该类中重写的listIterator(int index)方法实现的,同样是定义了一个实现ListIterator< E >泛型接口的内部类ListItr,同样有类似于游标的变量nextIndex,但是元素的获取则是通过类似于双链表的结点来获取的,通过next来获取下一个结点,通过prev来获取前一个结点。
8.3 ArrayList
ArrayList是List接口的实现类,因此List接口中的包括其继承的Collection接口的全部方法都有实现。
- 初始化方法:
ArrayList<E> arrayList = new ArrayList<>();
E:泛型数据类型,只能是引用数据类型(包括包装类) - ArrayList类底层是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。
- ArrayList底层扩容原理:
(1)利用空参构造创建ArrayList集合时,会在底层创建一个默认长度为0的数组elementData;
(2)添加第一个元素时,底层会创建一个新的长度为10的数组;
(3)数组存满时,再添加数据会将容量扩大为原来的1.5倍;
(4)如果一次添加多个元素,扩为1.5倍还装不下,则新创建的数组长度以实际长度为准(原来长度+一次性添加长度),如addAll()方法。
8.4 LinkedList
LinkedList也是List接口的实现类,底层数据结构是双链表,查询慢,首尾操作的速度是极快的,所以多了很多首尾操作的特有API。
(1) 在列表开头插入指定元素 void addFirst(E e);
(2) 在列表尾部追加元素 void addLast(E e);
(3) 返回列表中第一个元素 E getFirst();
(4) 返回列表中最后一个元素 E getLast();
(5) 删除并返回列表中第一个元素 E removeFirst();
(6) 删除并返回列表中最后一个元素 E removeLast(); //这写方法都可用其它方法实现,如remove(size()-1);
LinkedList底层add原理:带头尾指针的双向链表。
LinkedList底层get原理:根据index值判断是从头指针向后依次查找还是从尾指针向前依次查找。
8.5 HashSet
HashSet:无序、不重复、无索引,底层是用哈希表来存储数据,哈希表是一种对增删改查数据性能都较好的结构。
哈希表在JDK8之前是用数组+链表实现,JDK8之后用数组+链表+红黑树来实现。
- 哈希值:对象的整数表现形式。
(1)根据hashCode()
方法算出来的int类型的整数。
(2)该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算。
(3)一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值。 - 对象的哈希值特点
(1)如果没有重写hashCode()方法,不同对象计算出的哈希值是不同的,每个对象的地址都不同。
(2)如果已经重写hashcode()方法,不同的对象只要属性值相同,计算出的哈希值就是一样的。
(3)在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。(哈希碰撞) - HashSet添加数据底层原理(底层是由HashMap的put方法实现)
(1)创建一个默认长度16,默认加载因子为0.75的数组(当大于等于0.75时自动扩容为原来的2倍),数组名table。
(2)根据元素的哈希值跟数组的长度计算出应存入的位置,公式为int index = (数组长度 - 1) & 哈希值;
(3)判断当前位置是否为null,如果是null直接存入。
(4)如果位置不为null,表示有元素,则调用equals方法比较属性值,若一样,则不存,若不一样,则存入数组,形成链表。
JDK8之前:新元素存入数组,老元素挂在新元素下面,即头插法添加数据。
JDK8之后:新元素直接挂在老元素下面,即尾插法添加数据。
(5)JDK8以后,当链表长度超过8,而且数组长度大于等于64时,自动转换为红黑树。
(6)如果集合中存储的是自定义对象,必须要重写hashCode(为了用属性值来计算哈希值,而不是地址值)和equals方法(为了用属性值进行比较,而不是地址值)。
8.6 LinkedHashSet
LinkedHashSet:有序、不重复、无索引,它是HashSet的子类,有序是指可保证存储和取出的元素顺序一致。LinkedHashSet底层数据结构依然是哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序。
8.7 TreeSet
TreeSet:不重复、无索引、可排序(按照元素的默认规则排序,从小到大)。TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好。
TreeSet两种比较方式:
(1)默认排序/自然排序:Javabean类可以实现Comparable接口来指定比较规则,重写compareTo()方法,该方法中,this代表当前要添加的数据,o代表红黑树中已经存在的数据,返回值:若为正数,代表添加的数据较大;若为负数,代表添加的数据较小;若为0,代表当前要添加的数据已经存在,则舍弃。
(2)比较器排序:创建TreeSet对象时候,传递比较器Comparator指定规则,o1代表当前要添加的数据,o2代表红黑树存在的数据。若两者都存在,则以比较器排序为准。
TreeSet<Integer> set = new TreeSet<>(new Comparator<Integer>() { //Integer只是这里泛型的一种,其它任何引用数据类型都可以
@Override
public int compare(Integer o1, Integer o2) { //Comparator是函数式接口,可用Lambda表达式来表示
return o1-o2;
}
});
单列集合的选则:
8.8 Map
![](https://i-blog.csdnimg.cn/blog_migrate/1d7dbb4bf6b06955ccf4ed7172737c74.png#pic_center)
Map是双列集合的祖宗接口,数据都是按键值对(Entry
)成对的。
(1) 添加元素 V put(K key, V value); //添加时如果键不存在,返回null;若键存在,则覆盖之前的value并返回
(2) 根据键删除元素 V remove(Object key);
(3) 清空数据 void clear();
(4) 判断集合是否包含指定的键 boolean containsKey(Object key);
(5) 判断集合是否包含指定的值 boolean containsValue(Object value);
(6) 根据键找对应的值 V get(Object key);
(7) 返回全部键组成的单列Set Set<K> keySet();
(8) 返回Entry对象的单列Set Set<Map.Entry<K, V>> entrySet(); //Entry是Map接口的一个内部接口,有getKey和getValue方法
(9) 判断集合是否为空 boolean isEmpty();
(10) 求集合长度,即键值对个数 int size();
Map集合的3种遍历方式:
- 键找值:由keySet()方法得到全部的键的Set单列集合,再遍历Set同时根据get()方法获取对应键的值。
- 键值对:通过
Set<Map.Entry<K,V>> entries = map.entrySet();
获得一个键值对单列集合,再遍历单列集合得到Entry对象。 - forEach:通过
map.forEach(Lambda表达式);
,该Lambda表达式实现了BiConsumer函数式接口的action方法,BiConsumer与Consumer区别在于有两个泛型变量,该forEach遍历底层是用增强for循环配合键值对遍历实现的。
8.9 HashMap
HashMap是Map接口的一个实现类,特点都是由键决定的:无序、不重复、无索引,HashMap跟HashSet底层原理是一模一样的,都是哈希表结构,put方法会先创建一个Entry对象,再根据Entry对象的键计算出对应的哈希值,再进行存储,若键一样则会覆盖,同样依赖hashCode()和equals()方法来保证键的唯一,因此对于自定义的键对象,需要重写这两个方法以便保证用的是属性值而不是地址值。
HashMap底层原理:数组+链表+红黑树
- HashMap的成员
(1)Node<K,V>[] table 哈希表数组的名字
(2)DEFAULT_INITIAL_CAPACITY 数组默认长度16
(3)DEFAULT_LOAD_FACTOR 默认加载因子
(4)内部类Node<K,V>作为哈希表链表结点实现了Map.Entry<K,V>接口,有4个量:int hash(键的哈希值);key(final修饰的键);value(值);next(下一个结点的地址值)。
(5)内部类TreeNode<K,V>作为哈希表红黑树结点继承了LinkedHashMap.Entry<K,V>,而LinkedHashMap.Entry<K,V>又继承了HashMap.Node<K,V>,因此它即可作为普通的链表结点也可作为红黑树结点,比Node<K,V>结点多了5个量:parent(父指针);left(左孩子);right(右孩子);prev(用于删除);boolean red(结点的颜色)。 - 添加元素
(1)空参构造初始化HashMap对象:仅设置了加载因子为0.75,此时table仍为null。
(2)put(key,value)方法又调用putVal(hash(key), key, value, false, true)方法,hash(key)是用键计算了对应的哈希值,第四个参数false代表若键重复则会进行覆盖。
(3)putVal方法:定义了一个Node<K,V>数组tab=table(由于table是在堆空间,而tab在栈空间,更方便处理数据),定义了整型n记录数组长度,定义了整型i代表索引。
第一步:判空:判断tab是否为空或长度是否为0,若是则调用resize()方法(若是当前数组为空,则会创建一个默认长度为16,加载因子为0.75的数组;若当前数组不为空,则会判断是否达到扩容条件,若达到扩容条件,则数组扩容为原来的2倍,数据移到新的哈希表中)。
第二步:添加:根据i=(n-1)&hash;计算出键的索引,并从tab中取出对应索引的结点赋值给临时变量结点p。
第一种情况:p为空,则证明当前要添加的Entry没有相同的键,则直接根据hash、key和value新建一个Node结点并赋值给tab[i];
第二种情况:p不空且键不重复,则挂在数组对应位置下面形成链表(链表尾部,添加之后会判断链表长度是否大于8,若是,则会调用treeifyBin方法,该方法底层也会判读数组长度是否大于64,只有两个都成立才会将链表转换成红黑树)或红黑树(通过instance判断p是否是TreeNode);
第三种情况:p为空且键重复,则会用新的值覆盖原本的值。
第三步:判扩容:添加成功后,size加1后判断是否大于threshold(数组长度*0.75,即扩容时机),若大于,则调用resize()方法进行扩容。
8.10 LinkedHashMap
LinkedHashMap是HashMap的子类,特点都是由键决定的:有序、不重复、无索引,类似于LinkedHashSet都是多了一个双向链表来记录存储的顺序。
8.11 TreeMap
TreeMap和TreeSet一样底层是由红黑树实现的,特点由键决定:不重复、无索引、可排序(对键排序)。
TreeMap底层原理:红黑树
- TreeMap的成员
(1)内部类Entry<K,V>实现了接口Map.Entry<K,V>,代表红黑树结点,有6个成员变量:K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK;
(2)成员Comparator<? super K> comparator;比较器对象表示键的比较规则。
(3)成员Entry<K,V> root;表示根结点。
(4)成员int size = 0;表示结点的个数,即集合的长度。
(5)无参构造将comparator赋值为null;有参构造将comparator赋值为传入的比较规则。 - 添加元素
(1)先判断根节点是否为空,若为空则调用addEntryToEmptyMap方法,创建一个Entry对象并作为根节点。
(2)再判断比较器是否为空,若为空,则按照默认比较规则进行比较,开始会将key强转为Comparable<? super K>比较器对象,因此对于自定义的类如果采用空参构造要实现Comparable接口,否则会报错;若比较器不空,则按照比较器的规则进行比较。两者都是要找到插入红黑树的父结点,若此时有键重复了,则对值进行覆盖并返回;若键不重复,则找到其父结点,调用addEntry方法插入红黑树中。
(3)最后addEntry方法将结点插入红黑树中,再调用fixAfterInsertion方法根据红黑树规则对红黑树进行调整。
注意:
(1)TreeMap添加元素时,不需要键重写hashCode()和equals()方法,只需有键的比较规则即可,HashMap需要。
(2)HashMap有红黑树,但是不需要对键实现Comparable接口或传递比较器对象,而是默认用哈希值的大小创建红黑树。
(3)HashMap效率比TreeMap效率更高一些,但对一些最坏情况,比如HashMap的元素都在一个链表中,TreeMap效率更高。
(4)Map集合中,如果键重复了,put方法会对值覆盖,putIfAbsent则不会覆盖。
(5)3种Map的选取,默认: HashMap(效率最高);如果要保证存取有序: LinkedHashMap;如果要进行排序:TreeMap。
8.12 Properties
Properties父类Hashtable(与HashMap的区别:Hashtable中大多数方法由synchronized修饰,是线程安全的,所以效率较低;Hashtable的key和value都不能为null,而HashMap的key最多可以1个为null,value可以多个为null;Hashtable初始容量为11,扩容为2n+1,而HashMap初始容量为16,扩容为2n;Hashtable的父类为Dictionary,而HashMap父类为AbstractMap;JDK1.8后HashMap底层比Hashtable底层的数组+链表多了红黑树,即当链表超过8且数组长度大于64就转换为红黑树),Properties类表示一组持久的属性, Properties可以保存到流中或从流中加载,属性列表中的每个键及其对应的值都是一个字符串。直接Properties properties = new Properties();
创建即可,不用泛型,它可以是任何类型,但最好用String类型。
(1) 通过InputStream基本流读取属性列表(键值对) void load(InputStream inStream);
(2) 通过Reader基本流读取属性列表(键值对) void load(Reader reader);
(3) 通过OutputStream基本流写出属性列表(键值对) void store(OutputStream out, String comments); //comments为注释信息
(4) 通过Writer基本流写出属性列表(键值对) void store(Writer writer, String comments);
8.13 不可变集合
如果不想让别人修改集合中的内容,就可以定义不可变集合,这种集合只能查询,不能增删改。
(1) 创建一个具有指定元素的List集合对象 List<E> List.of(E...elements);
(2) 创建一个具有指定元素的Set集合对象 Set<E> Set.of(E...elements); //参数一定要保证唯一性
(3) 创建一个具有指定元素的Map集合对象 Map<K,V> Map.of(E...elements); //两个元素组成一个键值对,键不能重复,最多有10个键值对
(4) 创建一个具有指定元素的Map集合对象 Map<K,V> Map.ofEntries(Map.Entry<? extends K,? extends V>... entries);
如Map.ofEntries(map对象.entrySet().toArray(new Map.Entry[0]));来获取一个不可变map对象。
或直接用 不可变map对象 = Map.copyOf(可变map对象); //这个传的无论是可变还是不可变,返回就是不可变
8.14 Stream流
Stream流是JDK8新特性,是用于操作数据源(集合、数组等)所生成的容器。java.util.Stream与java.io是完全不同的东西。
Stream流使用步骤:
- 先得到一条Stream流(流水线),并把数据放上去。
(1) 单列集合 default Stream<E> stream(); //Collection中的默认方法
(2) 双列集合 无法直接使用Stream流,可以通过keySet()或entrySet()转换为单列集合再使用Stream流
(3) 数组(Arrays) public static <T> Stream<T> stream(T[] array); //Arrays工具类中的静态方法,Arrays.stream(数组名);
(4) 多个数据(Stream) public static <T> Stream<T> of(T...values); //Stream接口中的静态方法,Stream.of(values1...);
注:如果用Stream.of(数组名);来获取Stream流,数组必须是引用数据类型,若是基本数据类型则会将整个数组作为一个数据放入Stream流,而不是每个元素
- 利用Stream流中的API进行各种操作。
(1)中间方法:方法调用完毕之后,还可以调用其它方法。
(1) 过滤 Stream<T> filter(Predicate<? super T> predicate); //重写该函数式接口的test方法
(2) 获取前几个元素 Stream<T> limit(long maxSize);
(3) 跳过前几个元素 Stream<T> skip(long n);
(4) 元素去重,依赖(hashCode和equals方法) Stream<T> distinct(); //由于底层是通过HashSet来实现的,因此要有hashCode和equals方法
(5) 合并a和b两个流为一个流(静态方法) Stream<T> concat(Stream a, Stream b); //a和b类型一致,不一致则合并的流是它们父类类型
(6) 转换流中的数据类型 Stream<R> map(Function<T,R> mapper); //重写该函数式接口的apply方法(仅一个参数,即流元素)
(7) 流排序 Stream<T> sorted(); //按自然顺序排序,也可传入比较器对象指定比较规则
注意1:中间方法,会返回新的Stream流对象,原来的Stream流只能使用一次,因此一般使用链式编程。
注意2:修改Stream流中的数据,不会影响原来集合或数组中的数据。
(2)终结方法:最后一步,调用完毕之后,不能调用其它方法。
(1) 遍历流 forEach(Consumer<? extends T> action); //重写该函数式接口中的accept方法(仅一个参数)
(2) 统计 count(); //返回值long类型
(3) 最大最小值 max(); //返回值Optional<T>类型
(4) 收集流中的数据,放到数组中 toArray(); //空参返回的是Object[]数组
toArray(IntFunction<A[]> generator);//返回A[]数组,重写该函数式接口的apply(int value)方法,vaule代表流中元素的个数,直接返回new A[value]即可
(5) 收集流中的数据,放到集合中 collect(Collector collector)
1. 放入List集合 Collectors.toList();
2. 放入Set集合 Collectors.toSet();
3. 放入Map集合 Collectors.toMap(键的生成规则,值的生成规则); //各自重写函数式接口的apply方法(仅一个参数,即流元素),键不能重复
9. Java异常
所有的异常类是从 java.lang.Exception 类继承的子类,异常分为编译时异常和运行时异常,错误不是异常。
9.1 异常的分类
- 编译时异常:编译阶段出现的异常(javac),必须手动处理,否则代码报错,作用在于提醒程序员,运行时异常之外都是编译时异常。
IOException:输入输出流异常 ;FileNotFoundException:文件找不到的异常 ;ClassNotFoundException:类找不到异常 ;DataFormatException:数据格式化异常;NoSuchFieldException:没有匹配的属性异常;NoSuchMethodException:没有匹配的方法异常; SQLException:数据库操作异常;TimeoutException:执行超时异常。 - 运行时异常:代码运行时出现的异常(java),RunTimeException及其子类,编译阶段不需要处理,一般由参数传递错误引起的。
NullPointerException:空指针引用异常;ClassCastException:类型强制转换异常;IllegalArgumentException:传递非法参数异常;
ArithmeticException:算术运算异常;IndexOutOfBoundsException:下标越界异常;NegativeArraySizeException:创建一个大小为负数的数组错误异常;NumberFormatException:数字格式异常。 - 错误(非异常) :程序中无法处理的错误,表示运行应用程序中出现了严重的错误。这些错误是不可查的,非代码性错误。
9.2 异常的作用
- 异常是用来查询bug的关键参考信息。
- 异常可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况(如抛出异常)。
9.3 异常的处理方式
- JVM默认的处理方式:把异常的名称,异常原因及异常出现的位置等信息输出在了控制台;程序停止执行,下面的代码不会再执行。
- 捕获异常(自己处理异常):当代码出现异常时,可以让程序继续往下执行。try中出现异常就会到对应catch中找(try中异常下边若还有代码则不会执行了),若有对应异常catch被捕获则执行对应catch中的代码,并执行try catch下的代码;若没有对应的异常则会按照JVM默认的处理方式处理。且对于多个异常,父类异常应写在最下面。
try {
可能出现异常的代码;
} catch (异常类名 变量名) { //可有多个catch捕获多重异常,可以用|同时捕获多个异常
异常的处理代码;
} finally { //可选,无论是否发生异常,finally代码块中的代码总会被执行。
程序代码;
}
- 抛出异常(交给调用者处理):
throws
:写在方法声明处,告诉调用者该方法可能会出现那些异常,运行时异常可省略不写;throw
:写在方法内部,手动抛出异常给调用者,下边的程序不会再执行,如抛出一个运行时异常throw new RunTimeException(“这是一个运行时异常”);,然后调用者再对异常进行捕获。
9.4 异常的常用方法
定义在java.lang.Throwable中的方法。printStackTrace()方法最为常用,信息最全。
(1) 返回异常的详细字符串描述 String getMessage();
(2) 返回异常的类型和描述 String toString();
(3) 把异常的错误信息输出在控制台 void printStackTrace(); //仅仅打印信息,不会停止程序运行
9.5 try-with-resources语法糖
JDK7之前所有被打开的系统资源,比如流、文件或者 Socket 连接等,都需要被开发者手动关闭,否则将会造成资源泄露。JDK7 之后,Java 新增的 try-with-resource 语法糖来打开资源,并且可以在语句执行完毕后确保每个资源都被自动关闭 。try 用于声明和实例化资源,catch 用于处理关闭资源时可能引发的所有异常。try中可以声明多个资源,方法是使用分号 ; 分隔各个资源,最后会自动以相反的顺序关闭这些资源。JDK9之后可以在try外边声明资源,在try括号中写上变量名即可。这些资源必须实现AutoCloseable接口。
try (resource declaration) {
// 使用的资源
} catch (ExceptionType e1) {
// 异常块
}
9.6 自定义异常
- 定义异常类
- 写继承关系:运行时异常继承RunTimeException,编译时异常继承Exception
- 空参构造
- 带参构造(String message)
10. IO流
10.1 File类(文件操作类)
File类是 java.io包中唯一代表磁盘文件本身的对象,也就是说,如果希望在程序中操作文件和目录,则都可以通过 File 类来完成。File 类定义了一些方法来操作文件,如新建、删除、重命名文件和目录等。File 类不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流。
- 构造方法
(1) 根据文件路径创建File对象 File(String pathName); //如果pathName是文件夹则代表目录,若是具体文件则代表文件
(2) 根据父路径名和子路径名创建File对象 File(String parent, String child);
(3) 根据父路径对应File对象和子路径名创建File对象 File(File parent, String child);
- 成员方法
(1) 判断File是否为文件夹 boolean isDirectory();
(2) 判读File是否为文件 boolean isFile();
(3) 判断File是否存在 boolean exists(); //存在返回true
(4) 求文件的大小(字节数量) long length(); //只能求文件的大小,不能求文件夹的大小(可以遍历文件夹,累加文件大小)
(5) 求文件的绝对路径 String getAbsolutePath();
(6) 求定义时使用的路径 String getPath(); //即构造函数时的路径
(7) 求文件的名称,带后缀 String getName(); //文件夹是没有后缀的
(8) 获取父路径File对象 File getParentFile();
(9) 求文件的最后修改时间(毫秒) long lastModified();
- 创建和删除
(1) 创建一个新的空的文件 boolean createNewFile(); //如果当前路径的File存在则返回false创建失败;若父级路径不存在,抛IO异常;若路径无后缀,则创建一个无后缀的文件
(2) 创建单级文件夹 boolean mkdir();
(3) 创建多级文件夹 boolean mkdirs();
(4) 删除文件、空文件夹(不走回收站) boolean delete(); //有内容的文件夹无法删除
- 获取并遍历
(1) 获取当前路径下所有内容 File[] listFiles(); //返回File数组
(2) 获取当前路径下所有内容 String[] list(); //返回路径字符串数组
(3) 过滤当前路径下所有内容 String[] list(FilenameFilter filter); //根据过滤器返回路径字符串数组
(4) 过滤当前路径下所有内容 File[] listFiles(FileFilter filter); //根据过滤器返回File数组,只有一个参数pathName
(5) 过滤当前路径下所有内容 File[] listFiles(FilenameFilter filter);//根据过滤器返回File数组,两个参数dir和name(父+子)
(6) 获取全部的盘符 File[] File.listRoots();
最重要的listFiles方法的注意点:
(1)当调用者File表示的路径不存在、是文件、需要权限才能访问的文件夹时,返回null。
(2)当调用者File表示的路径是一个空文件夹时,返回一个长度为0的数组。
(3)当调用者File表示的路径是一个有内容的文件夹时,将里面所有文件和文件夹(包含隐藏文件)的路径放在File数组中返回。
10.2 IO流
IO流用于读写文件中的数据(可以读写文件,或网络中的数据…)。流在关闭时先开的流最后关闭,随用随创建,什么时候不用了就关闭。
10.2.1 字节输入流(InputStream)
利用InputStream类的方法可以从输入流中读取一个或一批字节。
(1) 从输入流中读取一个8位字节 int read(); //将8位字节转换为0-255整数并返回,若到输入流末尾,则返回-1
(2) 从输入流中读取字节到字节数组 int read(byte[] b); //返回读取的字节数
(3) 从输入流中读取字节到字节数组指定位置 int read(byte[] b, int off, int len);
(4) 关闭输入流 void close(); //InputStream本身的close不执行任何操作,子类会重写
(5) 获取可以从输入流中读取的字节数 int available();
(6) 从输入流中跳过指定数目的字节 long skip(long n); //返回跳过的字节数
(7) 在输入流当前位置设置标记 void mark(int readLimit); //readLimit指定了最多被设置标记的字节数
(8) 判断输入流是否允许设置标记 boolean markSupported(); //允许则返回true
(9) 将输入流的指针返回到上次mark的位置 void reset();
10.2.1.1 字节文件输入流(FileInputStream)
FileInputStream可以从文件系统的某个文件中获取输入字节。在创建FileInputStream类的对象时,如果找不到指定的文件将拋出 FileNotFoundException异常,该异常必须捕获或声明拋出。
(1) 通过File对象创建 FileInputStream(File file);
(2) 通过路径名创建 FileInputStream(String name); //底层也是通过路径创建File对象实现的,若文件不存在则直接报错FileNotFoundException
10.2.1.2 字节缓冲输入流(BufferedInputStream)
BufferedInputStream底层自带8192字节大小的缓冲区来提高效率,需要把基本流包装成高级流,提高读取数据性能。基本流会在高级流的底层关闭,所以不需要手动去关闭。 缓冲区可以减少内存读取和写入硬盘的次数,而内存自身运算速度非常快,可先将数据尽量多的存入缓冲区,在内存内部进行处理。
(1) 通过InputStream基本流对象创建 BufferedInputStream(InputStream in); //默认缓冲区8192字节
(2) 通过InputStream基本流对象创建并指定缓冲区的大小 BufferedInputStream(InputStream in, int size);
10.2.1.3 反序列化流(ObjectInputStream)
反序列化流(ObjectInputStream)可以把序列化到本地文件中的对象,读取到程序中,又称为对象操作输入流。但是对于已经序列化的对象来说,如果更改了Javabean类中的成员变量等信息,在反序列化时就会因为和之前的类不同(可以理解为Javabean的版本不同)而出现错误,可以定义一个private static final serialVersionUID = 版本号L
变量来指定版本(IDEA可以设置自动生成版本号)。如果不想把某属性被序列化,则可用transient
来修饰。如果调用readObject读取对象的个数比序列化流写入的对象个数多,则会爆EOFException异常,因此若序列化多个同类对象,可以将它们保存在集合中。
(1) 通过InputStream基本流对象创建 ObjectInputStream(InputStream in);
(2) 读取序列化数据为对象 Object readObject(); //需要强制转换为指定的对象
10.2.1.4 解压流(ZipInputStream)
解压流(ZipInputStream)可以读取zip压缩文件中的文件夹和文件。
(1) 通过InputStream基本流创建 ZipInputStream(InputStream in);
(2) 通过InputStream基本流和指定字符集创建 ZipInputStream(InputStream in, Charset charset);
(3) 读取下一个ZIP文件条目ZipEntry ZipEntry getNextEntry(); //若没有了就返回null,ZipEntry有isDirectory()方法
(4) 关闭当前ZIP文件条目以读取下一个 void closeEntry();
10.2.2 字节输出流(OutputStream)
利用OutputStream类的方法可以向输出流中写入一个或一批字节。
(1) 向输出流中写入一个字节 void write(int b); //超过1个字节整数范围会乱码
(2) 向输出流中写入一个字节数组 void write(byte[] b);
(3) 向输出流中写入字节数组的一部分 void write(byte[] b, int off, int len);
(4) 关闭输出流(系统会释放于输出流相关资源) void close(); //OutputStream本身close()不执行任何操作,其子类会重写
(1)可以将数据存入String字符串中,再调用getBytes()方法将字符串转换为字节数组,最后写入输出流。
(2)换行符:Windows(\r\n
);Linux(\n
);Mac(\r
)。
10.2.2.1 字节文件输出流(FileOutputStream)
在创建FileOutputStream类的对象时,如果指定的文件不存在,则创建一个新文件;如果文件已存在,则清除原文件的内容重新写入。目标文件所在父级目录必须存在,否则会拋出FileNotFoundException 异常。且目标文件的名称不能是已存在的目录。
(1) 通过File对象创建 FileOutputStream(File file); //会清空文件,重新写入
(2) 通过File对象创建并指定是否追加 FileOutputStream(File file,boolean append); //append为true则会在文件末尾追加
(3) 通过路径名创建 FileOutputStream(String name);
(4) 通过路径名创建并指定是否追加 FileOutputStream(String name,boolean append);
10.2.2.2 字节缓冲输出流(BufferedOutputStream)
BufferedOutputStream底层自带8192字节大小的缓冲区来提高效率,需要把基本流包装成高级流,提高写出数据性能。
(1) 通过OutputStream基本流对象创建 BufferedOutputStream(OutputStream in); //默认缓冲区8192字节
(2) 通过OutputStream基本流对象创建并指定缓冲区的大小 BufferedOutputStream(OutputStream in, int size);
10.2.2.3 序列化流(ObjectOutputStream)
序列化流(ObjectOutputStream)可以把Java对象写到本地文件中,又称为对象操作输出流。使用序列化流将对象保存到文件时会出现NotSerializableException异常,因此需要让Javabean类实现Serializable接口
,Serializable接口没有抽象方法,即标记型接口,一旦实现这个接口就表示当前的Javabean类可以被序列化。序列化后的文件是不可以被修改的,否则就无法反序列化为原来的对象了。
(1) 通过OutputStream基本流创建对象 ObjectOutputStream(OutputStream out);
(2) 将对象序列化(写出)到文件中 void writeObject(Object obj);
10.2.2.4 压缩流(ZipOutputStream)
压缩流(ZipOutputStream)用于以zip文件格式写入文件。,
(1) 通过OutputStream基本流创建 ZipOutputStream(OutputStream in);
(2) 通过OutputStream基本流和指定字符集创建 ZipOutputStream(OutputStream in, Charset charset);
(3) 把ZipEntry对象写入压缩包 void putNextEntry(ZipEntry e);
//首先要创建一个ZipEntry对象new ZipEntry(String name),name是压缩包里的路径,再调用putNextEntry放入压缩包,
//此时无数据,因此还需要通过ZipOutputStream的写入方法将数据写入到ZipEntry,即写到压缩包对应的文件中
(4) 关闭当前ZIP文件条目以写入下一个 void closeEntry();
10.2.2.5 字节打印流(PrintStream)
打印流只能写出数据,特有的写出方法可以将数据原样写出,特有的写出方法可以实现自动刷新和自动换行(写出+换行+刷新),字节流底层没有缓冲区,所以开不开自动刷新都一样(构造方法中有autoFlush这一变量来确定)。System.out就是特殊的打印流,系统中的标准输出流,由虚拟机创建,默认指向控制台,且不能关闭(关闭就不能用了)。
(1) 通过OutputStream基本流对象/File对象/路径名创建 PrintStream(OutputStream/File/String);
(2) 通过路径名和字符集创建 PrintStream(String fileName, Charset charset); //还有其它构造方法
(3) 写出任意数据并自动刷新,自动换行 void println(Xxx xxx);
(4) 写出任意数据,不换行 void print(Xxx xxx);
(5) 写出带有占位符的语句 void printf(String format, Object... args);
10.2.3 字符输入流(Reader)
Reader类是所有字符流输入类的父类。Reader类同样包含close()、mark()、skip()和reset()等方法。字符流底层也是字节流,默认也是一个字节一个字节读取,遇到中文就会读取多个字节(GBK:2个字节;UTF-8:3个字节),读取之后方法底层会根据字符集进行解码并转换成十进制。
(1)创建字符输入流对象:关联文件,并创建缓冲区(长度为8192的字节数组)。
(2)读取数据:判断缓冲区是否有数据可以读取,若无,则从文件中获取数据到缓冲区,尽可能的装满缓冲区,文件也没数据了就返回-1;若有,则直接从缓冲区中读取。
(1) 从输入流中读取一个字符 int read(); //会把读取的字符转换成0-65535的整数,返回-1证明到输入流的末尾了
(2) 从输入流中读取若干字符到字符数组 int read(char[] cbuf); //返回读取的字符数
(3) 从输入流中读取若干字符到字符数组指定位置 int read(char[] cbuf, int off, int len); //返回实际读取的字符数
10.2.3.1 字符文件输入流(FileReader)
(1) 通过File对象创建 FileReader(File file);
(2) 通过File对象和指定字符集创建 FileReader(File file, Charset charset); //Charset.forName("GBK")
(3) 通过路径创建 FileReader(String fileName);
(4) 通过路径和指定字符集创建 FileReader(String fileName, Charset charset);
10.2.3.2 字符缓冲输入流(BufferedReader)
字符缓冲输入流BufferedReader特有方法:String readLine();
可以读取一整行数据,如果没有数据可以读了,则返回null,但是不会把回车换行读到内存中。底层会创建长度为8192的字符数组作为缓冲区,而FileReader自带的是8192大小的字节数组。
(1) 通过Reader基本流对象创建 BufferedReader(Reader in);
(2) 通过Reader基本流对象创建并指定缓冲区大小 BufferedReader(Reader in, int sz);
10.2.3.3 字符转换输入流(InputStreamReader)
转换流是字符流和字节流之间的桥梁,JDK11之后可用FileReader(File file, Charset charset);来指定字符集,但若字节流想要使用字符流特有的方法如BufferedReader的readLine()方法,仍可以通过字符转换输入流将字节流转换成字符流,再用BufferedReader高级输入流包装它转换成BufferedReader对象。
(1) 通过InputStream基本输入流创建默认字符集 InputStreamReader(InputStream in);
(2) 通过InputStream基本输入流创建指定字符集 InputStreamReader(InputStream in, String charsetName);
10.2.4 字符输出流(Writer)
Writer类是所有字符输出流的父类。Writer类也包含close()方法(若缓冲区有数据则会将缓冲区数据写入文件最后断开关联)。当缓冲区8192个字节数组满了会自动写入关联的文件中。
(1) 向输出流中写入一个字符 void write(int c); //会根据整数c查找对应的字符集对应的字符
(2) 把字符数组写入输出流 void write(char[] cbuf);
(3) 把字符数组某部分写入输出流 void write(char[] cbuf, int off, int len);
(4) 向输出流中写入一个字符串 void write(String str);
(5) 向输出流中写入字符串的一部分 void write(String str, int off, int len);
(6) 向输出流中追加一个字符 Writer append(char c);
(7) 向输出流中追加字符序列 Writer append(CharSequence csq);
(8) 向输出流中追加字符序列的一部分 Writer append(CharSequence csq, int start, int end);
(9) 将缓冲区中数据刷新到文件中 void flush(); //关联并没有断
10.2.4.1 字符文件输出流(FileWriter)
(1) 通过File对象创建 FileWriter(File file);
(2) 通过File对象和指定是否追加 FileWriter(File file, boolean append);
(3) 通过File对象和指定字符集 FileWriter(File file, Charset charset);
(4) 通过File对象、是否追加和字符集 FileWriter(File file, Charset charset, boolean append);
(5) 通过路径创建 FileWriter(String fileName);
(6) 通过路径和指定是否追加 FileWriter(String fileName, boolean append);
(7) 通过路径和指定字符集 FileWriter(String fileName, Charset charset);
(8) 通过路径、是否追加和字符集 FileWriter(String fileName, Charset charset, boolean append);
10.2.4.2 字符缓冲输出流(BufferedWriter)
字符缓冲输出流BufferedWriter特有方法:void newLine();
跨平台的换行。
(1) 通过Writer基本流对象创建 BufferedWriter(Writer in);
(2) 通过Writer基本流对象创建并指定缓冲区大小 BufferedWriter(Writer in, int sz);
10.2.4.3 字符转换输出流(OutputStreamWriter)
(1) 通过OutputStream基本输入流创建默认字符集 OutputStreamWriter(OutputStream in);
(2) 通过OutputStream基本输入流创建指定字符集 OutputStreamWriter(OutputStream in, String charsetName);
10.2.4.4 字符打印流(PrintWriter)
字符打印流底层有缓冲区,想要自动刷新需要在构造函数中指定。基本用法和字节打印流一样。
(1) 通过Writer基本流对象/File对象/路径名创建 PrintWriter(Writer/File/String);
(2) 通过路径名和字符集创建 PrintWriter(String fileName, Charset charset);
(3) 通过OutputStream基本流对象创建 PrintWriter(OutputStream out); //也可加上autoFlush,Charset
(4) 通过Writer基本流对象并指定是否自动刷新 PrintWriter(Writer out, boolean autoFlush); //这个和(3)构造最常用
(5) 写出任意数据并自动刷新,自动换行 void println(Xxx xxx);
(6) 写出任意数据,不换行 void print(Xxx xxx);
(7) 写出带有占位符的语句 void printf(String format, Object... args);
10.3 字符集
- ASCII:存储英文,只需一个字节。编码规则:前面补0,补齐8位;解码规则:直接转换为10进制。
- GBK字符集:国家标准扩展,简体中文Windows系统默认使用的就是GBK。
(1)英文:用一个字节存储,GBK完全兼容ASCII,编码规则和ASCII一样:前面补0,补齐8位(一定以0开头)。
(2)汉字:用两个字节存储,高位字节二进制一定以1开头(与英文区分开),高位字节转成十进制之后是一个负数。 - Unicode字符集(万国码):UTF(Unicode Transformation Format):Unicode转换格式,如UTF-8:
(1)英文:占一个字节,二进制第一位是0,转成十进制是正数。
(2)中文:占三个字节,二进制第一位是1,第一个字节转换成十进制是负数。
乱码出现的原因:
(1)读取数据时未读完整个汉字。
(2)编码和解码的方式不统一。
如何不产生乱码:
(1)不要用字节流读取文本文件。
(2)编码解码使用同一个码表,同一个编码方式。
10.4 Commons-io
Commons-io的jar包中封装了很多关于IO流的操作,可以导入jar包或Maven导入依赖来操作。
- FileUtils类(文件/文件夹相关),静态方法
(1) 复制文件 void copyFile(File srcFile, File destFile);
(2) 复制文件夹 void copyDirectory(File srcDir, File destDir); //原封不动的拷贝
(3) 复制文件夹 void copyDirectoryToDirectory(File srcDir, File destDir); //拷贝到了destDir的里面
(4) 删除文件夹 void deleteDirectory(File directory);
(5) 清空文件夹 void cleanDirectory(File directory);
(6) 读取文件数据为字符串 String readFileToString(File file, Charset encoding);
(7) 写出操作 void write(File file, CharSequence data, String encoding);
- IOUtils类(流相关),静态方法
(1) 复制文件 int copy(Inputstream input, Outputstream output);
(2) 复制大文件 int copyLarge(Reader input, Writer output);
(3) 读取数据 String readLines(Reader input);
(4) 写出数据 void write(String data, OutputStream output);
11. 多线程
进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
线程:一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程不能独立的存在,它必须是进程的一部分。
多线程可以提高对CPU的利用率,提高效率。
并发:在同一时刻,有多个指令在单个CPU上交替
执行。
并行:在同一时刻,有多个指令在多个CPU上同时
执行。
11.1 多线程实现方法
- 继承Thread类,重写run方法。
- 实现Runnable接口,实现run方法。
new Thread(()-{线程执行内容}).start();
,可以直接用Lambda表达式实现Runnable的匿名表达式。 - 实现Callable接口和创建FutureTask对象(可以获取多线程运行的结果),实现call方法,创建FutureTask对象来管理线程运行结果get()。
11.2 Thread常用方法
Java中的线程是抢占式调度,具有随机性,优先级越大则抢到CPU的概率越大。守护线程:当其它非守护线程执行完毕之后,守护线程会陆续结束。
(1) 获取线程的名字 String getName();//如果没有给线程设置名字,默认名字为Thread-序号
(2) 设置线程的名字(构造方法也可设置名字) void setName();
(3) 获取当前线程的对象(静态方法) Thread Thread.currentThread(); //JVM开启后运行的main线程
(4) 让线程休眠指定的时间,单位毫秒(静态方法) void Thread.sleep(long time);
(5) 设置线程的优先级 void setPriority(int newPriority); //最大优先级为10,最低为1,默认为5
(6) 获取线程的优先级 int getPriority();
(7) 设置为守护线程 void setDaemon(boolean on); //on为true就会设置成守护线程
(8) 出让线程/礼让线程(静态方法) void Thread.yield(); //出让当前CPU执行权,但是也会执行
(9) 插入线程/插队线程 void join(); //等待这个线程死亡,再继续执行下边的线程
11.3 线程生命周期
![](https://i-blog.csdnimg.cn/blog_migrate/ae80eec453285a23d27c45021215651b.png#pic_center)
注意:JVM中没有运行状态,因为它把线程交给操作系统了,JVM就不管了。
11.4 同步代码块
由于线程是抢占式调度,CPU随时可能被其它线程抢走,因此会导致数据不安全(某程序还未执行完就被其它线程抢走,数据可能被其它线程更改,当该线程再次执行时,数据就不是之前的数据了)。同步代码块可以把操作共享数据的代码锁起来, synchronized(锁) {操作共享数据的代码}
,锁对象必须是唯一的,可以用static修饰的成员变量,也可用自身类.class的字节码文件充当锁。
(1)锁是默认打开的,当有一个线程进去时,锁会自动关闭,即使其它线程抢到了CPU资源,也无法进去,只能等待。
(2)里面的代码全部执行完毕,线程出来,锁自动打开。
11.5 同步方法
同步方法就是把synchronized关键字加到方法上,修饰符 synchronized 返回值类型 方法名(方法参数) {...}
,同步方法是锁住方法里面所有的代码,锁对象不能自己指定(Java已经规定好的,对于非静态方法:this;对于静态方法:当前类的字节码文件对象)。
11.5 Lock锁与死锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock(Lock是接口不能直接实例化)。可通过Lock lock = new ReentrantLock();
来实例化,通过lock()
方法来获得锁(即手动上锁),通过unlock()
方法来释放锁(即手动开锁),可以把unlock放在finally中执行。
死锁:当锁嵌套使用时,可能会导致线程各自拿着自己的锁不放而又再等其它线程释放锁的无法继续执行的状况。
11.6 等待唤醒机制
等待唤醒机制又叫生产者消费者模式。线程A和B共享一块缓冲区,生产者A往缓冲区中放数据,而消费者B从缓冲区拿数据;如果缓冲区满了,生产者就无法继续放数据而等待,当消费者拿走数据后就会唤醒生产者继续放数据;如果缓冲区为空,消费者就因无数据可拿而等待,当生产者放入数据后就会唤醒消费者可以拿数据了。缓冲区的作用就是为了平衡生产者和消费者的处理能力,起到一个数据缓存的作用,同时也达到了一个解耦的作用。
(1)解耦:生产者和消费者之间通过一个共享的缓冲区进行通信,彼此之间并不直接依赖,这种解耦合使得系统更加灵活,能够适应不同的负载和不同的需求。
(2)并发性:生产者和消费者可以并发地执行,这可以充分利用系统的资源,提高系统的吞吐量和响应速度。
(3)缓冲作用:通过缓冲区,可以平衡生产者和消费者之间的速度差异,避免生产者和消费者之间出现因速度不一致而产生的死锁和饥饿问题。
(4)可扩展性:生产者消费者模式易于扩展,可以根据实际需求增加生产者和消费者的数量,或者使用多个缓冲区提高系统的并发性能。
(1) 当前线程等待,直到被其他线程唤醒 void wait();
(2) 随机唤醒单个线程 void notify();
(3) 唤醒所有线程 void notifyAll();
阻塞队列
实现等待唤醒机制:实现Iterable、Collection、Queue和BlockingQueue接口的实现类阻塞队列ArrayBlockingQueue(底层是数组,有界,需手动指定长度)和LinkedBlockingQueue(底层是链表,无界,最大值为int的最大范围)。可以通过阻塞队列的put()
方法把数据放入队列,通过take()
方法从队列中拿走数据,注意:put方法和take方法底层都是定义了锁的,因此不用再定义锁了,但是put和take方法外的程序是没有锁的。生产者和消费者必须共用一个阻塞队列才可以实现等待唤醒机制。
11.7 线程池
线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。
- 创建线程池对象,
Executors
:线程池的工具类通过调用方法返回不同类型的线程池对象。
(1) 创建一个没有上限的线程池 ExecutorService Executors.newCachedThreadPool();
(2) 创建一个有固定数量的线程池 ExecutorService Executors.newFixedThreadPool(int nThreads);
- 提交任务,线程池会创建新的线程对象,任务执行完毕,线程归还给线程池,等下次再提交任务时,就不需要创建新的线程,而是直接复用已有的线程即可;若提交任务时,线程池中既没有空闲线程,又无法创建新的线程,该任务就会排队等待。
(1) 提交实现Runnable接口的任务 Future<?> submit(Runnable task); //同样可以用Lambda表示式来实现
(2) 提交实现Callable接口的任务 Future<T> submit(Callable<T> task);
11.8 自定义线程池
利用ThreadPoolExecutor类的构造方法来创建自定义的线程池。最大线程数=核心线程数+临时线程数,当核心线程数都在使用,且队列中排队等待的任务满了,才会使用临时线程,而当用了临时线程和队满仍然不够时,就会拒绝服务任务。
4核8线程:Intel研发的超线程技术可把处理器内部的一个物理CPU模拟成两个逻辑CPU,4核8线程就是将四个物理核心模拟成八个逻辑核心,同时会有四核支持八线程的操作,因此最大并发数就为8。
线程池大小选择:CPU密集型(计算多,读取文件少):最大并行数+1(加1是候补,防止前边有线程出问题,这个线程就可以代替);I/O密集型(读取文件多,计算少):最大并行数 x 期望CPU利用率 x 总时间(CPU计算时间+等待时间) / 计算时间,可用thread dump工具来进行测试计算时间和等待时间。
ThreadPoolExecutor(int corePoolSize, //核心线程数,不能小于0
int maximumPoolSize, //最大线程数,不能小于0且大于等于核心线程数
long keepAliveTime, //临时线程最多不工作的时间,超过就会销毁,不能小于0
TimeUnit unit, //临时线程最多不工作的时间单位,如TimeUnit.SECONDS
BlockingQueue<Runnable> workQueue, //任务队列,即阻塞队列,不能为null
ThreadFactory threadFactory, //线程工厂,如Executors.defaultThreadFactory(),不能为null
RejectedExecutionHandler handler);//任务拒绝策略,如new ThreadPoolExecutor.AbortPolicy(),静态内部类,不能为null
12. 网络编程
C/S架构:客户端/服务器(Client/Server),在用户本地需要下载并安装客户端程序,在远程有一个服务器端程序。
B/S架构:浏览器/服务器(Browser/Server),只需要一个浏览器,用户通过不同的网址来访问不同的服务器。
12.1 网络编程三要素
- IP地址:上网设备在网络中的地址,是唯一的标识。
(1)IPv4:32位,点分十进制
(2)IPv6:128位,冒分十六进制 - 端口号:应用程序在设备中唯一的标识,取值范围0-65535,一般用户用1024往上的端口,一个端口号只能被一个应用程序使用。
- 协议:数据在网络中传输的规则,常见的协议有UDP、TCP、http、https和ftp等。
(1)UDP:用户数据报协议(User Datagram Protocol),UDP是面向无连接的传输层通信协议,速度快一次最多发送64KB数据,数据不安全,容易丢失数据。提供了应用程序之间要发送数据的数据报,由于UDP缺乏可靠性且属于无连接协议,所以应用程序通常必须容许一些丢失、错误或重复的数据包。
(2)TCP:传输控制协议(Transmission Control Protocol) 是一种面向连接的、可靠的传输层通信协议。TCP 保障了两个应用程序之间的可靠通信。
12.2 InetAddress
该类代表着IP地址,底层会根据输入的是IPv4还是IPv6创建对应的子类并返回。
(1) 根据IP地址创建对象 InetAddress InetAddress.getByAddress(byte[] addr);
(2) 根据主机名创建对象 InetAddress InetAddress.getByName(String host);
(3) 根据主机名和IP地址创建对象 InetAddress InetAddress.getByAddress(String host, byte[] addr);
(4) 获取本地主机对象 InetAddress InetAddress.getLocalHost();
(5) 获取IP地址字符串 String getHostAdress();
(6) 获取此IP地址的主机名 String getHostName();
12.3 UDP通信
- 创建DatagramSocket对象(数据报套接字)
(1) 空参构造,随机绑定本机可用的端口 DatagramSocket();
(2) 指定端口 DatagramSocket(int port);
(3) 创建指定IP地址和端口的UDP套接字 DatagramSocket(int port, InetAddress laddr);
- 创建DatagramPacket对象(数据报包)
(1) 构造数据报包用于发送 DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port);
(2) 构造数据报包用于发送 DatagramPacket(byte[] buf, int offset, int length, SocketAddress address);
(3) 构造数据报包用于发送 DatagramPacket(byte[] buf, int length, InetAddress address, int port);
(4) 构造数据报包用于发送 DatagramPacket(byte[] buf, int length, SocketAddress address);
(5) 构造对象用于接收数据包 DatagramPacket(byte[] buf, int length); //或者可以通过set方法设置内容作为数据报包发送出去
(6) 构造对象用于接收数据包 DatagramPacket(byte[] buf, int offset, int length);
成员方法(都有对应的get方法)
(1) 设置端口号 void setPort(int iport);
(2) 设置数据 void setData(byte[] buf);
(3) 设置数据 void setDate(byte[] buf, int offset, int length);
(4) 设置数据包长度 void setLength(int length);
(5) 设置要发送的IP地址 void setAddress(InetAddress iaddr);
(6) 设置远程主机的套接字 void setSocketAddress(SocketAddress address);
(7) 获取发送或接收的偏移量 int getOffset();
- 用DatagramSocket对象发送或接收DatagramPacket对象,send()、receive(),receive()方法是阻塞的,如果没接收到数据就一直会阻塞等待,
发送时数据包指定的端口和接受时数据报套接字的端口要一致
。 - 释放资源,DatagramSocket对象调用close()方法关闭。
UDP的单播、组播和广播
(1)单播:一个设备只发送信息给另一台设备。
(2)组播:消息发送给一组设备,预留的组播IP:224.0.0.1-224.0.0.255,创建MulticastSocket组播套接字对象,在接收数据时,应该先加入指定IP的那一组才能接收数据joinGroup(InetAddress mcastaddr)。
(3)广播:消息发送给整个局域网,IP:225.225.225.225
12.4 TCP通信
- 客户端
(1)创建Socket对象,指定要连接的IP和端口(套接字) Socket client = new Socket(String host, int port);
(2)根据Socket对象获取输出流对象,并写出数据 client.getOutputStream();,该Socket对象同样可获取输入流对象,读取从服务端传输的数据。
(3)释放输出流资源和Socket资源。 - 服务端(可以结合循环+多线程来接收多个客户端的连接,也可以用线程池来减少线程的频繁创建与销毁)
(1)创建ServerSocket对象,指定客户端要连接的端口 ServerSocket server = new ServerSocket(10000);
(2)监听客户端的连接,一直等待连接直到连接上 Socket socket = server.accept();
(3)根据Socket对象获取输入流对象,并读取数据(转换流转成字符流) new InputStreamReader(socket.getInputStream());,该Socket对象同样可获取输出流对象,向客户端写出数据,前提是监听到客户端已经结束了输出流(即客户端调用了shutdownOutput()方法)
(4)释放资源。
UUID:表示不可变通用唯一标识符,UUID是由一组32位数的16进制数字所构成,以连字号分为五段,形式为8-4-4-4-12的32个字符,String str = UUID.randomUUID().toString().replace("-", "");
13 反射
反射允许对封装类的字段(成员变量),方法(成员方法)和构造函数(构造方法)的信息进行编程访问。JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个属性和方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。前提:必须先要获取到该类的Class字节码文件对象。反射的作用:获取一个类里面所有的信息,获取到了之后,再执行其他的业务逻辑;结合配置文件,动态的创建对象并调用方法。
13.1 获取Class对象
以下三种方法获取Class对象的完全一样的。
- 将java文件编译成class文件,在硬盘操作,尚未加载到内存,即源代码阶段:
Class.forName("全类名");
,最为常用。 - 字节码class文件加载到内存后,即加载阶段:
类名.class;
,一般当作参数,如同步代码块可用它来代替锁。 - 在内存中创建对象后,即运行阶段:
对象.getClass();
,获取对象后才可以使用。
13.2 反射获取成员对象
万物皆对象, 构造方法对象(Constructor)、字段即成员变量(Field)和成员方法(Method)。
- 反射获取构造方法对象Constructor
(1) 通过Class对象获取所有public修饰的构造方法对象 Constructor<?>[] getConstructors();
(2) 通过Class对象获取所有的构造方法对象(包括私有) Constructor<?>[] getDeclaredConstructors();
(3) 通过Class对象获取单个public修饰的构造方法对象 Constructor<?> getConstructor(Class<?>...parameterTypes);//如int.class
(4) 通过Class对象获取单个构造方法对象(包括私有) Constructor<?> getDeclaredConstructor(Class<?>...parameterTypes);
(5) 获取构造方法的访问权限 int getModifiers(); //如public返回1,private返回2
(6) 获取Class名 String getName(); //根据构造方法对象获得对应的Class名
(7) 获取构造方法的全部参数 Parameter[] getParameters(); //获取的什么构造方法对象就对应什么参数
(8) 临时取消对访问权限的校验 void setAccessible(boolean flag);//如对于private修饰的构造方法,就可以利用(8)创建对象
(9) 利用构造方法对象来创建对象 T newInstance(Object... initargs); //构造方法对象应有的参数,
- 反射获取成员变量对象Field
同样有获取访问权限的getModifiers()方法,临时取消访问权限校验的setAccessible(true)方法。
(1) 通过Class对象获取所有public修饰的成员变量 Field[] getFields();
(2) 通过Class对象获取所有成员变量(包括私有) Field[] getDeclaredFields();
(3) 通过Class对象获取单个public修斯的成员变量 Field getField(String name);
(4) 通过Class对象获取单个成员变量(包括私有) Field getDeclaredField(String name);
(5) 获取成员变量对象的名字 String getName();
(6) 获取成员变量对象的类型 Class<?> getType();
(7) 获取某对象成员变量的值 Object get(Object obj); //参数为某对象,获取的值就是该对象成员变量的值,可以强转
(8) 修改某对象成员变量的值 void set(Object obj, Object value);
- 反射获取成员方法对象Method
同样有获取访问权限的getModifiers()方法,临时取消访问权限校验的setAccessible(true)方法,获取方法名字的getName()方法,获取方法参数的getParameters()方法。
(1) 通过Class对象获取所有public修饰的成员方法 Method[] getMethods(); //父类中public修饰的方法也会获取
(2) 通过Class对象获取所有的成员方法(包括私有) Method[] getDeclaredMethods(); //不会获取父类中的方法,但会获取本类私有的
(3) 通过Class对象获取单个public修饰的成员方法 Method getMethod(String name, Class<?>...parameterTypes); //name为方法的名字
(4) 通过Class对象获取单个成员方法(包括私有) Method getDeclaredMethod(String name, Class<?>...parameterTypes);
(5) 获取成员方法对象抛出的异常 Class[] getExceptionTypes();
(6) 运行某对象的成员方法 Object invoke(Object obj, Object...args); //执行obj的此方法并传递参数(若没有就不写)返回返回值
13.3 动态代理
Java动态代理是一种机制,允许在运行时创建代理对象,这些代理对象能够在不改变原始对象的情况下对其进行增强或修改其行为。在Java中,动态代理通常用于实现面向切面编程(AOP)的概念,它可以用来拦截方法调用并执行一些额外的操作,例如日志记录、性能监测等。
Java动态代理通常使用Java自带的java.lang.reflect.Proxy类来实现,其使用了Java反射机制来动态生成代理对象。
(1)要使用动态代理,需要定义一个接口,原始对象要实现该接口中的方法。
(2)然后使用Proxy类的静态方法newProxyInstance()来创建代理对象。这个方法接受三个参数:
类加载器(原始对象.getClass().getClassLoader()
);
要代理的接口列表(new Class[]{原始对象类实现的接口名.class}
)
一个InvocationHandler对象,它负责拦截方法调用并执行相应的操作,该对象如下所示:
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//proxy为代理的对象,method为代理的方法,args为对应的参数
Object result = method.invoke(target, args); //target为要代理的对象
return result;
}
}
14. 常用IDEA快捷键、插件和注解
14.1 常用快捷键
- 单行注释:
Ctrl+/
- 多行注释:
Ctrl+Shift+/
- 提取方法:
Ctrl+Alt+M
- 自动修正:
Alt+Enter
- 构造函数、getter、setter:
Alt+Insert
- 格式化代码:
Ctrl+Alt+L
- 复制当前行:
Ctrl+D
- 删除当前行:
Ctrl+X
- 批量修改变量名:
Shift+F6
- 将代码放try catch、if、for等中:
Ctrl+Alt+T
- 大小写转换:
Ctrl+Alt+U
- 快速生成变量:
Ctrl+Alt+V
- 看方法的参数:
Ctrl+P
- 竖着改动相同变量名:
Alt+左键
- 快捷搜索:
Ctrl+N
- 查看类中方法:
Ctrl+F12
- 快速移动行:
Ctrl+Shift+方向键
- 查看接口的继承或实现:
Ctrl+H
14.2 常用插件
- 快速生成标准Javabean:
ptg
- 正则表达式速成:
any-rule
14.3 常用注解
- 方法重写:
@Override
- 函数式接口:
@FunctionalInterface