JavaSE
第一章 Java语言概述
1.1 Java语言特点
面向对象(使用计算机模拟现实世界,解决现实问题,贴近人类思维模式)
简单(相对C、C++,不易造成内存溢出、减少代码量、代码可读性强)
跨平台(操作系统、服务器、数据库)
安全性:Java的存储分配模型是它防御恶意代码的主要方法之一。所以很多大型企业级项目开发都会选择用Java开发。
编译和解释性:Java编译程序生成字节码,而不是通常的机器码,这使得Java开发程序比用其他语言开发程序快很多。
健壮性:Java编写的程序具有多方面的可靠性。Java编译器能够检测许多在其他语言中仅在运行时才能检测出来的问题。
分布式:Java 语言支持 Internet 应用的开发,在基本的 Java 应用编程接口中有一个网络应用编程接口(java net),它提供了用于网络应用编程的类库,包括 URL、URLConnection、Socket、ServerSocket 等。
体系结构中立:Java 程序(后缀为 java 的文件)在 Java 平台上被编译为体系结构中立的字节码格式(后缀为 class 的文件),然后可以在实现这个 Java 平台的任何系统中运行。
1.2 计算机语言执行机制
编译执行:
将源文件编译成机器码,一次编译,多次执行。
执行效率高,不可跨平台。解释执行:
将源文件一行一行解释,一行一行执行。不同的操作系统具备不同的解释器。
执行效率低,可以跨平台。
1.3 Java语言的执行机制
先编译、再解释:
将源文件编译成字节码文件(平台中立文件.class),再将字节码文件进行解释执行。
Java的设计理念:Write Once Run Anywhere
1.4 JDK、JRE、JVM之间的关系
JVM(Java Virtual Machine)虚拟机:
使用软件在不同操作系统中,模拟相同的环境。JRE(Java Runtime Environment)运行环境:
包含JVM + 类库,完整的Java运行环境。JDK(Java Development Kit)开发环境:
包含JRE + 开发工具包(编译器+调试工具)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IaqrOPtv-1661564741153)(Java语言概述.assets/1659268351240.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nNIYrOoX-1661564741156)(Java语言概述.assets/20200709130053990.png)]
1.5 为什么配置环境变量为bin目录
配置环境变量相当于将JDK
可执行命令
的目录通知给操作系统java 以及 javac 都是JDK提供的可执行文件 (二进制文件)
配置了环境变量就可以在计算机的任意目录使用bin目录里的javac和java等。
1.6 编译和执行
1.徽标+ R 呼出DOS命令窗口
2.使用cd 跳转到java文件所在的位置 (或者在地址栏中直接输入cmd 也可以)
3.javac + 文件名(带后缀) 将java文件编译为class文件
4.java + class文件名(不带后缀) 执行class文件
1.7 类的阐述
- 同一个源文件中可以定义多个类。
- 编译后,每个类都会生成独立的 .class文件。
- 一个类中,只能有一个main方法,每个类都可以有自己的main方法。
- public修饰的类称为公开类,要求类名必须与文件名称完全相同,包括大小写。
- 一个源文件中,只能有一个公开类。
第二章 Java基础语法
2.1 关键字与保留字
被Java官方赋予了特殊含义
以及用途
的小写单词,称之为关键字
Java现阶段版本中未使用,在未来可能会使用的单词,称之为保留字
关键字或者保留字我们自定义的名称要避免与之冲突
2.2 标识符
Java 对各种变量、方法和类等要素命名时使用的字符序列称为标识符
总结:凡是自己可以起名字的内容都称之为标识符。
标识符命名规则
1.标识符由英文字母大小写,0~9,_或$组成。
2.数字不可开头。
3.不能使用关键字和保留字,但可以包含关键字和保留字
4.JAVA中严格区分大小写,标识符长度无限制。
5.标识符不能含空格。*提示:凡是自己可以起名字的地方都叫标识符。
2.3数据类型
2.3.1基本数据类型
八大基本数据类型:byte、short、int、long、float、double、boolean、char
八大基本数据类型所占字节数分别为:1、2、4、8、4、8、1、2
1.整型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TxgVUhU8-1661564741157)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day02-%E7%AC%94%E8%AE%B0/img/%E6%95%B4%E6%95%B0%E7%B1%BB%E5%9E%8B.png)]
注意:int为整数的默认类型
为long类型赋值 如果取值范围没有超过int 则末尾的L 可加 可不加
如果取值范围超过int 必须加L 大小写都可以 推荐大写
因为整数的默认类型为int类型
- 当byte类型的变量赋值为128时,会报错误不兼容的类型: 从int转换到byte可能会有损失,因为大于byte类型取值范围的值,默认为int类型的数值不能直接转为byte类型需要强制类型转换;short同理。
- 当int类型的变量赋值为2147483648时就会报错,错误为过大的整数: 2147483648,原因是数值超出了int类型的取值范围。
- 当long类型的变量没有超过int类型的取值范围时,可以不用加L,如果超过了int类型的取值范围,就需要加L,不然如果赋值为2147483648就会报错,错误为过大的整数: 2147483648,原因是整数的默认类型为int,而这个变量的数值已经超出了int类型的取值范围,所以要把int类型转为long类型要加L。
- 当赋值给long类型的变量时,且值没有的取值没有超过int的取值范围,则会按照int类型来存储,也就是占4个字节。
2.浮点型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sav1wU1T-1661564741158)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day02-%E7%AC%94%E8%AE%B0/img/%E6%B5%AE%E7%82%B9%E7%B1%BB%E5%9E%8B.png)]
浮点类型 float(单精度浮点数) double(双精度浮点数)
float和 double都是近似值 不是精确值
double为浮点类型的默认数据类型 如需为float类型赋值
末尾必须加F 大小写都可以 推荐大写
了解:如果给double类型赋值超过了float的取值范围 末尾必须加D 大小都可以 推荐大写
因为浮点数的默认类型为double类型
- 当float类型的变量赋值一定要加F
- 当double类型赋值超过了float的取值范围 末尾必须加D
3.布尔型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3p8U0PT-1661564741159)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day02-%E7%AC%94%E8%AE%B0/img/%E5%B8%83%E5%B0%94%E7%B1%BB%E5%9E%8B.png)]
可直接赋值true / false
也可赋值一个结果为true / false的表达式
注意:Java中的boolean不能参与算数运算
4.字符型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DtmtG0E0-1661564741159)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day02-%E7%AC%94%E8%AE%B0/img/%E5%AD%97%E7%AC%A6%E7%B1%BB%E5%9E%8B.png)]
字符赋值:char c1 = ‘A’;(通过’'描述为字符赋值)
整数赋值:char c2 = 65;(通过十进制数65在字符集中对应的字符赋值)
直接给char类型赋值整数数值 如果是在0 ~ 127 这个范围 将参考ASCII 码表 ,如果超出了这个范围 将参照Unicode编码表(万国码)
万国码记录了世界上绝大多数国家的语言 包含中文,万国码是一个十六进制的编码表 中文的取值范围 是 \u4e00 ~ \u9fa5
- 进制赋值:char c3 = ‘\u0041’;(通过十六进制数41在字符集中所对应的字符赋值)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLy2ijxw-1661564741161)(Java基础.assets/1659357386280.png)]
第一个因为数字49所对应的acsll编码表是字符1,所以c11输出的是字符1
第二个因为数字49所对应的acsll编码表是字符1,而后面又强制类型转换为数字类型,所以还是数字49不能是字符1
2.3.2引用数据类型
1.字符串
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lpZKlrwJ-1661564741161)(JavaSE.assets/截图-1659542342658.png)]
2.数组
3.对象
2.3.2变量名的命名规范
字下美人数骆驼
字:字母
下:下划线
美:美元符号$
人:人民币符号 ¥
数:数字
可以以字母、下划线、美元符号、人民币符号开头,可以包含数字,不能以数字开头
骆驼:驼峰命名法 单词首字母小写,后续每有一个新的单词,首字母大写 personName studentAge
见名知义,有意义
2.3.3声明变量的3种方式
1、先声明,再赋值:【常用】
数据类型 变量名;
变量名 = 值;2、声明并赋值:【常用】
数据类型 变量名 = 值;3、多个同类型变量的声明与赋值:【了解】
数据类型 变量1 , 变量2 , 变量3 = 值3 , 变量4 , 变量5 = 值5;
2.4 类型转换
自动提升,手动下降
2.4.1 自动类型转换(提升)
自动类型转换
1.两种类型要兼容 数值与数值类型 兼容 整数类型与char类型兼容
2.目标类型(等号左边)取值范围大于
源类型(等号右边)取值范围
两个操作数有一个为double,计算结果提升为double。
如果操作数中没有double,有一个为float,计算结果提升为float。
如果操作数中没有float,有一个为long,计算结果提升为long。
如果操作数中没有long,有一个为int,计算结果提升为int。
如果操作数中没有int,均为short或byte或者char,计算结果仍旧提升为int。
两个操作数做计算结果自动提升为取值范围较大的类型,如果没有int,均为short,byte或者char,结果依旧为int类型
特殊:任何类型与String相加(+)时,实为拼接,其结果为String。
//int类型将变量ch3的char类型转换为int类型
//1默认是int类型,char的取值范围小于int类型,他两相加用int类型存储更合适,这里其实是运算自动类型提升
System.out.println(ch3 + 1);
计算机存储数据使用二进制存储
计算机统一使用补码来表示所有的整数二进制,所以你在计算器看到的都是补码。
正整数的原码和补码是相同的。
负数的原码和补码不同 ,是由原码先转为反码从而得到补码的。
负数的反码:原码符号位不变,其余位取反。
负数的补码:反码基础之上+1。
2.4.2 强制类型转换(下降)
强制类型转换
1.两种类型要兼容
2.目标类型(等号左边)取值范围小于源类型(等号右边)
1. 正常情况(数据大小合适足够存放)
public class Test1{
public static void main(String [] args){
// 强制类型转换
// 1.两种类型要兼容
// 2.目标类型(等号左边)取值范围小于源类型(等号右边)
short s1 = 120;
byte b1 = (byte)s1;
System.out.println(b1);
int i1 = 4512;
short s2 = (short)i1;
System.out.println(s2);
long l1 = 5623;
int i2 = (int)l1;
System.out.println(i2);
float f1 = 30.14F;
long l2 = (long)f1;
System.out.println(l2);
double d1 = 20.5;
byte b2 = (byte)d1;
System.out.println(b2);
}
}
2. 特殊情况(超过目标类型取值范围)
强制类型转换 特殊情况
将超出目标类型取值范围的数值强制转换
分为:
- 数据截断
- 符号位发生变化: 原本的正数变为负数,这样一截断就变成补码的形式
public class Test2{
public static void main(String [] args){
// 强制类型转换 特殊情况
// 将超出目标类型取值范围的数值强制转换
short s1 = 257; // 16位
byte b1 = (byte)s1; // 8位
System.out.println(b1); // 长度不够 数据截断
short s2 = 128;
byte b2 = (byte)s2;
System.out.println(b2); // 符号位发生变化
short s3 = 129;
byte b3 = (byte)s3;
System.out.println(b3); // 符号位发生变化 原本的正数变为负数 补码
}
}
2.5 运算符
2.5.1 算术运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PSoCiN5i-1661564741162)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day03-%E7%AC%94%E8%AE%B0/img/%E7%AE%97%E6%95%B0%E8%BF%90%E7%AE%97%E7%AC%A6.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3iboKzYT-1661564741163)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day03-%E7%AC%94%E8%AE%B0/img/%E5%8A%A0%E5%8A%A0%E5%87%8F%E5%87%8F.png)]
++或者-- 如果单独作为一条语句书写 在前在后 没有区别
如果不是单独作为一条语句书写是有区别的
++或者–在前 先执行++或者-- 再执行其他的 先自增(减),再赋值
++或者–在后 先执行其他的 再执行++或者-- 先赋值,再自增(减)
2.5.2 赋值运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KIzkgK2p-1661564741164)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day03-%E7%AC%94%E8%AE%B0/img/%E8%B5%8B%E5%80%BC%E8%BF%90%E7%AE%97%E7%AC%A6.png)]
s1+=10这样写相当于s1=s1+10,但是与他还是有一定区别的:
s1是short类型,而10默认是int类型,会自动类型转换,整个式子会提升为int类型。从而报错,int转为short不能自动类型转换。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IhhmrE3U-1661564741165)(JavaSE.assets/1659441662530.png)]
而直接s1+=10这样的写法没有另外一个变量再去提升一下,所以他本身short还是short类型。
类型取值较大的需要转为类型取值较小的,需要进行强制类型转换。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a40Z2DaS-1661564741165)(JavaSE.assets/1659441826040.png)]
2.5.3 关系运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h9SGB5FC-1661564741166)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day03-%E7%AC%94%E8%AE%B0/img/%E5%85%B3%E7%B3%BB%E8%BF%90%E7%AE%97%E7%AC%A6.png)]
关系运算符 最终结果都为布尔类型
2.5.4 逻辑运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uF49HTrE-1661564741167)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day03-%E7%AC%94%E8%AE%B0/img/%E9%80%BB%E8%BE%91%E8%BF%90%E7%AE%97%E7%AC%A6.png)]
&& 短路与 要求两个或者多个条件同时成立 结果才为真 true
短路与 有短路的效果 如果第一个条件不成立 则后续的条件不再执行& 与 要求两个或者多个条件同时成立 结果才为真 true
没有短路的效果 不管前边的条件是否成立 都将执行完所有的条件(表达式)|| 短路或 要求两个或者多个条件有一个成立 结果为真 true
短路或 有短路的效果 如果第一个条件已经成立 则后续的条件不再执行| 或 要求两个或者多个条件有一个成立 结果为真 true
没有短路的效果 不管前边的条件是否成立 都将执行完所有的条件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vvI6JYA2-1661564741167)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day03-%E7%AC%94%E8%AE%B0/img/%E4%B8%89%E7%9B%AE%E8%BF%90%E7%AE%97%E7%AC%A6.png)]
三目(三元)运算符
格式: 布尔表达式 ? 结果1 : 结果2
表示如果条件成立 则执行结果1 不成立 则执行结果2
2.5.5 位运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1aHW9dVn-1661564741168)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day03-%E7%AC%94%E8%AE%B0/img/%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BTtyNty8-1661564741168)(JavaSE.assets/截图.png)]
1.<<:左移几位表示乘以2的几次方 (在有效的范围以内 )
2.>>:右移几位表示除以2的几次方 (在有效的范围以内)
如果首位为0 则空缺的位置补0
如果首位为1 则空缺的位置补1
3.无符号右移 空缺位统一以0来填充
4. & 相同位的二进制数值进行与运算 如果都为1 则结果为1 其他的都为0
5.| 相同位的二进制数值进行或运算 有一个为1 则为1 其他都为0
6.^ 相同位的二进制数值进行异或运算 不同为1 相同位0
7.~ 包括符号位在内 0改为1 1改为0
2.5.6 优先级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DR9aO0ip-1661564741169)(JavaSE.assets/截图-1659543337255.png)]
2.6 不同进制表示整数
0B开头 表示二进制数值
0开头 表示八进制数值
0X 开头 表示十六进制数值
第三章 程序流程控制
3.1 局部变量
概念:定义在方法体内的变量都属于局部变量
局部变量 | 描述 |
---|---|
定义位置 | 方法体内 |
默认值 | 没有默认值,必须先赋值才能使用 |
作用范围 | 离当前变量最近的大括号以内 |
能否重名 | 在重合的作用范围以内不能重名 |
存储位置(了解) | 基本数据类型存在栈(虚拟机栈stack)中,引用数据类型变量名存在栈,值在堆(heap) |
生命周期(了解) | 随着方法的入栈(压栈)而有生命,随着方法的出栈(弹栈)而死亡 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XA4qTwvv-1661564741169)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day04-%E7%AC%94%E8%AE%B0/img/%E5%A0%86%E6%A0%88.png)]
3.2 Scanner工具类
Scanner 单词:扫描
java.util.Scanner :此类的作用用于实现接收用户输入的信息 实现用户和程序交互
此类提供了各种功能用于接收不同的类型的用户输入的数据
next() 表示接收用户输入的字符串
nextInt() 表示接收用户输入的int整数
nextFloat() 表示接收用户输入的float浮点数
nextDouble() 表示接收用户输入的double浮点数
nextBoolean()接收用户输入的布尔数据
注意:
1.没有nextChar() 也就是说 即使要让用户输入一个字符 也必须使用next()
2.每种功能都有其对应的数据类型,如果输入数据类型不兼容,将出现异常:
InputMismathException
3.3 选择结构
3.1.1 if语句
1. 基本if选择结构
语法:
if(布尔表达式){
//代码块1
}后续代码…
执行流程:
对布尔表达式进行判断。
结果为true,则先执行代码块1,再执行后续代码。
结果为false,则跳过代码块1,直接执行后续代码。
2. if-else 结构
语法:
if(布尔表达式){
//代码块1
}else{
//代码块2
}
后续代码…
执行流程:
对布尔表达式进行判断。
结果为true,则先执行代码块1,再退出整个结构,执行后续代码。
结果为false,则先执行代码块2,再退出整个结构,执行后续代码。
3. 多重if结构
语法:
if(布尔表达式1){
//代码块1
}else if(布尔表达式2){
//代码块2
}else if(布尔表达式3){
//代码块3
}else{
//代码块4
}执行流程:
表达式1为true,则执行代码块1,再退出整个结构。表达式2为true,则执行代码块2,再退出整个结构。
表达式3为true,则执行代码块3,再退出整个结构。
以上均为false,则执行代码块4,再退出整个结构。
注意:相互排斥,有一个为true,其他均不再执行,
适用于区间判断。
多重if:
else if 不能单独出现
else if个数不限
末尾的else根据情况 可写可不写
通常适用于区间判断
如果用于区间判断 那么条件书写应该是有序的 升序或者 降序 不能是乱序的如果条件判断的是一个区间,相互排斥,则顺序对程序的执行有影响
如果不是判断区间,则顺序对if……else if……程序执行结果没有影响。
4. 嵌套if结构
语法:
if(外层表达式){
if(内层表达式){
//内层代码块1
}else{
//内层代码块2
}
}else{
//外层代码块
}执行流程:
当外层条件满足时,再判断内层条件。注意:
一个选择结构中,可嵌套另一个选择结构。
嵌套格式正确的情况下,支持任意组合。
3.1.2 switch语句
switch用于等值的判断
语法:
switch(变量|表达式){
case 值1:
逻辑代码1;
case 值2:
逻辑代码2;
case 值n:
逻辑代码n;
default:
未满足时的逻辑代码;
}可判断的类型:
byte、short、int、char、String(JDK7+)枚举执行流程:
如果变量中的值等于值1,则执行逻辑代码1。
如果变量中的值等于值2,则执行逻辑代码2。
如果变量中的值等于值n,则执行逻辑代码n。
如果变量中的值没有匹配的case值时,执行default中的逻辑代码。注意:
1.所有case的取值不可相同。
2.switch中的break不是必须的 根据需求是否书写 以及书写在哪个位置
3.switch中的default根据需求是否书写 default可以书写在任何位置 按照规范要求写在末尾
4.写在末尾的default按照规范也要加上break (如果每种情况都独立的话)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHERAFm2-1661564741170)(…/…/%E5%B0%9A%E7%A1%85%E8%B0%B7javaEE/%E7%AC%94%E8%AE%B0/JavaSE.assets/1659511421804.png)]
如果default在最后不加上break不影响程序,但是建议还是要加上:
1、加上去阅读性提高了,因为每种情况都是相互独立的
2、default位置不是固定的,想写在哪里都可以,写在第一个也可以,最后一个也可以。写在第一个如果不加break会直接执行后面case。
3.4 循环结构
任何循环都有四个必不可少的条件
计数器初始化
判断条件
循环体
计数器变化
3.4.1 while
1. 语法格式
while(布尔表达式){
循环体;
}
2. 执行原理
先判断布尔表达式的结果,如果是 true,则执行循环体,循环体结束之后,再次判断布尔表达式
的结果,如果是 true,再执行循环体,循环体结束之后,再判断布尔表达式的结果,直到结果
为 false 的时候,while 循环结束。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Ez83shG-1661564741171)(JavaSE.assets/1659676155026.png)]
3. for与while的区别
while 循环语法结构比 for 更简单,for 循环的计数器比 while 更清楚一些,另外 for 循环的计数器对应的变量可以在 for 循环结束之后就释放掉,但是 while 循环的计数器对应的变量声明在 while 循环外面,扩大了该变量的作用域。
3.4.2 do…while
1.语法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zFMkqvdO-1661564741171)(JavaSE.assets/1659676353550.png)]
2.执行原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aDDYXtce-1661564741172)(JavaSE.assets/1659676613243.png)]
3.while与do…while的区别
while循环是先判断再执行,条件不成立一次都不执行,执行次数为0~N
do-while先执行再判断,不管条件是否成立,至少执行一次,执行次数为1~N
4. 总结
while、do…while循环可以用于处理循环次数确定的情况,也可以用于处理循环次数不确定的情况,常用于循环次数不确定的情况
3.4.3 for循环
1.语法格式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nfNpE5wv-1661564741172)(JavaSE.assets/1659698143774.png)]
2. 执行顺序
第一轮:
1.先执行计数器初始化 并且只执行一次
2.执行判断条件
3.执行循环体
4.执行计数器变化
第二轮:
从第2步开始
……
第一次:1->2->3->4
第二次:2->3->4
3.注意事项
-
for循环的特殊写法会让程序进入死循环
- for(;😉{ //循环体 } 陷入死循环
- for(int i = 0;;i++){ //循环体 } 缺少判断条件陷入死循环
- for(int i = 0;i <= 10;){ //循环体 } 缺少计数器变化导致死循环
-
在 for 循环当中声明的变量只在for 循环中有效,当 for 循环结束之后,初始化变量的内存就释放了/消失了。要想在for循环外使用初始化(计数器初始化)的作用域,就必须先在for循环之前定义初始化变量(计数器初始化),for 循环结束之后 i 变量的内存并不会被释放。后续的程序可以继续使用,直到声明该变量的方法结束后才会释放变量i的内存空间。
3.5.4 三种循环的对比
1.执行顺序
while 循环:先判断,再执行
do-while循环:先执行,再判断
for循环:先判断,再执行
2.适用情况
循环次数确定的情况,通常选用for循环
循环次数不确定的情况,通常选用while或do-while循环
3.5 转向语句
转向语句用于实现循环执行过程中程序流程的跳转。
3.5.1 break语句
1.适用场景:可以用在switch结构 或者 循环(三种循环)结构中
2.作用:分别表示跳出switch结构或者中断循环 未执行的循环次数不再执行
3.注意事项:
break 语句默认情况下只能终止离它“最近”的“一层”循环
break在循环中通常结合分支语句来使用
如果需要终止外层循环,则可以通过打标记的方式终止最外层循环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ubj6fiAA-1661564741173)(JavaSE.assets/1659700436423.png)]
3.5.2 continue语句
1.作用
continue 语句则是用来终止当前本次
循环,直接进入下一次
循环继续执行,通常也要结合分支语句使用
总之,break 用来终止循环,continue 用来中断当前本次循环,直接进入下一次循环继续执行。
2.break和continue区别
使用场合
break用于switch结构和循环结构中
continue用于循环结构中作用(循环结构中)
break语句终止某个循环,程序跳转到循环块外的下一条语句
continue跳出本次循环,进入下一次循环
双重循环亦如此
3.5.3 return语句
3.6 包的概念
包就是文件夹,用于我们方便管理java文件以及class
在包中定义的java文件,文件第一句必须以package声明所在包,包声明语句永远书写在第一行
包的命名规范:全部小写,域名倒置,不能以点开头或者结尾,可以包含点,每存在一个点表示一个子文件夹
package com.atguigu.test1;
public class Test1{
}
第四章 方法
4.1 方法
4.1.1 方法是什么(what)
方法其实就是一段普通的代码片段,可以完成某个特定的功能。
4.1.2 为什么使用方法(why)
问题:没有方法会使程序的代码冗余,重复代码多
方法的作用:方法要解决的问题是代码的重复性,可以解决代码的复用问题。减少代码冗余,增加代码的重用性。
4.1.3 方法用在什么地方(where)
当需要实现一段可以重复利用的功能的时候
4.1.4 方法如何使用(how)
[修饰符列表] 返回值类型 方法名(形式参数列表){
方法体;
}
形式参数:方法
定义
的时候书写的参数 形参规定了参数的个数、类型、顺序实际参数:方法
调用
的时候传入的参数 必须符合形参规定的个数、类型、顺序
4.2 返回值和返回值类型
public static 返回值类型 方法名(形参列表){
//方法体;
}
-
返回值类型可以是我们所学过的任何数据类型(8种基本数据类型,1种引用数据类型:String)
-
如果返回值类型书写不是void,那么就必须使用return关键字返回对应的结果
-
return关键字表示结束方法并且返回内容
4.3 return其他用法
4.3.1 在选择分支结构中返回内容
如果需要在分支结构中返回内容 那么必须保证每一条分支都有正确的返回值
4.3.2 在返回值类型为void的方法中使用return
在返回值类型为void的方法中使用return关键字,只表示中断方法,return关键字之后不能加任何内容
4.4 方法执行过程中内存的变化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0v8arO9q-1661564741174)(JavaSE.assets/1659950357660.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HLBiaAhs-1661564741174)(JavaSE.assets/1660044875941.png)]
不是栈顶部不能活跃,数据3一开始处于栈顶,所以数据3在活跃数据1、2是暂停的,等到数据3出栈后,数据2漏出来了,此时数据2处在栈顶,栈帧此时也会指向数据2,数据2具有活跃权,数据1是暂停的。
在运行期由类加载器加载编译后的class字节码文件加载到方法区当中,而不是在编译时就载入。
-
java 程序开始执行的时候先通过类加载器找到硬盘上的字节码(class)文件,然后将其加载到 java 虚拟机的方法区当中,开始调用 main 方法,main 方法被调用的瞬间,会给 main 方法在“栈”内存中分配所属的活动空间,此时发生压栈动作,main 方法的活动空间处于栈底。
-
方法只定义不去调用的话,只是把它的代码片段存储在方法区当中,java 虚拟机是不会在栈内存当中给该方法分配活动空间的,只有在调用的瞬间,java 虚拟机才会在“栈内存”当中给该方法分配活动空间,此时发生压栈动作,直到这个方法执行结束的时候,这个方法在栈内存中所对应的活动空间就会释放掉,此时发生弹栈动作。
-
java程序执行的时候,类加载器会将class文件加载到方法区,当方法调用时,jvm才会在栈中给当前方法分配内存,此时发生压栈动作,直到方法结束时,这个方法的内存空间就会被释放掉,此时发生弹栈动作。
-
注意:只要是方法都是在栈中分配空间。构造方法、静态方法、实例方法都一样,在调用时都会在栈里面压栈。构造方法执行结束后,会弹栈,构造方法执行之后会在堆里面创建一个对象,因为构造方法的目的就是创建对象。
4.5 方法重载
4.5.1 方法重载是什么(what)
方法重载(overload)是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数类型或参数个数或参数顺序。
4.5.2 为什么要使用方法重载(why)
- 问题缺陷:每个方法功能相似,只是类型不同,要定义几个不同的方法名,程序员需要记忆多个方法名,增加程序员压力
方法重载的作用:调用方法时所需要记忆的方法名更少一些,代码更加美观一些。
4.5.3 什么情况下我们考虑使用方法重载呢?(where)
在同一个类当中,如果多个功能是相似的,可以考虑将它们的方法名定义的一致,使用方法重载机制,这样便于程序员的调用,以及代码美观,但相反,如果两个方法所完成的功能完全不同,那么方法名也一定要不一样,这样才是合理的。
4.5.4 方法重载如何使用(how)
首先它们在同一个类当中,方法名一样,参数列表不同(类型、个数、顺序)
① 在同一个类当中。
② 方法名相同。
③ 参数列表不同:个数不同算不同,顺序不同算不同,类型不同也算不同。
注意:方法重载与访问修饰符、方法返回值类型无关
4.6 方法递归
4.6.1 什么是方法递归(what)
-
递归:递进和回归
-
递归的概念:就是方法自己调用自己,递归实质上是一个大的问题,拆分为若干个小的问题
-
递归的两个重要条件:
- 当前的问题可以拆分为若干个小的问题
- 必须有正确的出口
4.5.2 为什么使用方法递归(why)
递归可以解决某些循环所解决不了的问题。
第五章 数组
5.1 数组的概念
- 概念:一组连续的存储空间,存储多个相同数据类型的值,长度是固定的
5.2 数组的定义方式
5.2.1先声明、再分配空间:
数据类型[] 数组名;
数组名 = new 数据类型[长度];
5.2.2声明并分配空间:
数据类型[] 数组名 = new 数据类型[长度];
5.2.3声明并赋值(繁):
数据类型[] 数组名 = new 数据类型[]{value1,value2,value3,…};
5.2.4声明并赋值(简):
数据类型[] 数组名 = {value1,value2,value3,…};
package com.atguigu.test1;
/**
* @author WHD
* @description TODO
* @date 2022/8/8 9:30
* 数组的四种定义方式
*/
public class Test1 {
public static void main(String[] args) {
// 方式1 先声明 再开辟空间
int [] arr1;
arr1 = new int[5];
// 方式2 连声明 带开辟空间
int [] arr2 = new int[6];
int arr4 [] = new int[10];
// 方式3 声明并且赋值(繁琐)
int [] arr3 = new int[]{78,56,12,45,89};
// 方式4 声明并且赋值 (简单)
int [] arr4 = {45,12,56,89};
}
}
5.3 数组的访问
元素:数组中的每个数据称之为元素
访问:赋值/取值
数组元素的访问通过下标(索引,角标)实现,下标从0开始,往后依次+1
5.3.1如何使用下标访问元素
赋值 : 数组名[下标] = 值;
取值 : System.out.println(数组名[下标]);
访问超出范围的元素将会报数组下标越界异常ArrayIndexOutOfBoundsException
5.3.2数组的属性length
数组的属性:length 是一个int类型的数据 表示数组的长度
用法:数组名.length
5.3.3数组的默认值
数组的默认值:
整数:0
小数:0.0
字符:\u0000
布尔:false
其他:null
5.4 数组的常见操作
5.4.1 数组的扩容
数组的扩容
1.定义比原数组更长的新数组
2.将原数组中的数据依次复制到新数组中
3.完成地址的替换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5SOMnUfi-1661564741175)(JavaSE.assets/地址的替换.png)]
package com.atguigu.test4;
/**
* @author WHD
* @description TODO
* @date 2022/8/8 11:09
* 数组的扩容
* 1.定义比原数组更长的新数组
* 2.将原数组中的数据依次复制到新数组中
* 3.完成地址的替换
*/
public class Test1 {
public static void main(String[] args) {
int [] oldArr = {1,2,3,4,5};
System.out.println("原数组hash(地址)值" + oldArr);
int [] newArr = new int[oldArr.length * 2];
System.out.println("新数组hash(地址)值" + newArr);
for(int i = 0;i < oldArr.length;i++){
newArr[i] = oldArr[i];
}
System.out.println("**********元素复制完成**********");
for(int i = 0;i < newArr.length;i++){
System.out.print(newArr[i] + "\t");
}
System.out.println();
int a = 10;
int b = a; // 基本数据类型 赋值的是值
oldArr = newArr; // 引用数据类型直接赋值 赋值的是地址 这里赋值的是数组的地址
System.out.println("原数组hash(地址)值" + oldArr);
System.out.println("新数组hash(地址)值" + newArr);
System.out.println("**********完成地址的替換以後,再次遍历oldArr**********");
for(int i = 0;i < oldArr.length;i++){
System.out.print(oldArr[i] + "\t");
}
System.out.println();
}
}
5.4.2 数组的拷贝
-
循环将原数组中所有元素逐一赋值给新数组。
-
System.arraycopy(原数组,原数组起始,新数组,新数组起始,长度);
-
java.util.Arrays.copyOf(原数组, 新长度);//返回带有原值的新数组。
package com.atguigu.test6;
import java.util.Arrays;
/**
* @author WHD
* @description TODO
* @date 2022/8/8 14:44
* 循环将原数组中所有元素逐一赋值给新数组。
*
* System.arraycopy(原数组,原数组起始,新数组,新数组起始,长度);
*
* java.util.Arrays.copyOf(原数组, 新长度);//返回带有原值的新数组。
*/
public class Test1 {
public static void main(String[] args) {
// 方式1
int [] oldArr = {1,2,3,4,5};
int [] newArr = new int[oldArr.length * 2];
for(int i = 0;i < oldArr.length;i++){
newArr[i] = oldArr[i];
}
// 方式2
int [] nums1 = {1,2,3,4,5};
int [] nums2 = new int[nums1.length * 2];
System.arraycopy(nums1,1,nums2,2, 2);
for (int i = 0;i < nums2.length;i++){
System.out.print(nums2[i] + "\t");
}
System.out.println();
System.out.println("-------------------------------------------------");
// 方式3
int [] nums3 = {22,33,44,55};
int [] nums4 = Arrays.copyOf(nums3,10);
for(int i = 0;i < nums4.length;i++){
System.out.print(nums4[i] + "\t");
}
System.out.println();
}
}
5.5 值传递和引用传递的区别
例子:就好像课室,课室有66把椅子,别人从我们课室拿走了一把,对我们全班66个人来说,我们都共同少了一把椅子;再如有个人在地球上挖了一个洞,对地球人来说,我们的地球都少了一个洞。
面试题:值传递和"引用传递"的区别?
java中只有值传递,引用传递传递的也是一个值,只不过这个值是一个地址
- 基本数据类型作为参数传递 属于值传递 传递的是值本身 (值的副本 值的拷贝) 在方法中对参数的改变 不会影响实参
- 引用数据类型作为参数传递 属于引用传递 传递的是地址 在方法中对参数的改变 会影响实参
- String类型是特殊引用数据类型 作为参数传递 不会影响实参
程序分析
package com.atguigu.test8;
/**
* @author WHD
* @description TODO
* @date 2022/8/8 15:47
* 面试题:值传递和"引用传递"的区别?
* java中只有值传递,引用传递传递的也是一个值,只不过这个值是一个地址
* 基本数据类型作为参数传递 属于值传递 传递的是值本身 (值的副本 值的拷贝) 在方法中对参数的改变 不会影响实参
* 引用数据类型作为参数传递 属于引用传递 传递的是地址 在方法中对参数的改变 会影响实参
* String类型是特殊引用数据类型 作为参数传递 不会影响实参
*/
public class Test1 {
public static void m1(int num){
num+=10;
System.out.println("m1方法中num是值为:" + num);
}
public static void m2(int [] nums){
System.out.println("m2方法中nums的hash值为:" + nums);
System.out.println("-----------------m2方法中打印nums数组元素-----------------");
for(int i = 0;i < nums.length;i++){
nums[i]++;
System.out.print(nums[i] + "\t");
}
System.out.println();
}
public static void m3(String str){
str += "hello";
System.out.println("m3方法中str中的内容为:" + str);
}
public static void main(String[] args) {
String str = "abc";
m3(str);
System.out.println("main方法中str中的内容为:" + str);
System.out.println("----------------------------------------------------");
int a = 10;
m1(a);
System.out.println("main方法中a值为:" + a);
System.out.println("----------------------------------------------------");
int [] arr1 = {1,2,3,4,5};
System.out.println("main方法中arr1的hash值为:" + arr1);
m2(arr1);
System.out.println("main方法中打印arr1数组中的元素");
for(int i = 0;i < arr1.length;i++){
System.out.print(arr1[i] + "\t");
}
System.out.println();
}
}
JVM内存分析图
基本数据类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZD3a4GJK-1661564741176)(JavaSE.assets/1660188946503.png)]
引用数据类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gazcjZ4C-1661564741176)(JavaSE.assets/1660188928253.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CT7JoPTI-1661564741176)(JavaSE.assets/1660188629441.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UKeWSB5V-1661564741177)(JavaSE.assets/1660188819955.png)]
5.6 可变长参数
-
概念:可接收多个同类型实参,个数不限,使用方式与数组相同。
-
语法:数据类型… 形参名
-
使用条件:
-
必须在形参列表的末尾
-
整个形参列表中只能有一个可变长参数
-
package com.atguigu.test9;
/**
* @author WHD
* @description TODO
* @date 2022/8/8 16:26
* 可变长参数:可接受0个或者多个同类型的实参
* 1.必须在形参列表的末尾
* 2.整个形参列表中只能有一个可变长参数
*/
public class Test1 {
public static void m1(int... nums){
for(int i = 0;i < nums.length;i++){
System.out.print(nums[i] + "\t");
}
System.out.println();
System.out.println("m1方法执行完毕");
}
public static void m2(int [] nums){
for(int i = 0;i < nums.length;i++){
System.out.print(nums[i] + "\t");
}
System.out.println();
}
public static void main(String[] args) {
m1(1,2,3,4,5,55,66,99,77);
int [] arr1 = {1,2,3,4,5};
m2(arr1);
}
}
5.7 常见算法
5.7.1 冒泡排序
冒泡排序:相邻的两个数值比较大小,互换位置。
冒泡规则:外层循环n - 1 内层循环 n - 1 -i
外层循环为比较轮数,内层循环比较次数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9QXHRHaB-1661564741178)(JavaSE.assets/1660010570857.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Wun2oZq-1661564741178)(JavaSE.assets/冒泡排序1.gif)]
/**
* @author
* @description TODO
* @date 2022/8/9 9:35
* 冒泡排序:相邻的两个数值比较大小,互换位置。
*
* 冒泡规则:外层循环n - 1 内层循环 n - 1 -i
*/
public class Test1 {
public static void main(String[] args) {
int [] nums = {5,89,11,0,23,56774,885,96,20,36};
int a = 10;
int b = 20;
// int temp = a;
// a = b;
// b = temp;
for(int i = 0;i < nums.length -1;i++){ // 外层循环 控制比较的轮数 n - 1
for(int j = 0; j < nums.length - 1 - i;j++ ){ // 内层循环控制每一轮比较的次数 n - 1 -i
if( nums[j] > nums[j + 1]){
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
System.out.println(Arrays.toString(nums));
}
for(int i = 0;i < nums.length;i++){
System.out.print(nums[i] + "\t");
}
System.out.println();
}
}
5.7.2 选择排序算法
选择排序:固定值与其他值依次比较大小,互换位置。
外层循环属于比较的元素A 内层循环属于比较的元素B
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MSfCLfOD-1661564741179)(JavaSE.assets/1660014529517.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-76EumYE2-1661564741179)(JavaSE.assets/选择排序2.gif)]
/**
* @author
* @description TODO
* @date 2022/8/9 10:31
* 选择排序:固定值与其他值依次比较大小,互换位置。
*
* 赵四 188 广坤 165 小宝 150
*
* 外层循环属于比较的元素A 内层循环属于比较的元素B
*
*
*/
public class Test1 {
public static void main(String[] args) {
int [] nums = {-2,100,5,2,89,11,0,23,56774,885,96,20,36};
for(int i = 0;i < nums.length - 1;i++){
int index = i;
for(int j = i + 1;j < nums.length;j++){
if(nums[index] < nums[j]){
index = j;
}
}
if(index != i){
int temp = nums[index];
nums[index] = nums[i];
nums[i] = temp;
}
System.out.println("第" + (i + 1) + "轮比较完成以后元素的顺序" + Arrays.toString(nums));
}
System.out.println(Arrays.toString(nums));
}
}
5.7.3 杨辉三角
杨辉三角:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
/**
* @author
* @description TODO
* @date 2022/8/9 14:08
* 杨辉三角:(了解)
* 1
* 1 1
* 1 2 1
* 1 3 3 1
* 1 4 6 4 1
* 1 5 10 10 5 1
* 1 6 15 20 15 6 1
*/
public class Test1 {
public static void main(String[] args) {
int [] [] nums = new int[6][6];
for(int i = 0;i < nums.length;i++){
nums[i][0] = 1; // 每一行的第一个元素都赋值为1
nums[i][i] = 1; // 每一行的最后一个元素也赋值为1
if(i > 1){
for(int j = 1;j < i;j++){
nums[i][j] = nums[i -1][j] + nums[i -1][j -1];
}
}
}
for(int i = 0;i < nums.length;i++){
for(int j = nums.length -1;j >= i;j--){
System.out.print(" ");
}
for(int j = 0;j <= i;j++){
System.out.print(nums[i][j] + " ");
}
System.out.println();
}
}
}
5.8 Arrays类常用方法
Arrays工具类常用方法
copyOf(原数组,新长度):复制数组
toString(数组名) :将数组元素转换为字符串
sort(数组名):升序排序
binarySearch(元素值):找到指定元素在数组中的下标 必须先排序
其他打开API自己研究一下
5.9 二维数组
二维数组:数组中的元素还是数组
5.9.1 二维数组的定义方式
1.采用 new 关键字直接创建
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AvyrbBuI-1661564741180)(JavaSE.assets/1660043134769.png)]
2. 从高维开始逐维创建
高维度长度(第一个中括号)必须指定 低维度可以单独指定
public class ArrayTest06 {
public static void main(String[] args) {
//从高维开始逐维创建
int[][] data = new int[2][];
data[0] = new int[2];
data[1] = new int[4];
data[0][0] = 1;
data[0][1] = 2;
data[1][0] = 1;
data[1][1] = 2;
data[1][2] = 3;
data[1][3] = 4;
//输出二维数组
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
System.out.println(data[i][j]);
}
}
}
}
3. 采用初始化语句块创建数组对象
public class ArrayTest07 {
public static void main(String[] args) {
//静态初始化
// 多个数组之间用逗号隔开
int[][] data = {{1,2},{1,2,3,4}};
for (int i=0; i<data.length; i++) {
for (int j=0; j<data[i].length; j++) {
System.out.println(data[i][j]);
}
}
}
}
第六章 初识面向对象
6.1 面向对象的理解
6.1.1 面向过程和面向对象有什么区别?
从语言方面出发:
对于C语言来说,是完全面向过程的。
对于C++语言来说,是一半面向过程,一半是面向对象。(C++是半面向对象的)
对于Java语言来说,是完全面向对象的。
6.1.2 什么是面向过程的开发方式?
面向过程的开发方式主要的特点是:
注重步骤,注重的是实现这个功能的步骤。
第一步干什么
第二步干什么
…
另外面向过程也注重实现功能的因果关系。
因为A所有B
因为B所以C
因为C所以D
…
面向过程中没有对象的概念。只是实现这个功能的步骤以及因果关系。
6.1.3 面向过程有什么缺点?(耦合度高,扩展力差。)
面向过程最主要是每一步与每一步的因果关系,其中A步骤因果关系到B步骤,A和B联合起来形成一个子模块,子模块和子模块之间又因为因果关系结合在一起,假设其中任何一个因果关系出现问题(错误),此时整个系统的运转都会出现问题。(代码和代码之间的耦合度太高,扩展力太差。)
螺栓螺母拧在一起:耦合度高吗?
这是耦合度低的,因为螺栓螺母可以再拧开。(它们之间是有接口的。)
螺栓螺母拧在一起之后,再用焊条焊接在一起,耦合度高吗?
这个耦合度就很高了。耦合度就是黏连程度。
往往耦合度高的扩展力就差。
耦合度高导致扩展力差。(集成显卡:计算机显卡不是独立的,是集成到主板上的)
耦合度低导致扩展力强。(灯泡和灯口关系,螺栓螺母关系)
6.1.4 采用面向过程的方式开发一台计算机会是怎样?
这台计算机将没有任何一个部件,所有的都是融合在一起的。你的这台计算机是一个实心儿的,没有部件的。一体机。假设这台一体机的任何一个“部位”出问题,整个计算机就不能用了,必须扔掉了。(没有对象的概念。)
6.1.5 采用面向对象的方式开发一台计算机会是怎样?
内存条是一个对象
主板是一个对象
CPU是一个对象
硬盘是一个对象
然后这些对象组装在一起,形成一台计算机。
假设其中CPU坏了,我们可以将CPU拆下来,换一个新的。
6.1.6 面向过程有什么优点?(快速开发)
对于小型项目(功能),采用面向过程的方式进行开发,效率较高。不需要前期进行对象的提取,模型的建立,采用面向过程方式可以直接开始干活。一上来直接写代码,编写因果关系。从而实现功能。
6.1.7 什么是面向对象的开发方式?
采用面向对象的方式进行开发,更符合人类的思维方式。(面向对象成为主流的原因)
人类就是以“对象”的方式去认识世界的。
所以面向对象更容易让我们接受。
面向对象就是将现实世界分割成不同的单元,然后每一个单元都实现成对象,然后给一个环境驱动一下(行为方法驱动),让各个对象之间协作起来形成一个系统。
对象“张三”
对象“香烟”
对象“打火机”
对象“吸烟的场所”
然后将以上的4个对象组合在一起,就可以模拟一个人的抽烟场景。
其中“张三”对象可以更换为“李四”
其中“香烟”也可以更换品牌。
其中“打火机”也可以更换。
其中“吸烟的场所”也可以更换。
采用面向对象的方式进行开发:耦合度低,扩展力强
。
6.1.8 找一个合适的案例。说明一下面向对象和面向过程的区别?
蛋炒饭:
鸡蛋和米饭完全混合在一起。没有独立对象的概念。
假设客户提出新需求:我只想吃蛋炒饭中的米饭,怎么办?
客户提出需求,软件开发者必须满足这个需求,于是
开始扩展,这个软件的扩展是一场噩梦。(很难扩展,耦合度太高了。)
盖饭:
老板,来一份:鱼香肉丝盖饭
鱼香肉丝是一道菜,可以看成一个独立的对象。
米饭可以看成一个独立的对象。
两个对象准备好之后,只要有一个动作,叫做:“盖”
这样两个对象就组合在一起了。
假设客户提出新需求:我不想吃鱼香肉丝盖饭,想吃西红柿鸡蛋盖饭。
这个扩展就很轻松了。直接把“鱼香肉丝”对象换成“西红柿鸡蛋”对象。
6.1.9 所关注的东西不同
面向过程主要关注的是:实现步骤以及整个过程。
面向对象主要关注的是:对象A,对象B,对象C,然后对象ABC组合,或者CBA组合…
6.2面向对象的概述
6.2.1万物皆对象
自然界中的任何事物都可以理解为一个对象。通过分析其特征和行为,将其描述的具体。
6.2.2. 特征和属性
一类事物共有的信息,称之为特征,在Java类中使用属性描述。
6.2.3. 行为和方法
一类事物共有的动作,称之为行为,在Java类中使用方法描述。
6.2.4. 类和对象的关系
类是对象的抽象,对象是类的具体。
类是一个抽象概念,比如"人类"
对象是具体的概念,比如"赵四,广坤,你的同桌,好基友……"
6.2.5. 创建类和对象
通过分析当前这一类事物共有的特征和行为,分别通过属性和方法描述。
6.3类和对象的概念
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KYGGepH9-1661564741180)(JavaSE.assets/1660137892016.png)]
6.3.1 什么是类
- 类实际上在现实世界当中是不存在的,是一个抽象的概念。类是一个模板,是我们人类大脑进行“思考、总结、抽象”的一个结果。(主要是因为人类的大脑不一般才有了类的概念。)
- 类本质上是现实世界当中某些事物具有共同特征,将这些共同特征提取出来形成的概念就是一个“类”,“类”就是一个模板。
类 = 属性 + 方法
属性来源于:状态
方法来源于:动作类就是一个模板,抽象了一个共同特征,对象是实际存在的个体
一个类描述了所有对象的共同特征(属性+方法)
1.怎么定义一个类,语法格式是什么?
[修饰符列表] class 类名 {
//类体 = 属性 + 方法
// 属性在代码上以“变量”的形式存在(描述状态)
// 方法描述动作/行为
}
注意:修饰符列表可以省略。
6.3.2 什么是对象
- 对象是实际存在的个体。(真实存在的个体)
- 在java语言中,要想得到“对象”,必须先定义“类”,“对象”是通过“类”这个模板创造出来的。
- 类就是一个模板:类中描述的是所有对象的“共同特征信息”,对象就是通过类创建出的个体。
类:是不存在的,人类大脑思考总结的一个描述“共同特征信息”的模板。
对象:实际存在的个体。
实例:对象还有另一个名字叫做实例。
实例化:通过类这个模板创建对象的过程,叫做:实例化。
抽象:多个对象具有共同特征,进行思考总结抽取共同特征的过程。类 --【实例化】–> 对象(实例)
对象 --【抽象】–> 类
6.3.3 对象的创建和使用
1.对象和引用的区别?
对象是new出来的,在堆内存中存储。
引用是变量,在栈内存中存储,并且该变量中保存了内存地址指向了堆内存当中的对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8M0II8N8-1661564741181)(JavaSE.assets/1660144388876.png)]
2.创建一个对象时内存的变化过程
- 当程序运行时(java 类名 执行),类加载器将class字节码文件加载到方法区当中存储
- 代码片段加载完成之后,会先执行main方法,此时会给main方法分配空间,main方法入栈。
- 然后执行new student对象,当new student对象的时候,堆内存会分配一块空间给student对象
- 当创建对象之后系统会调用构造方法自动给该实例的实例变量赋默认值,实例变量也存储在堆当中
- 执行等号左边,栈会给s1引用(局部变量)分配空间,用来存储对象的地址。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cz6RgGm1-1661564741181)(JavaSE.assets/1660143315178.png)]
为什么s1引用存储的是对象内存地址,却能通过它访问实例变量?
因为s1保存了对象的内存地址,通过地址java虚拟机会寻址到这个空间上,然后从这个空间上再找no、找name
以下代码的内存图
public class User {
String name;
int id ;
Address address;
}
public class Address {
String city;
String street;
String zipCode;
}
public class Test1 {
public static void main(String[] args) {
Address address = new Address();
address.city = "深圳宝安区";
address.zipCode = "10000";
address.street = "航城街道";
System.out.println(address.city);
System.out.println(address.street);
System.out.println(address.zipCode);
User user = new User();
user.id = 10;
user.name = "张三";
user.address = address;
System.out.println(user.id);
System.out.println(user.name);
System.out.println(user.name+"的城市所在"+user.address.city);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eXaaxsBo-1661564741182)(JavaSE.assets/1660183482057.png)]
3.空指针异常
空指针异常是指:“空引用”访问实例(对象)相关的数据,都会出现空指针异常
正常new对象指定内存空间的内存图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kVZ5q9GA-1661564741182)(JavaSE.assets/1660186507467.png)]
将Customer c赋值为null时的内存图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fU4D2dL4-1661564741183)(JavaSE.assets/1660186626695.png)]
关于垃圾回收器:GC
在java语言中,垃圾回收器主要针对的是堆内存。当一个java对象没有任何引用指向该对象的时候,GC会考虑将该垃圾数据释放回收掉。
以下代码为什么编译时期不会报错?
Customer c = new Customer(); c = null; System.out.println(c.id);
因为编译器只检查语法,编译器发现c是Customer类型,Customer类型中有id属性,所以可以:c.id。语法过了。但是运行的时候需要对象的存在,但是对象没了,尴尬了,就只能出现一个异常。
6.4 实例变量与局部变量的区别
实例变量必须先采用“引用.”的方式去访问!!!
什么是实例变量?
对象又被称为实例。
实例变量实际上就是:对象级别的变量。
public class 明星类{
double height;
}
身高这个属性所有的明星对象都有,但是每一个对象都有“自己的身高值”。
假设创建10个明星对象,height变量应该有10份。
所以这种变量被称为对象级别的变量。属于实例变量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VlpcMIgS-1661564741183)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day08-%E7%AC%94%E8%AE%B0/img/%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F%E5%AE%9E%E4%BE%8B%E5%8F%98%E9%87%8F%E5%8C%BA%E5%88%AB.png)]
实例变量/属性/字段 | 局部变量 | |
---|---|---|
存储位置 | 实例变量依托与对象而存在 而对象存在堆中 所以实例变量全部存在堆中 | 基本数据类型存在栈中 引用数据类型名字在栈 值在堆 |
生命周期 | 随着对象的创建而存在(有默认值) 随着对象被垃圾回收而死亡 | 随着方法的入栈而生效,随着方法的出栈而死亡 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lOp5bBUG-1661564741184)(…/…/JavaEE%E8%AF%BE%E4%BB%B6/%E7%AC%94%E8%AE%B0/day08-%E7%AC%94%E8%AE%B0/img/%E5%AF%B9%E8%B1%A1%E4%BB%A5%E5%8F%8A%E5%B1%9E%E6%80%A7%E5%86%85%E5%AD%98%E4%BD%8D%E7%BD%AE.png)]
6.5 构造方法
构造方法:用于构建创造对象,以及实例变量初始化的特殊方法
语法格式:访问权限修饰符 + 类名(形参列表){}
普通方法:访问权限修饰符 + 返回值类型 + 方法名(形参列表){}
在编写代码过程中关于idea提示字母表示含义:
f : field 字段–属性–实例变量
m : method 方法
c : constructor 构造方法
p : parameter 参数
6.5.1 构造方法重载
构造方法重载:同一个类中的构造方法,参数列表不同
6.5.2 无参构造特点
构造方法不能通过点调用 无参构造是JVM默认提供 如果书写了有参构造方法 无参构造将被覆盖 如需使用 必须显式书写
当一个类中没有提供任何构造方法,系统默认提供一个无参数的构造方法。这个无参数的构造方法叫做缺省构造器。
当一个类中手动的提供了构造方法,那么系统将不再默认提供无参数构造方法。建议将无参数构造方法手动的写出来。
- 缺省构造器实际上有默认的给实例变量赋值的操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ilv41PVC-1661564741184)(JavaSE.assets/1660198497209.png)]
我们定义无参构造方法的时候,看上去构造方法没有任何代码赋值,实际上系统会给他赋默认值并且隐藏了这个操作。这三行代码是看不见的。
6.6 this关键字
this关键字:表示当前对象 (属于一个隐式参数)
可以访问本类的:属性、方法、构造方法
访问属性:this.属性名
访问方法:this.方法名()
访问构造:this(实参); this访问构造方法 必须在本类构造方法的第一句
我们在创建的对象会访问构造方法,但是访问构造方法并不会创建对象,只是用于属性的初始化
第七章 面向对象的三大特性
7.1 封装
7.1.1 封装的概念
现实生活中有很多现实的例子都是封装的,例如:
手机,电视机,笔记本电脑,照相机,这些都是外部有一个坚硬的壳儿。封装起来,保护内部的部件。保证内部的部件是安全的。另外封装了之后,对于我们使用者来说,我们是看不见内部的复杂结构的,我们也不需要关心内部有多么复杂,我们只需要操作外部壳儿上的几个按钮就可以完成操作。
将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问
7.1.2 不封装有什么问题
实例变量对外暴露,可以在外部程序中随意访问,导致不安全。
7.1.3 封装的好处
- 便于使用者正确使用系统,防止错误修改属性
- 降低了构建大型系统的风险
- 提高程序的可重用性
- 降低程序之间的耦合度
7.1.4 封装的作用
- 保证内部结构的安全。
- 屏蔽复杂,暴露简单。
7.1.5 封装的步骤:属性私有 方法公开
-
属性私有:将属性使用private修饰 表示当前属性只能在本类中访问
-
方法公开:针对每一个属性书写两个方法 get & set 方法 get方法用于获取属性值 set方法用于设置属性值
7.2 访问权限修饰符
7.2.1 类的访问权限修饰符
类的访问修饰符:
public:本项目中任何位置都可以访问
默认不写:包级别的访问权限 只能在本包中访问
7.2.2 类成员的访问权限修饰符
类成员的访问权限修饰符:
private:本类
默认不写:本包
protected:本包以及不在同包的子类
public:任何位置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cmlTql0i-1661564741185)(…/…/%E5%B0%9A%E7%A1%85%E8%B0%B7javaEE/%E7%AC%94%E8%AE%B0/JavaSE.assets/%E8%AE%BF%E9%97%AE%E4%BF%AE%E9%A5%B0%E7%AC%A62-1660130016621.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ey3odqS-1661564741185)(…/…/%E5%B0%9A%E7%A1%85%E8%B0%B7javaEE/%E7%AC%94%E8%AE%B0/JavaSE.assets/%E8%AE%BF%E9%97%AE%E4%BF%AE%E9%A5%B0%E7%AC%A61-1660130007851.png)]
7.3 继承
7.3.1 不使用继承有什么缺点?
代码臃肿。代码没有得到重复利用。
使用继承的优点:代码得到重复利用。
使用继承的缺点:代码的耦合度增加了。
7.3.2 继承的作用
- 基本作用:子类继承父类,代码可以得到复用。(这个不是重要的作用,是基本作用。)
- 主要(重要)作用:因为有了继承关系,才有了后期的方法覆盖和多态机制。
7.3.3 继承的相关特性
-
B类继承A类,则称A类为
超类(superclass)、父类、基类
,B类则称为子类(subclass)、派生类、扩展类
。
class A{}
class B extends A{}
我们平时聊天说的比较多的是:父类和子类。
superclass 父类
subclass 子类 -
java 中的
继承只支持单继承,不支持多继承
,C++中支持多继承,这也是 java 体现简单性的一点,换句话说,java 中不允许这样写代码: class B extends A,C{ } 这是错误的。
-
虽然 java 中
不支持多继承
,但有的时候会产生间接继承的效果,例如:class C extends B,class B extends A,也就是说,C 直接继承 B,其实 C 还间接继承 A。 -
java 中规定,子类继承父类,
除构造方法和被 private 修饰的数据不能继承外
,剩下都可以继承。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O8dRuaWC-1661564741186)(JavaSE.assets/1660282260976.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GowXMHj1-1661564741186)(JavaSE.assets/子类不能继承内容-1660367706441.png)]
-
java 中的类没有显示的继承任何类,则默认继承 Object类,Object类是 java 语言提供的根类(老祖宗类),也就是说,一个对象与生俱来就有 Object类型中所有的特征。
-
继承也存在一些缺点,例如:CreditAccount 类继承 Account 类会导致它们之间的
耦合度非常高
,Account 类发生改变之后会马上影响到 CreditAccount 类 -
当创建子类对象时,会先去调用父类的无参构造方法将父类的成员变量进行初始化,没有创建父类的对象。
子类实例化是没有在堆内存中开辟父类对象空间的,构造方法只是用来初始化成员变量的。开辟堆内存空间必须使用new指令或者反射或者序列化,new + constructor,可以看成是开辟空间,然后初始化。
7.3.4 方法覆盖
1.什么是方法覆盖
方法覆盖又叫做:方法重写(重新编写),英语单词叫做:Override、Overwrite,都可以。比较常见的:方法覆盖、方法重写、override
当子类对父类继承过来的方法进行“方法覆盖”之后,子类对象调用该方法的时候,一定执行覆盖之后的方法。
2.什么时候考虑使用方法覆盖?
父类中的方法无法满足子类的业务需求,子类有必要对继承过来的方法进行覆盖。
3.什么条件满足的时候构成方法覆盖?
- 有继承关系的两个类
- 具有相同方法名、返回值类型(子类)、形式参数列表
- 访问权限不能更低,可以更高。
- 抛出异常不能更多,可以更少。
注意1:方法覆盖只是针对于方法,和属性无关。
注意2:私有方法无法覆盖。
经过测试:会输出父类的私有方法
注意3:构造方法不能被继承,所以构造方法也不能被覆盖。
注意4:方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。
对于返回值类型是基本数据类型来说,必须一致。
对于返回值类型是引用数据类型来说,重写之后返回值类型可以变的更小(但意义不大,实际开发中没人这样写。)
4.方法重载和方法覆盖有什么区别?
- 方法重载发生在同一个类当中。
- 方法覆盖是发生在具有继承关系的父子类之间。
- 方法重载是一个类中,方法名相同,参数列表不同。
- 方法覆盖是具有继承关系的父子类,并且重写之后的方法必须和之前的方法一致:方法名一致、参数列表一致、返回值类型一致。
7.4 多态
7.4.1 什么是多态?
多态:指的是同一个方法调用,由于对象不同,可能会有不同的行为。(现实生活中,同一个方法,具体的实现会不一样)同一个事物,因为不同的条件/环境
产生不同的效果
程序中的多态:父类引用指向子类对象 同一个引用类型
使用不同的实例
执行不同的操作
多种形态,多种状态。编译的时候是一种形态,运行的时候是另一种形态。
父类型引用指向子类型对象。
包括编译阶段和运行阶段。
编译阶段:编译期叫做静态绑定
,绑定父类的方法。
运行阶段:运行期叫做动态绑定
,动态绑定子类型对象的方法。
多态存在的条件
-
多态是方法的多态,属性没有多态性
-
多态的存在有三个必要条件(继承、方法重写、父类引用子类对象)。
-
父类引用指向子类对象后,用该父类引用子类重写的方法,此时多态就出现了。
7.4.2 多态的基础语法
向上转型:子—>父 (upcasting)
又被称为自动类型转换:Animal a = new Cat();
向下转型:父—>子 (downcasting)
又被称为强制类型转换:Cat c = (Cat)a; (需要添加强制类型转换符)
**注意:**无论是向上转型,还是向下转型,两种类型之间必须有继承关系
,没有继承关系编译器报错。只要有继承关系,使用向上转型和向下转型都可以编译通过,但是运行就不一定了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WWJKd26d-1661564741187)(JavaSE.assets/1660388820855.png)]
向上转型具体的表现
-
父类作为形参,子类作为实参
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Do91HL6-1661564741187)(JavaSE.assets/1660402489294.png)]
-
父类作为定义返回值,子类作为实际返回值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j3uzXnJr-1661564741188)(JavaSE.assets/1660406238142.png)]
-
数组/集合定义为父类类型的,实际元素为子类类型
7.4.3 多态的底层实现
1. 多态的分类
同一个事物,因为条件/环境的不同,产生的不同的效果
静态多态:方法重载 在编译期间就可以根据我们传入的实参 决定具体调用哪个方法 这种方式 就是静态多态 (程序在编译期间产生的多态,方法重载,在编译期间就能够确定要调用哪个同名的方法,因为传的参数只能匹配到唯一的一个与之相符的方法。我们调用同一个方法,只是传的参数不一样,会产生不一样的效果,所以方法重载也是多态)
动态多态:方法重写 父类引用指向子类对象 随着代码的执行 动态的决定具体调用哪个子类重写父类的方法 这个过程就是动态多态 (随着程序的执行而动态的去决定方法调用的过程)
2."实方法"和虚方法
实方法:表示在编译期间就可以确定调用此方法的对象是谁,以及调用的是哪个方法,这样的方法称之为实方法
比如: static修饰的方法 private修饰的方法
(具体是哪个对象调的在编译期间可以确定,具体调用哪个方法在编译期间也能确定。因为不能重写,所以不管你是用哪个类调的,永远都是这一个方法。)
虚方法:在编译期间无法确定具体调用哪个方法的这种方法 就属于虚方法
比如:实例方法
(这个new 对象在程序执行过程中动态的在堆中创建对象,这个对象是哪个子类,在编译期间是无法确定的。唯一能确定的是这个对象属于引用类型的子类或者是本类,而一但不确定了,这个方法就是一个虚拟的方法,只有在程序执行过程中才知道要new的是哪个对象,要去方法区的方法表中找这个实际的对象所对应的方法。编译期间只看左侧的类型,因为无法确定右侧实际是什么类型的,一旦不能确定,就不知道子类有哪些独有的方法。所以只要你是左侧类型的,就肯定能调用左侧类型的方法。只有在程序执行过程中,才知道你具体new的是哪个对象。确定你new的是哪个子类以后,才可以知道你具体调用的是哪个子类重写的方法)
实方法JVM底层调用指令:
invokespecial
invokestatic
虚方法JVM底层调用指令:
invokevirtual
invokeinterface
3. 静态绑定和动态绑定
绑定:是值对象和方法之间的绑定
静态绑定:是指编译期间就可以确定调用此方法的对象是谁 这种就属于静态绑定
动态绑定:在运行期间才能确定调用此方法的对象是谁 这种属于动态绑定
4.方法表
每个类信息文件中 都维护一个方法表 方法表存在于方法区中 相当于是一个数组
父类方发表与子类方法表的下标都是一样的,比如你要调用toString(),就用0下标找到toString(),先从Pet类里面找,如果子类没有重写就再拿0下标去父类Object类中找。因为下标一样。
首先加载类信息文件,加载文件之后就能确定能使用哪些方法,但是此时还未new对象,没有new对象就不能确定能调用哪些方法。现在new了一个dog对象,在dog方法表中找到重写的print()方法,确定是dog类重写的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ET81vLmH-1661564741188)(JavaSE.assets/多态方法调用过程.png)]
向上转型举个例子:
public class Test01{
public static void main(String[] args){
Animal a1 = new Animal();
a1.move(); //动物在移动!!!
Cat c1 = new Cat();
c1.move(); //cat走猫步!
Bird b1 = new Bird();
b1.move(); //鸟儿在飞翔!!!
/*
1、Animal和Cat之间有继承关系吗?有的。
2、Animal是父类,Cat是子类。
3、Cat is a Animal,这句话能不能说通?能。
4、经过测试得知java中支持这样的一个语法:
父类型的引用允许指向子类型的对象。
Animal a2 = new Cat();
a2就是父类型的引用。
new Cat()是一个子类型的对象。
允许a2这个父类型引用指向子类型的对象。
*/
Animal a2 = new Cat();
Animal a3 = new Bird();
a2.move(); //cat走猫步!
a3.move(); //鸟儿在飞翔!!!
}
}
// 动物类:父类
public class Animal{
// 移动的方法
public void move(){
System.out.println("动物在移动!!!");
}
}
// 鸟儿类,子类
public class Bird extends Animal{
// 重写父类的move方法
public void move(){
System.out.println("鸟儿在飞翔!!!");
}
}
// 猫类,子类
public class Cat extends Animal{
// 对move方法进行重写
public void move(){
System.out.println("cat走猫步!");
}
}
分析:a2.move();
java程序分为编译阶段和运行阶段。
- 先来分析编译阶段:
- 对于编译器来说,编译器只知道a2的类型是Animal,所以编译器在检查语法的时候,会去Animal.class字节码文件中找move()方法,找到了,绑定上move()方法,编译通过,静态绑定成功。(编译阶段属于静态绑定。)
- 再来分析运行阶段:
- 运行阶段的时候,实际上在堆内存中创建的java对象是Cat对象,所以move的时候,真正参与move的对象是一只猫,所以运行阶段会
动态执行
Cat对象的move()方法。这个过程属于运行阶段绑定。(运行阶段属于动态绑定。)
- 运行阶段的时候,实际上在堆内存中创建的java对象是Cat对象,所以move的时候,真正参与move的对象是一只猫,所以运行阶段会
程序分编译阶段和运行阶段,编译阶段只知道a2的类型是Animal,还不能new对象,所以从Animal里面找move方法,找到了就静态绑定成功了,编译通过。开始运行,运行的时候等号右边会把对象new出来,此时和堆内存的实际对象有关,真正执行的时候会自动调用“堆内存中真实对象” 的相关方法。所以运行的时候就会动态的调用底层是什么对象,那就是什么对象在移动。真正负责移动的是对象,而不是引用。
向下转型举个例子:
public class Test01{
public static void main(String[] args){
Animal a5 = new Cat(); // 底层对象是一只猫。
//错误: 找不到符号
x.catchMouse(); //猫正在抓老鼠!!!!
}
}
// 猫类,子类
public class Cat extends Animal{
// 对move方法进行重写
public void move(){
System.out.println("cat走猫步!");
}
// 猫除了move之外,应该有自己特有的行为,例如抓老鼠。
// 这个行为是子类型对象特有的方法。
public void catchMouse(){
System.out.println("猫正在抓老鼠!!!!");
}
}
- 分析程序一定要分析编译阶段的
静态绑定
和运行阶段的动态绑定
。只有编译通过的代码才能运行。没有编译,根本轮不到运行。- 因为编译器只知道a5的类型是Animal,去Animal.class文件中找catchMouse()方法结果没有找到,所以静态绑定失败,编译报错。无法运行。(语法不合法。)
- 所以,这里需要调用子类
特有
的方法,就需要“向下转型”
public class Test01{
public static void main(String[] args){
Animal a6 = new Bird(); //表面上a6是一个Animal,运行的时候实际上是一只鸟儿。
/*
运行是出现异常,这个异常和空指针异常一样非常重要,也非常经典:
java.lang.ClassCastException:类型转换异常。
*/
Cat y = (Cat)a6;
y.catchMouse();
}
}
// 鸟儿类,子类
public class Bird extends Animal{
// 重写父类的move方法
public void move(){
System.out.println("鸟儿在飞翔!!!");
}
// 也有自己特有的方法
public void sing(){
System.out.println("鸟儿在歌唱!!!");
}
}
分析以下程序,编译报错还是运行报错???
-
编译阶段,编译器检测到a6这个引用是Animal类型,而Animal和Cat之间存在继承关系,所以可以向下转型。编译没毛病。
-
运行阶段,堆内存实际创建的对象是:Bird对象。在实际运行过程中,拿着Bird对象转换成Cat对象就不行了。因为Bird和Cat之间没有继承关系。
7.4.4 什么时候必须使用“向下转型”?
需要调用或者执行子类对象中“特有”的方法
。必须进行向下转型,才可以调用。
7.4.5 instanceof运算符
instanceof :(运行阶段动态判断)表示判断左侧的实例是否属于右侧的类型,是为true,不是为false
第一:instanceof可以在运行阶段动态判断引用指向的对象的类型
。
第二:instanceof的语法:(引用 instanceof 类型)
第三:instanceof运算符的运算结果只能是:true/false
第四:c是一个引用,c变量保存了内存地址指向了堆中的对象。
假设(c instanceof Cat)为true表示:
c引用指向的堆内存中的java对象是一个Cat。
假设(c instanceof Cat)为false表示:
c引用指向的堆内存中的java对象不是一个Cat。
程序员要养成一个好习惯:
任何时候,任何地点,对类型进行向下转型时,一定要使用
instanceof 运算符进行判断。(java规范中要求的。)
这样可以很好的避免:ClassCastException
7.4.6 多态在开发中的作用
-
软件在扩展新需求过程当中,修改Master这个类有什么问题?
一定要记住:软件在扩展过程当中,修改的越少越好。
修改的越多,你的系统当前的稳定性就越差,未知的风险就越多。
-
其实这里涉及到一个软件的开发原则:
软件开发原则有七大原则(不属于java,这个开发原则属于整个软件业):
其中有一条最基本的原则:
OCP(开闭原则)
-
什么是开闭原则?
对扩展开放(你可以额外添加,没问题),对修改关闭(最好很少的修改现有程序)。
在软件的扩展过程当中,修改的越少越好。
-
-
多态在开发中的作用是:
降低程序的耦合度,提高程序的扩展力。
(也是OCP的目的)向上转型是为了提升程序的灵活性,向下转型是为了实用性。
有继承才有方法重写,有方法重写才有多态
public class Master{
public void feed(Dog d){}
public void feed(Cat c){}
}以上的代码中表示:Master和Dog以及Cat的关系很紧密(耦合度高)。导致扩展力很差。
public class Master{
public void feed(Pet pet){
pet.eat();
}
}
以上的代码中表示:Master和Dog以及Cat的关系就脱离了,Master关注的是Pet类。
这样Master和Dog以及Cat的耦合度就降低了,提高了软件的扩展性
。
// 主人类
public class Master{
/*
// 假设主人起初的时候只是喜欢养宠物狗狗
// 喂养宠物狗狗
public void feed(Dog d){
d.eat();
}
// 由于新的需求产生,导致我们“不得不”去修改Master这个类的代码
public void feed(Cat c){
c.eat();
}
*/
// 能不能让Master主人这个类以后不再修改了。
// 即使主人又喜欢养其它宠物了,Master也不需要修改。
// 这个时候就需要使用:多态机制。
// 最好不要写具体的宠物类型,这样会影响程序的扩展性。
public void feed(Pet pet){
// 编译的时候,编译器发现pet是Pet类,会去Pet类中找eat()方法,结果找到了,编译通过
// 运行的时候,底层实际的对象是什么,就自动调用到该实际对象对应的eat()方法上。
// 这就是多态的使用。
pet.eat();
}
}
// 宠物狗狗类
public class Dog extends Pet{
// 吃
public void eat(){
System.out.println("狗狗喜欢啃骨头,吃的很香。");
}
}
public class Cat extends Pet{
// 吃
public void eat(){
System.out.println("猫咪喜欢吃鱼,吃的很香!!!");
}
}
第八章 static和this关键字
8.1 static关键字
8.1.1 概述
- 所有static关键字修饰的都是类相关的,类级别的。
- 所有static修饰的,都是采用“类名.”的方式访问。
- static修饰的变量:静态变量
- static修饰的方法:静态方法
static不能在方法中修饰变量
8.1.2 变量的分类
变量根据声明的位置进行划分:
- 在方法体当中声明的变量叫做:局部变量。
- 在方法体外声明的变量叫做:成员变量。
- 成员变量又可以分为:实例变量、静态变量
8.1.3 对象和类相关的访问方式
对象相关的方法和属性在访问时,都需要采用“引用.”的方式访问,需要先new对象
- 类相关(static修饰)的方法和属性在访问时,需要采用“类名.”的方式访问,不需要new对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3r0ImTk1-1661564741189)(JavaSE.assets/1660202678329.png)]
实例的:一定需要使用“引用.”来访问。
静态的:建议使用“类名.”来访问,但使用“引用.”也行。在程序运行的过程中也会把引用变成“类名.”(不建议使用"引用.")。
结论:
空指针异常只有在什么情况下才会发生呢?
只有在“空引用”访问“实例”相关的,都会出现空指针异常。
8.1.4 静态和实例变量在什么时候赋默认值?
1.静态变量(在类加载时机初始化)
静态变量在类加载时初始化(与实例变量的默认值相同),不需要new对象,静态变量的空间就开出来了。所以可以通过类名直接调用而不需要对象。
被static修饰的属性,不属于任何对象属于整个类,可以被当前类的所有对象共享,在内存中只存在一份
静态变量存在于方法区中的静态区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mz6o7x1H-1661564741189)(JavaSE.assets/1660205938821.png)]
JDK的方法区只是一个统称,具体到每个版本名称不同。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oJb4jMu8-1661564741190)(JavaSE.assets/static关键字内存图.png)]
首先,不管new再多的对象,只要是同一个对象类加载器都只加载一次。通过md5加密算法,来计算当前类的字符串文件有没有改变生成一个全球唯一的值,如果改变了就重新加载如果没有改变就不再加载。都把他们加载到方法区当中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FaRuhwFP-1661564741190)(JavaSE.assets/1660309390858-1660367797898.png)]
为什么静态方法不能直接调用实例相关呢?
因为类加载时,对象还没来得及存在(创建),只要类加载了,静态相关的就能使用了而实例相关还不能用。
2.实例变量(在创建对象时机初始化)
- 语法层面:因为实例变量不能通过类名.属性名访问,所以实例变量不是在类加载的时候赋值的。
- 内存层面:只有在new对象时才会在堆中开辟对象的内存空间,从而进一步在内存空间里开辟实例变量的内存空间。new对象时会调用构造方法进行初始化赋值,
所以实例变量是在创建对象(构造方法执行)的过程中完成初始化赋值的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oC0VNXuo-1661564741191)(JavaSE.assets/1660198497209.png)]
8.1.5 实例和静态有什么区别?
1.方法模拟的是对象的行为动作
-
实例方法表示是对象级别的方法,应该是一个对象级别的行为。
例如:科比和你都会打篮球,但是他打的很好,你打的很一般,你们两(两个不同的对象)的行为是不一样的。但是你们都有一个共同的行为都是打篮球。所以打篮球应该是有一个具体的人去打,因为每个人打的效果都不一样。
例如:张三考试,得分90,李四考试,得分100。不同的对象参加考试的结果不同。我们可以认定“考试”这个行为是与对象相关的行为。建议将“考试”这个方法定义为实例方法。
-
带static的方法是类级别的方法,不需要对象参与。
从语法来说,也可以说是当这个方法不需要处理实例变量的时候就可以定义为静态方法。如果需要处理实例变量,还定义为静态的方法,程序就会报错。因为在调用时不需要创建对象就能调用这个静态方法,根本没有对象参与进来,而实例变量又需要对象参与,所以就会报无法从静态上下文访问非静态变量。
2.属性模拟的是对象的状态
-
实例变量表示是对象级别的变量,应该是一个对象级别的状态。会随着对象的改变值发生变化。
例如:科比和你都有身高这个状态,但是他身高2米,你身高1米8,你们两个的状态都是不一样的,所以每个对象的状态都是不同的,一个对象一份。
-
带static的变量是类级别的变量,不需要对象的参与。所有对象都共享一个属性。
例如:定义一个中国人类,国籍永远都是固定值是相同的,不会随着对象的变化而变化,显然在这个类中国籍属于整个类的特征,不是属于某个对象。如果把这个属性定义为实例变量,就有点浪费空间了。
例如:100L接水机,对于每个人来说接一个共享的饮水机,都会减少2L。
public class StaticTest02{ public static void main(String[] args){ Chinese c1 = new Chinese("1231456456456456","张三","中国"); Chinese c2 = new Chinese("7897897896748564","李四","中国"); } } // 定义一个类:中国人 class Chinese{ // 身份证号 // 每一个人的身份证号不同,所以身份证号应该是实例变量,一个对象一份。 String idCard; // 姓名 // 姓名也是一个人一个姓名,姓名也应该是实例变量。 String name; // 国籍 // 对于“中国人”这个类来说,国籍都是“中国”,不会随着对象的改变而改变。 // 显然国籍并不是对象级别的特征。 // 国籍属于整个类的特征。整个族的特征。 //修改之前 String country; //修改之后 static String country; public Chinese(){ } public Chinese(String s1,String s2, String s3){ idCard = s1; name = s2; country = s3; }
}
不加static之前的内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXO84PZv-1661564741191)(JavaSE.assets/1660205512683.png)]
加static之后的内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWw358YJ-1661564741192)(JavaSE.assets/1660205938821.png)]
定义这个实例变量本来会在堆内存单独占两块内存空间,现在定义为静态变量,则只占方法区一块内存空间。
#### 8.1.6 什么时候声明为静态,什么时候声明为实例
##### 1.什么时候声明为静态变量,什么时候声明为实例变量
如果这个类型的所有对象的某个属性值都是一样的,不建议定义为实例变量,浪费内存空间。
建议定义为类级别特征,定义为静态变量,在方法区中只保留一份,节省内存开销。
> 一个对象一份的是实例变量。
> 所有对象一份的是静态变量。
##### 2.什么时候声明为静态方法,什么时候声明为实例方法
参考标准:
- `当这个方法体当中,直接访问了实例变量,这个方法一定是实例方法。`
- 我们以后开发中,大部分情况下,如果是`工具类`的话,工具类当中的方法一般都是静态的。(静态方法有一个优点,是不需要new对象,直接采用类名调用,极其方便。工具类就是为了方便,所以工具类中的方法一般都是static的。)
#### 8.1.7 静态代码块
##### 1.什么是静态代码块
```java
static {
java语句;
java语句;
}
静态代码块一般是按照自上而下的顺序执行。
注意:静态代码块在类加载时执行,并且在main方法执行之前执行。
2.static静态代码块在什么时候执行呢?
类加载时执行。并且只执行一次。
静态代码块有这样的特征/特点。
3.静态代码块有啥作用,有什么用?
- 静态代码块不是那么常用。(不是每一个类当中都要写的东西。)
- 静态代码块这种语法机制实际上是SUN公司给我们java程序员的一个特殊的时刻/时机。这个时机叫做:
类加载时机
。
4.静态代码块的执行顺序
-
静态变量在类加载时初始化。静态代码块在类加载的时候执行。他们两个的执行时机是一样的。所以会有先后代码执行顺序的区别。
// 静态变量在什么时候初始化?类加载时初始化。 // 静态变量存储在哪里?方法区 static int i = 100; // 静态代码块什么时候执行?类加载时执行。 static { // 这里可以访问i吗? System.out.println("i = " + i); }
-
静态代码块不能访问实例变量,因为类加载时,实例变量的空间还没有开辟出来。实例变量的空间是在对象创建时的时候开辟的。
5.总结有顺序要求的有哪些?
- 对于一个方法来说,方法体中的代码是有顺序的,遵循自上而下的顺序执行。
- 静态代码块1和静态代码块2是有先后顺序的。
- 静态代码块和静态变量是有先后顺序的。先加载完静态变量才能在代码块中访问。
- 赋值语句是从右到左执行的。其他语句默认是从左到右执行。
8.1.8 实例代码块
1.什么是实例代码块
{
java语句;
java语句;
java语句;
}
2.实例语句块执行时机
只要是构造方法执行,必然在构造方法执行之前,自动执行“实例语句块”中的代码。实际上这也是SUN公司为java程序员准备一个特殊的时机,叫做对象构建时机。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mAS5duNa-1661564741192)(JavaSE.assets/1660218113981.png)]
3.作用
如果几个重载的构造方法前有许多相同的代码,则可以将这些公共的代码提取到实例代码块中,这样在每次构造方法执行之前都会执行实例代码块。
8.2 this关键字
8.2.1 this是什么
- this是一个变量,是一个引用 ,一个对象一个this,this是存在堆内存的对象里面
- this保存当前对象的内存地址,指向自身。所以,严格意义上来说,this代表的就是“
当前对象
” - this关键字大部分都可以省略
this在内存中是这样存储的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LrMPE23n-1661564741193)(JavaSE.assets/1660220084596.png)]
8.2.2 this的使用
-
this是一个实例方法里的隐式参数,谁调用这个方法,这个this就是谁。
-
c1调用shopping(),this是c1。c1调用shopping(),this是c1
-
在shopping方法中,实例变量name必须使用“引用.”的方式访问,但是我们没有使用“引用.name”也能访问没有报错,这是因为name前面省略了“this.”,而this代表当前对象。
public class ThisTest01{ public static void main(String[] args){ Customer c1 = new Customer("张三"); c1.shopping(); Customer c2 = new Customer("李四"); c2.shopping(); Customer.doSome(); } } // 顾客类 class Customer{ // 属性 // 实例变量(必须采用“引用.”的方式访问) String name; //构造方法 public Customer(){ } public Customer(String s){ name = s; } // 顾客购物的方法 // 实例方法 public void shopping(){ // 这里的this是谁?this是当前对象。 // c1调用shopping(),this是c1 // c2调用shopping(),this是c2 //System.out.println(this.name + "正在购物!"); // this. 是可以省略的。 // this. 省略的话,还是默认访问“当前对象”的name。 System.out.println(name + "正在购物!"); } // 静态方法 public static void doSome(){ // this代表的是当前对象,而静态方法的调用不需要对象。矛盾了。 // 错误: 无法从静态上下文中引用非静态 变量 this //System.out.println(this); } }
-
静态方法不能访问实例变量,因为实例变量的访问必须先new对象,使用“引用.”的方式去访问。而静态方法没有当前对象this。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oq36gvcN-1661564741194)(JavaSE.assets/1660221908069.png)]
8.2.3 this什么时候不能省略
- 在实例方法中,或者构造方法中,为了
区分局部变量和实例变量
,这种情况下:this. 是不能省略的。- 如:使用在setter和getter方法中增强可读性。
8.2.4 this()调用本类的构造方法
语法格式:
this(实际参数列表);
- 通过一个构造方法1去调用构造方法2,
可以做到代码复用
。但需要注意的是:“构造方法1”和“构造方法2” 都是在同一个类当中。 - 对于this()的调用只能出现在构造方法的第一行。
8.2.4 this的总结
- this是一个关键字,是一个引用,保存内存地址指向自身。
- this可以使用在实例方法中,也可以使用在构造方法中。
- this出现在实例方法中其实代表的是当前对象。
- this不能使用在静态方法中。
- “this.” 大部分情况下可以省略,但是用来
区分局部变量和实例变量
的时候不能省略。 - this() 这种语法只能出现在
构造方法第一行
,表示当前构造方法调用本类其他的构造方法,目的是代码复用。
8.3 显示参数和隐式参数
8.3.1 隐式参数
- 隐式参数就是未在方法是定义的,但的确又动态影响到程序运行的“参数”。
- this在构造方法或实例方法当中,实际上是一个虚拟参数(也称为隐式参数)。
- 可以这样理解,java底层帮我们把它传到了方法的形参列表中,只是将它“隐藏”起来了。
- Java在第一个参数this可以省略,如果省略在jdk编译的时候会自动加上,手动指定也不会报错,但是第一个参数名字必须是this。在调用时,可以不管this。
public int getLove(){
return this.love;
}
//实质上是这样的
public int getLove(Pet this){
return this.love;
}
public Employee(String n, double s) {
this.name = n;
this.salary = s;
}
//实质上是这样的
public Employee(Employee this,String n, double s) {
this.name = n;
this.salary = s;
}
8.3.2 显式参数
- 显式参数,就是方法中明确定义的参数。例如:在这个方法中,n和s就是显式参数
public Employee(String n, double s) {
this.name = n;
this.salary = s;
}
8.4 super关键字
8.5 super和this的底层实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MOIa41g8-1661564741194)(JavaSE.assets/this和super内存图.png)]
this表示一个虚拟参数,体现在class文件中,我们可以通过javap -verbose class文件名.class 来查看class文件信息
在本类中所有的实例(普通,非静态)方法 ,构造方法中都有此参数,所以我们才可以在这些方法中使用this
查看反编译后的代码我们可以看到每个实例方法、构造方法都有一个
局部变量表
,可以看到底层在每一个实例方法里面帮我们传入了一个虚拟参数
,正因为他帮我们把this加在了形参列表的第一个位置,所以我们才可以使用。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OvTdV4DJ-1661564741194)(JavaSE.assets/1660310380103.png)]
super关键字访问父类构造方法:通过反编译class文件我们发现在子类的构造方中都通过
invokespecial
指令来调用父类的无参构造方法 < init>我们可以看到在子类Dog的无参构造方法里面,虚拟机执行了invokespecial(调用特定的方法)这个指令,代表调用父类Ped的无参构造方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwmJ7WK2-1661564741195)(JavaSE.assets/1660309736449.png)]
第九章 面向接口
9.1final关键字
9.1.1 语法(最终的、不可变的)
-
final 修饰的类不能被
继承
-
final 修饰的方法不能被
覆盖
(重写) -
final 修饰引用数据类型,引用(地址)不能改变,但是对象的
数据
可以改变 -
final 不能修饰
构造方法
-
final修饰的变量不能被改变,变量必须手动(显示)赋值
- 实例变量必须声明并且赋值或者在
构造方法
中赋值 - 静态变量必须声明并且赋值或者在
静态代码块
中赋值 - 局部变量可以先声明再赋值
- 实例变量必须声明并且赋值或者在
-
final和static联合使用,称为
常量
。
9.2 抽象类
在class前添加abstract关键字
9.2.1 基本语法
- final和abstract不能联合使用,这两个关键字是对立的。
- 抽象类的子类可以是抽象类,也可以是非抽象类
- 抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法是供子类使用的。
- 抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中。
- 子类必须重写抽象类中所有的抽象方法,除非子类也是抽象类
- 抽象方法没有方法体,必须存在于抽象类中,均使用abstract关键字修饰
- 抽象类中可以书写 普通属性 普通方法 构造方法(用于给子类调用) 静态方法
9.3 接口(对行为的抽象)
接口我们可以看作是抽象类的一种特殊情况,在接口中只能定义抽象的方法和常量
定义:[修饰符列表] interface 接口名{}
9.3.1 基本语法
- 接口中只有常量+抽象方法。
- 接口不能被实例化,接口中没有构造方法
- 接口中的方法默认都是 public abstract 的
- 接口中的变量默认都是 public static final 的,必须显式初始化
- 接口可以继承多个接口
- 一个类可以实现多个接口
- 实现类(子类)必须实现(重写)接口中所有的抽象方法,除非实现类(子类)也是抽象类
- extends和implements可以共存,extends在前,implements在后。
- 先选择身份,再说你有什么能力。身份唯一,能力可以很多
9.3.2 JDK1.8接口新特性
- 可以写普通方法了 在返回值之前使用default关键字修饰
- 可以写静态方法
9.3.3 面试题:Java支持多继承吗?
不支持,但是可以使用接口继承多个接口这种方式,实现类似多继承的效果
9.3.4 抽象类和接口有什么区别?使用场景是什么?
抽象类是半抽象的。
接口是完全抽象的。
抽象类中有构造方法。
接口中没有构造方法。
接口和接口之间支持多继承。
类和类之间只能单继承。
一个类可以同时实现多个接口。
一个抽象类只能继承一个类(单继承)。
接口中只允许出现常量和抽象方法。
当你关注事物的本质,使用抽象类
当你关注某个功能,使用接口
9.4 内部类
9.4.1 内部类概述
1.内部类定义
所谓内部类就是在一个类内部进行其他类结构的嵌套操作。
2.内部类优缺点
-
内部类的优点:
- 内部类与外部类可以方便的访问彼此的私有域(包括私有方法、私有属性)。
- 内部类是另外一种封装,对外部的其他类隐藏。
- 内部类可以实现java的单继承局限。
-
内部类的缺点:
结构复杂。
9.4.4 创建内部类
1.在外部类外部 创建非静态内部类
语法: 外部类.内部类 内部类对象 = new 外部类().new 内部类();
举例: Outer.Inner in = new Outer().new Inner();
2.在外部类外部 创建静态成员内部类
语法: 外部类.内部类 内部类对象 = new 外部类.内部类();
举例: Outer.Inner in = new Outer.Inner();
3.在外部类内部 创建内部类
在外部类内部创建内部类,就像普通对象一样直接创建:Inner in = new Inner();
9.4.3 内部类的分类
在Java中内部类主要分为成员内部类、静态内部类、方法内部类、匿名内部类
1. 实例内部类
-
成员内部类内部
不允许存在任何static变量或方法,但是可以访问外部的
,正如成员方法中不能有任何静态属性(成员方法与对象相关、静态属性与类有关)class Outer { private String name = "test"; public static int age =20; class Inner{ public static int num =10;//不允许存在静态属性 public void fun() { System.out.println(name); System.out.println(age);//可以访问外部类静态相关 } } }
-
成员内部类是依附外部类对象而存在的,只有创建了外部类对象才能创建内部类。
2. 静态内部类
关键字static可以修饰成员变量、方法、代码块、其实还可以修饰内部类,使用static修饰的内部类我们称之为静态内部类,静态内部类和非静态内部类之间存在一个最大的区别,非静态内部类在编译完成之后会隐含的保存着一个引用,该引用是指向创建它的外围类,但是静态类没有。没有这个引用就意味着:
-
静态内部类的创建
不需要依赖外部类对象可以直接创建
。外部类.内部类 内部类对象 = new 外部类.内部类();
-
静态内部类
不可以使用任何外部类的非static类(包括属性和方法)
,但可以存在自己的成员变量
。class Outer { public String name = "test"; private static int age =20; static class Inner{ private String name; public void fun() { System.out.println(name); System.out.println(age);//不能直接访问外部类的实例相关 } } }
3. 局部内部类
方法内部类顾名思义就是定义在方法里的类
-
方法内部类不允许使用访问权限修饰符(public、private、protected)均不允许。
-
方法内部类
对外部完全隐藏
,除了创建这个类的方法可以访问它以外,其他地方均不能访问 (换句话说其他方法或者类都不知道有这个类的存在)方法内部类对外部完全隐藏,出了创建这个类的方法可以访问它,其他地方均不能访问。 -
方法内部类如果想要使用方法形参,该形参必须使用final声明(JDK8形参变为隐式final声明)
4. 匿名内部类
new [匿名] implements 接口(){}
匿名内部类就是一个没有名字的方法内部类,因此特点和方法与方法内部类完全一致,除此之外,还有自己的特点:
- 匿名内部类必须继承一个抽象类或者实现一个接口。
- 匿名内部类没有类名,因此没有构造方法。
9.4.5 外部类与外部类的关系
- 对于非静态的内部类,内部类的创建依赖外部类的实例对象,在没有外部类实例之前是无法创建内部类的。
- 内部类可以直接访问外部类的元素(包括私有域)—外部类在内部类之前创建,创建内部类时会将外部类的对象传入
- 外部类可以通过内部类的引用间接访问内部类元素 要想访问内部类属性,必须先创建内部类对象
第十章 常用类
10.1 Enum枚举
10.1.1 枚举的概念
枚举指由一组固定的常量组成的类型
10.1.2 语法
-
枚举是一种引用数据类型。
-
枚举编译之后也是class文件。
-
当一个方法执行结果超过两种情况,并且是一枚一枚可以列举出来的时候,建议返回值类型设计为枚举类型。
-
定义语法:
enum 枚举类型名{ 枚举值1,枚举值2,枚举值3 }
-
访问:通过“类型名.枚举值”
Week day = Week.MON;
10.2 包装类
10.2.1包装类型概述
基本类型的包装类主要提供了更多的实用操作,这样更容易处理基本类型。所有的包装类都是final 的,所以不能创建其子类,包装类都是不可变对象。
每一个基本数据类型 多有其对应的包装类型(引用数据类型) 存在于
java.lang
包中[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uBmNp0hj-1661564741195)(JavaSE.assets/1660643619526.png)]
10.2.2 继承结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UjBOTrlm-1661564741196)(JavaSE.assets/1660643675144.png)]
10.2.3 构造方法
- 每一个包装类都支持传入一个与之对应的基本数据类型,构造当前包装类实例
- 除了Character类之外,其他的包装类,还支持传入一个String类型构造当前包装类实例
- 数值包装类传入String对象构造实例,此字符串必须可以解析为正确的数值才可以否则将报 NumberFormatException
- 使用字符串构造Boolean实例,不区分大小写,内容为true则为true,其他的一律为false
10.2.3 常用方法
1.基本数据类型转包装类
方法定义:static Integer valueOf(int i)
译为:这个值是谁的变量类型
2.包装类转基本数据类型
方法定义:int intValue()
译为:这个值是多少
3.基本数据类型转字符串
方法定义:static String toString(int i)
4.字符串转基本数据类型
方法定义:static int parseInt(String s)
每个包装类(除了Character包装类)都提供有parseXXX 这样一个方法,用于将字符串转换为基本数据类型
10.2.4 自动装箱拆箱
装箱和拆箱:从JDK1.5开始,允许基本数据类型和包装类对象混合运算,混合使用
装箱:将基本数据类型 自动转换为包装类对象
拆箱:将包装类对象 自动转换为基本数据类型
10.2.5 包装类面试题
整数数值包装类(Short Integer Long Character )面试题:
以上四种包装类类型 直接使用=号的方式赋值,如果两个对象取值:
- 在byte范围以内,两个对象==为true ,因为是直接从缓存数组中返回的值 ,并没有创建新的对象
- 超过byte取值范围,两个对象==为false,因为创建了新的对象所以地址不同
分析:自动装箱底层将调用valueOf方法,所以我们可以查看valueOf方法做了什么操作
public class Test2 {
public static void main(String[] args) {
// 装箱 这里底层依然调用valueOf方法 实现基本数据和包装类的转换
Integer i1 = 110;
Integer i2 = 110;
System.out.println(i1 == i2); // true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false
}
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HdA59cF4-1661564741196)(…/…/%E5%B0%9A%E7%A1%85%E8%B0%B7javaEE/%E7%AC%94%E8%AE%B0/JavaSE.assets/1660645490693.png)]
通过源码可知,底层创建了一个Integer类型的数组作为缓存,长度为[127-(-128)+1]=256,所以只要取值范围在-128~127都会从数组中取值,否则会新创建一个Integer对象。
java中为了提高程序的执行效率,将[-128到127]之间所有的包装对象提前创建好,放到了一个方法区的“整数型常量池”当中了,目的是只要用这个区间的数据不需要再new了,直接从
整数型常量池
当中取出来。
原理:x变量中保存的对象的内存地址和y变量中保存的对象的内存地址是一样的。
10.3 Math类
方法定义 | 方法返回 |
---|---|
static int abs(int a) | int 绝对值。 |
static double ceil(double a) | 向上取整 |
static double floor(double a) | 向下取整 |
static int max(int a, int b) | 两个数的较大值 |
static int min(int a, int b) | 两个数的较小值 |
static double pow(double a, double b) | a的b次方 |
static double random() | 随机取(0.0~1.0) |
static long round(double a) | 取四舍五入 |
static double sqrt(double a) | 参数平方根 |
10.4 Random类
提供了更多类型的随机数的获取
public class TestRandom {
public static void main(String[] args) {
Random ran1 = new Random();
System.out.println(ran1.nextLong());
System.out.println(ran1.nextInt());
System.out.println(ran1.nextInt(100));
System.out.println(ran1.nextBoolean());
System.out.println(ran1.nextDouble());
}
}
10.5 String类
10.5.1 概述
- String表示字符串类型,属于引用数据类型,不属于基本数据类型。
- 在java中
随便使用双引号括起来的都是String对
象。例如:“abc”,“def”,“hello world!”,这是3个String对象
。 - java中规定,双引号括起来的字符串,是
不可变的
,也就是说"abc"自出生到最终死亡,不可变,不能变成"abcd",也不能变成"ab" - 在JDK当中双引号括起来的字符串,例如:“abc” "def"都是直接存储在“方法区”的“
字符串常量池
”当中的。
10.5.2 存储位置
分析:这是使用new的方式创建的字符串对象。这个代码中的"xy"是从哪里来的?
凡是双引号括起来的都在字符串常量池中有一份。new对象的时候一定在堆内存当中开辟空间,所以在new String()的时候会在堆中开辟一个内存空间,这个内存空间会存储"xy"在字符串常量池中的内存地址。然后执行等号左边会在栈中分配s3的空间存储堆中的内存地址,从而指向堆中的对象,此时堆中的String对象也保存内存地址
public class StringTest01 {
public static void main(String[] args) {
// 这两行代码表示底层创建了3个字符串对象,都在字符串常量池当中。
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
// 分析:这是使用new的方式创建的字符串对象。这个代码中的"xy"是从哪里来的?
// 凡是双引号括起来的都在字符串常量池中有一份。
// new对象的时候一定在堆内存当中开辟空间。
String s3 = new String("xy");
// i变量中保存的是100这个值。
int i = 100;
// s变量中保存的是字符串对象的内存地址。
// s引用中保存的不是"abc",是0x1111
// 而0x1111是"abc"字符串对象在“字符串常量池”当中的内存地址。
String s = "abc";
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ns9sNaZ3-1661564741197)(JavaSE.assets/1660653602171.png)]
1.当name是对象的实例变量时的内存
public class User {
private int id;
private String name;
}
public class UserTest {
public static void main(String[] args) {
User user = new User(110, "张三");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-elamvSOh-1661564741197)(JavaSE.assets/1660654276544.png)]
2.采用字面值的方式赋值的内存
"hello"是存储在方法区的字符串常量池当中,只有一份,而== 双等号比较的是变量中保存的内存地址。
public class StringTest02 {
public static void main(String[] args) {
String s1 = "hello";
// "hello"是存储在方法区的字符串常量池当中
// 所以这个"hello"不会新建。(因为这个对象已经存在了!)
String s2 = "hello";
// 分析结果是true还是false?
// == 双等号比较的是不是变量中保存的内存地址?是的。
System.out.println(s1 == s2); // true
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ppm37P1L-1661564741198)(JavaSE.assets/1660654563480.png)]
3.采用new关键字创建对象的内存
public class StringTest02 {
public static void main(String[] args) {
String s1 = "hello";
// "hello"是存储在方法区的字符串常量池当中
// 所以这个"hello"不会新建。(因为这个对象已经存在了!)
String s2 = "hello";
// 分析结果是true还是false?
// == 双等号比较的是不是变量中保存的内存地址?是的。
System.out.println(s1 == s2); // true
//===============================================================
String x = new String("xyz");
String y = new String("xyz");
// 分析结果是true还是false?
// == 双等号比较的是不是变量中保存的内存地址?是的。
System.out.println(x == y); //false
// 通过这个案例的学习,我们知道了,字符串对象之间的比较不能使用“==”
// "=="不保险。应该调用String类的equals方法。
// String类已经重写了equals方法,以下的equals方法调用的是String重写之后的equals方法。
System.out.println(x.equals(y)); // true
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uXq2o7d5-1661564741198)(JavaSE.assets/1660654990438.png)]
10.5.3 字符串常量池
在JVM中有一块叫做字符串常量池
的存储区域,在JDK 1.6以及以前的版本中,字符串池是放在方法区,这时的方法区也加做永久代;JDK1.7的时候,方法区合并到了堆内存中,这时的常量池也可以说是在堆内存中;JDK1.8及以后,方法区又从堆内存中剥离出来了,但实现方式与之前的永久代不同,这时的方法区被叫做元空间,常量池就存储在元空间。字符串"abc"实际上就是存储在了字符串常量池中。
String对象的两种创建方式
- 第一种:采用字面值的方式赋值
String s1 = "abc";
- 第二种:采用new关键字创建对象
String s2 = new String ("abc");
当我们用“==”来测试这两种方式创建的String对象时,发现结果是false
System.out.println (s1 == s2); //false
这两种方式创建String对象是有区别的。
-
第一种方式是在字符串常量池中获取对象,引用s1直接指向字符串常量池中的String对象,这种方式只会在字符串常量池中创建一个String对象;
-
第二种方式使用了new关键字,我们知道使用new创建对象时会在
堆内存
中创建一个对象,所以这种方法会创建两个对象,堆内存中的对象指向了方法区内存中的对象,所以这种方式会创建两个对象(前提是"abc"并没有存在字符串常量池中),s2引用指向的是堆中的引用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AE0jqJeh-1661564741199)(JavaSE.assets/1660723839949.png)]
10.5.4 一共创建了几个对象
public class StringTest03 {
public static void main(String[] args) {
/*
一共3个对象:
方法区字符串常量池中有1个:"hello"
堆内存当中有两个String对象。
一共3个。
*/
String s1 = new String("hello");
String s2 = new String("hello");
}
}
10.5.5 String常用方法
charAt(int index):返回指定索引的字符
concat():连接字符串
isEmpty():判断当前字符串长度是否为0 (不是判断字符串是否为null)
toCharArray():将字符串转换为char数组
trim():去除字符串中首尾的空白,只要是不可见的,都可以去除
split(String regex):指定内容拆分字符串,返回字符串数组
contains(CharSequence s):判断当前字符串是否包含某一个字符串
compareTo(String anotherString):左大正,右大负,等值0
- 某个索引处的字符不同,返回字符之差
- 两个字符串长度不同,返回字符串长度之差
getBytes(Charset charset):根据字符串计算为一个byte数组
startsWith(String prefix):判是是否以某一个字符串开头
endsWith(String suffix):判断是否以某一个字符串结尾
indexOf(int ch):按照ASCII码或者Unicode编码查找某个字符第一次出现的索引,未找到为-1
indexOf(String str):查找某个字符第一次出现的索引,未找到为-1
lastIndexOf(int ch):按照ASCII码或者Unicode编码查找某个字符最后一次出现的索引,未找到为-1
lastIndexOf(String str):查找某个字符最后一次出现的索引,未找到为-1
length():获取字符串的长度
equalsIgnoreCase():忽略大小写比较
equals():严格区分大小写比较内容
toLowerCase():转换为小写
toUpperCase():转换为大写
substring(int beginIndex):指定位置开始截取字符串 截取到末尾
substring(int beginIndex,int endIndex):指定开始和结束下标截取字符串 包前不包后
replace(String oldStr,String newStr):根据指定的内容替换字符串
replaceAll(String regex,String newStr):根据正则表达式内容替换字符串
trim() 方法,可以去除首尾的空白,但是不能去除中间的空白
此时我们可以使用正则表达式 \s 匹配所有不可见的空白
10.6 StringBuffer和StringBuilder
StringBuffer 和 StringBuilder 依然属于字符串类 只不过这两类维护的是可变的字符序列
StringBuffer和 StringBuilder API完全相同
StringBuffer 是线程安全的 JDK1.0
StringBuilder 线程不安全 JDK1.5
10.6.1 频繁字符串拼接会出现什么问题
分析:因为字符串被设计为不可变 任何对字符串的操作都产生新的字符串所以当我们需要频繁的对一个字符串对象改变的时候 不适合使用String应该使用StringBuffer 或者 StringBuilder
/**
* 思考:我们在实际的开发中,如果需要进行字符串的频繁拼接,会有什么问题?
* 因为java中的字符串是不可变的,每一次拼接都会产生新字符串。
* 这样会占用大量的方法区内存。造成内存空间的浪费。
* String s = "abc";
* s += "hello";
* 就以上两行代码,就导致在方法区字符串常量池当中创建了3个对象:
* "abc"
* "hello"
* "abchello"
*/
public class StringBufferTest01 {
public static void main(String[] args) {
String s = "";
// 这样做会给java的方法区字符串常量池带来很大的压力。
// 会导致字符串对象的频繁创建
for(int i = 0; i < 100; i++){
//s += i;
s = s + i;
System.out.println(s);
}
//以下代码会创建出9个字符串对象。
//电脑、笔记本、华为、12代i7、SDD 1T
//电脑笔记本
//电脑笔记本华为
//电脑笔记本华为12代i7
//电脑笔记本华为12代i7SSD 1T
String condition = "电脑";
condition += "笔记本";
condition += "华为";
condition += "12代i7";
condition += "SDD 1T";
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhZ3gKZ9-1661564741199)(JavaSE.assets/1660655984248.png)]
10.6.2 StringBuffer的append()
StringBuffer底层实际上是一个char数组,往StringBuffer中放字符串,实际上是放到char数组当中了。
构造一个字StringBuffer,其中没有指定长度或字符,初始容量为 16 个字符。
实现自动扩容
- 构造一个字StringBuffer sb = new StringBuffer();。其构造方法里调用了父类的构造方法传入初始化容量16
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GMntVx0V-1661564741199)(JavaSE.assets/1660712426433.png)]
- 父类构造方法里创建了一个char数组。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhrCJfpb-1661564741200)(JavaSE.assets/1660712466739.png)]
- 调用append(),append()里面调用了父类的append()方法,将追加的内容也传给了父类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNCxvPAm-1661564741200)(JavaSE.assets/1660712681450.png)]
-
其中一个调用了确保数组容量的方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YaLT5QRD-1661564741201)(JavaSE.assets/1660712767148.png)]
-
调用了数组拷贝
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y0BLpgeg-1661564741201)(JavaSE.assets/1660712842593.png)]
- 而数组拷贝底层调用的是System.arraycopy()进行数组扩容。这样旧的数组由于没有指定,就会被垃圾回收器回收掉。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ul3n4BAS-1661564741201)(JavaSE.assets/1660712905699.png)]
这样就实现了在append追加的时候,如果char数组满了,会自动扩容。
String如果拼接字符串会创建新的对象,因为底层byte数组被final修饰,不能指向新的对象
而StringBuffer拼接字符串不会产生新的对象,因为数组扩容后,旧的数组会被垃圾回收器回收掉
10.6.3 String相关面试题
1.为什么String对象是不可变的 :任何对字符串内容的改变 都会产生新的字符串对象
-
因为String类是final修饰的
-
String对象底层是由一个char数组维护的 而此数组是由final修饰的
(final修饰的引用数据类型 地址/引用不能改变 但是引用指向的内容是可以改变的)
-
此数组是private修饰的
我看过源代码,String类中有一个char[]数组,这个char[]数组采用了final修饰,因为数组一旦创建长度不可变。并且被final修饰的引用一旦指向某个对象之后,不可再指向其它对象,所以String是不可变的!“abc” 无法变成 “abcd”
2.有没有其他的方法可以改变String对象?
有,使用反射实现
3.为什么使用等号给String赋值,两个对象相等
- 直接使用等号给String对象赋值,赋值之前先去字符串常量池中查找 是否有相同的内容的字符串对象
- 如果没有:将此字符串对象
存在常量池中
- 如果有:将之前已经存在的字符串对象的地址赋值给新的引用,此时多个引用指向常量池中的同一块空间
- 所以==比较为true
4.为什么使用不同的方式创建String,比较结果不同?
因为采用创建String对象的方式会在堆中开辟空间,堆中保存的值是字符串常量池中的地址,而栈中的引用则保存堆中所占的内存地址。
这两种方式创建String对象是有区别的。
-
第一种方式是在字符串常量池中获取对象,引用s1直接指向字符串常量池中的String对象,这种方式只会在字符串常量池中创建一个String对象;
-
第二种方式使用了new关键字,我们知道使用new创建对象时会在
堆内存
中创建一个对象,所以这种方法会创建两个对象,堆内存中的对象指向了方法区内存中的对象,所以这种方式会创建两个对象(前提是"abc"并没有存在字符串常量池中),s2引用指向的是堆中的引用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3U9BLgzc-1661564741202)(…/…/%E5%B0%9A%E7%A1%85%E8%B0%B7javaEE/%E7%AC%94%E8%AE%B0/JavaSE.assets/1660723839949.png)]
5.StringBuilder/StringBuffer为什么是可变的呢?
我看过源代码,StringBuffer/StringBuilder内部实际上是一个char[]数组,这个char[]数组没有被final修饰,StringBuffer/StringBuilder的初始化容量我记得应该是16,当存满之后会进行扩容,底层调用了数组拷贝的方法System.arraycopy()…是这样扩容的。所以StringBuilder/StringBuffer适合于使用字符串的频繁拼接操作。
6.如何优化StringBuffer的性能?
在创建StringBuffer的时候尽可能给定一个初始化容量。最好减少底层数组的扩容次数。预估计一下,给一个大一些初始化容量。
关键点:给一个合适的初始化容量。可以提高程序的执行效率。
10.7 System类
System 类 提供了获取当前系统时间毫秒数 各种系统相关信息等方法 gc 垃圾回收
方法定义 | 功能描述 |
---|---|
static void exit(int status) | 参数作为状态码,非0异常终止 |
static void gc() | 建议虚拟机垃圾回收 |
static Properties getProperties() | 获取当前系统属性 |
String getProperty(String key) | 指定键值对应属性 |
package com.atguigu.test4;
/**
* @author WHD
* @description TODO
* @date 2022/8/17 14:36
* System 类 提供了获取当前系统时间毫秒数 各种系统相关信息等方法 gc 垃圾回收
*
*/
public class TestSystem {
public static void main(String[] args) {
// 获取当前系统时间毫秒数 long类型的 从1970年1月1日0点0分0秒到目前
long millis = System.currentTimeMillis();
System.out.println(millis);
// 异常或者错误信息打印 语句
System.err.println("错误打印");
String str1 = "abc";
// gc garbage collection 垃圾回收方法 调用此方法并不能够保证一定要回收垃圾对象
// 这里调用的是Runtime类中的gc方法
System.gc();
System.out.println(str1);
// 获取当前操作系统 以及 Java环境相关信息
// 这些信息都以键值对的形式存在 key --- value
System.out.println("操作系统版本:" + System.getProperty("os.version"));
System.out.println("操作系统名称:" + System.getProperty("os.name"));
System.out.println("JDK版本号" + System.getProperty("java.version"));
System.out.println("当前JVM默认编码格式:" + System.getProperty("file.encoding"));
// 将当前系统以及Java相关信息全部打印
System.getProperties().list(System.out);
// 退出JVM虚拟机 0表示正常退出 非0表示非正常退出
// 这里写任何状态码 都可以退出JVM虚拟机
System.exit(5555);
System.out.println(66666);
}
}
10.8 Runtime类
方法定义 | 功能描述 |
---|---|
long maxMemory() | JVM试图使用的最大内存 |
long totalMemory() | 当前JVM所占用的总内存 |
long freeMemory() | 当前JVM空闲内存 |
Process exec(String command) | 执行本地的exe文件 |
public class TestRuntime {
public static void main(String[] args) throws IOException {
Runtime runtime = Runtime.getRuntime();
// 最大内存 默认是本机物理内存 1/4
System.out.println("获取当前JVM虚拟机最大内存字节:" + runtime.maxMemory() / 1024 / 1024 );
System.out.println("当前JVM虚拟机总内存:" + runtime.totalMemory() / 1024 / 1024);
System.out.println("当前JVM空闲内存:" + runtime.freeMemory() / 1024 / 1024);
// 通过Runtime对象执行本地的exe文件
runtime.exec("D:\\apps\\开发工具安装文件\\FeiQ.exe");
// gc() System类调用的就是 Runtime类中的gc方法
runtime.gc();
}
}
10.9 日期AIP
10.9.1 java.util.Date类
java.util.Date 此类为日期类 提供了操作时间 获取时间的方法
public class TestDate {
public static void main(String[] args) {
// 获取当前系统时间
Date date1 = new Date();
System.out.println(date1);
System.out.println(date1.getYear() + 1900 + "年");
System.out.println("月份:" + (date1.getMonth() + 1));
System.out.println(date1.getDate() + "号");
System.out.println(date1.getHours() + "时");
System.out.println(date1.getMinutes() + "分");
System.out.println(date1.getSeconds() + "秒");
long millis = 7984598712415751L;
Date date2 = new Date(millis);
System.out.println(date2);
m1();
}
//表示方法弃用
@Deprecated
public static void m1(){
}
}
10.9.2 SimpleDateFormat类
SimpleDateFormat :日期格式化类
- String format(Date date) 格式化日期 最终返回值为字符串
- Date parse(String str) 将指定的字符串转换为日期对象
public class TestSimpleDateFormat {
public static void main(String[] args) throws ParseException {
Date date1 = new Date();
// 无参构造方法 将默认使用的日期格式为 22-8-17 下午3:48
SimpleDateFormat sdf = new SimpleDateFormat();
String dateStr = sdf.format(date1);
System.out.println(dateStr);
System.out.println(date1);
//因为目前sdf对象使用默认无参构造 所以字符串必须符合这个格式
String dateStr1 = "22-8-17 下午3:48";
Date parseDate1 = sdf.parse(dateStr1);
System.out.println(parseDate1);
System.out.println("------------------------------------------");
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
System.out.println(sdf1.format(date1));
String dateStr2 = "2022/01/01 12:12:12";
Date parseDate2 = sdf1.parse(dateStr2);
System.out.println(parseDate2);
}
}
10.9.3 Calendar类
Calendar 日历类 :提供有操作/获取日期时间的方法 此类也不能new对象
package com.atguigu.test6;
import java.util.Calendar;
/**
* @author WHD
* @description TODO
* @date 2022/8/17 15:57
* Calendar 日历类 :提供有操作/获取日期时间的方法 此类也不能new对象
*/
public class TestCalendar {
public static void main(String[] args) {
Calendar instance = Calendar.getInstance();
System.out.println(instance.get(Calendar.YEAR) + "年");
System.out.println(instance.get(Calendar.MONTH) + 1 + "月份");
System.out.println(instance.get(Calendar.DAY_OF_MONTH) + "一个月中第几天");
System.out.println(instance.get(Calendar.DAY_OF_WEEK) + "一周中第几天");
System.out.println(instance.get(Calendar.HOUR) + "小时");
System.out.println(instance.get(Calendar.MINUTE) + "分钟");
System.out.println(instance.get(Calendar.SECOND) + "秒");
}
}
10.9.4 LocalDate&LocalTime& LocalDateTime
1. LocalDate
LocalDate :只有日期
2. LocalTime
LocalTime:只有时间
3. LocalDateTime
LocalDateTime : 有日期 有时间
package com.atguigu.test7;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
/**
* @author WHD
* @description TODO
* @date 2022/8/17 16:16
* LocalDate :只有日期
* LocalTime:只有时间
* LocalDateTime : 有日期 有时间
*/
public class TestLocalDate {
public static void main(String[] args) {
LocalDate localDate = LocalDate.now();
System.out.println(localDate);
LocalDate localDate1 = LocalDate.of(1998, 12, 12);
System.out.println(localDate1);
System.out.println(localDate1.getYear() + "年");
System.out.println(localDate1.getMonth() + "月");
System.out.println(localDate1.getDayOfMonth() + "日");
System.out.println("--------------------------------------------");
LocalTime localTime = LocalTime.now();
System.out.println(localTime);
LocalTime localTime1 = LocalTime.of(12, 10, 15);
System.out.println(localTime1);
System.out.println(localTime1.getHour());
System.out.println(localTime1.getMinute());
System.out.println(localTime1.getSecond());
System.out.println("--------------------------------------------");
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);
System.out.println(localDateTime.getYear());
System.out.println(localDateTime.getMonth());
System.out.println(localDateTime.getDayOfWeek());
System.out.println(localDateTime.getHour());
System.out.println(localDateTime.getMinute());
System.out.println(localDateTime.getSecond());
LocalDateTime localDateTime1 = LocalDateTime.of(2021, 1, 1, 12, 10);
System.out.println(localDateTime1);
}
}
10.10 BigInteger & BigDecimal
BigInteger 可以保存任意大的整数
BigDecimal 可以精确的保存任意小数
public class TestBigInteger {
public static void main(String[] args) {
BigInteger bigInteger1 = new BigInteger("7884124787148421786526359853222165751212123532");
System.out.println(bigInteger1);
BigInteger bigInteger2 = new BigInteger("50");
System.out.println(bigInteger1.add(bigInteger2));
System.out.println(bigInteger1.subtract(bigInteger2));
System.out.println(bigInteger1.multiply(bigInteger2));
System.out.println(bigInteger1.divide(bigInteger2));
}
}
public class TestBigDecimal {
public static void main(String[] args) {
double d1 = 1;
double d2 = 0.9;
System.out.println(d1 - d2);
BigDecimal bigDecimal1 = new BigDecimal("12784245712.6365784125722");
BigDecimal bigDecimal2 = new BigDecimal("12.5");
System.out.println(bigDecimal1.add(bigDecimal2));
System.out.println(bigDecimal1.subtract(bigDecimal2));
System.out.println(bigDecimal1.multiply(bigDecimal2));
System.out.println(bigDecimal1.divide(bigDecimal2));
BigDecimal bigDecimal3 = new BigDecimal("11");
BigDecimal bigDecimal4 = new BigDecimal("3");
System.out.println(bigDecimal3.divide(bigDecimal4, RoundingMode.HALF_UP));
System.out.println(bigDecimal3.divide(bigDecimal4,5, RoundingMode.HALF_UP));
}
}
第十一章 异常机制
11.1 异常的基本概念
什么是异常,在程序运行过程中出现的错误,称为异常
11.2 异常的分类
11.2.1 异常的结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7cMDbEvn-1661564741202)(JavaSE.assets/1660904267805.png)]
11.2.2 异常的分类
异常主要分为:错误、一般性异常(受控异常)、运行时异常(非受控异常)
错误
:如果应用程序出现了 Error,那么将无法恢复,只能重新启动应用程序,最典型的Error 的异常是:OutOfMemoryError检查异常
:(Exception的直接子类)出现了这种异常必须显示的处理,不显示处理 java 程序将无法编译通过运行时异常
:此种异常可以不用显示的处理,例如被 0 除异常,java 没有要求我们一定要处理
11.3 异常处理的两种方式
11.3.1 try-catch处理异常
try:尝试,表示将可能出现异常的代码,存放在try中
catch:捕获,catch中书写try代码块中可能出现的异常,用于捕获异常并处理
注意:try不能单独出现 必须结合 catch 或者 catch -finally 或者 finally
1.代码没有出现异常
正常执行,try-catch不会影响代码执行结果
2.代码出现异常
1.遇到异常 并且捕获到 程序不会中断
2.如果捕获的异常 跟出现的异常不匹配 程序依然中断
3.处理多个异常
捕获多种异常 可以通过书写多个catch块实现
当一个try-catch结构中使用异常子类以及异常父类捕获多种异常。先写子类,再写父类
注意:我们可以使用异常父类 Exception捕获所有的异常 但是实际开发不允许这样写 因为这样的写法 降低代码的阅读性
11.3.2 throws声明异常并throw抛出异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I715oWBx-1661564741203)(JavaSE.assets/throw和throws区别.png)]
1.throws声明异常
-
定义:throws声明异常,表示通知调用者此方法可能会出现哪些异常
-
语法:throws书写在方法定义形参列表之后,可以声明多个,多个异常使用逗号分割
-
如果声明的异常为检查异常(Exception的直接子类),那么调用方法必须处理此异常
2.throw抛出异常
- 定义:throw用于生成并且抛出异常
- 语法:直接在异常处throw new Exception
如果方法中抛出的是异常父类 Exception 或者 检查异常 必须在方法参数列表之后声明异常
如果抛出的是运行时异常 不必声明
11.3.3 异常处理原则
针对调用的方法 声明的异常 处理方法分为两种:
如果声明的是运行时异常(RuntimeException) 则调用者 可以处理 也可以不处理
如果声明的是检查异常(CheckedException) 则调用者 必须处理,处理方式有两种
1.使用try-catch处理
2.继续往后声明 在main方法中声明 声明给JVM虚拟机
11.3.4 finally
定义:finally 表示,不管是出现异常,还是没有出现异常,finally 里的代码都执行,finally 和 catch可以分开使用,但 finally 必须和 try 一块使用。
0.如何取得异常对象的具体信息
-
取得异常描述信息:getMessage()
-
取得异常的堆栈信息(比较适合于程序调试阶段):printStackTrace();
-
为什么先执行了finally语句块呢?
不是先执行了,而是堆栈信息是利用System.err打印的,用这个打印用的不是同一个线程,所以顺序执行不一定。
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O1mcwQO7-1661564741203)(…/…/%E5%B0%9A%E7%A1%85%E8%B0%B7javaEE/%E7%AC%94%E8%AE%B0/JavaSE.assets/1660878508539.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T2EcSP7g-1661564741204)(…/…/%E5%B0%9A%E7%A1%85%E8%B0%B7javaEE/%E7%AC%94%E8%AE%B0/JavaSE.assets/1660878355203.png)]
1.finally什么时候不执行
finally代码块不执行的唯一情况:在执行finally之前 退出 JVM虚拟机
public class Test3 {
public static void main(String[] args) {
try{
System.out.println("try代码块");
System.exit(1);
}catch(ArithmeticException e){
e.printStackTrace();
}finally{
System.out.println("finally代码块");
}
System.out.println("程序结束");
}
}
2. finally、final和finalize的区别
-
final 关键字
- final修饰的类无法继承
- final修饰的方法无法覆盖
- final修饰的变量不能重新赋值。
-
finally 关键字
- 和try一起联合使用。
- finally语句块中的代码是必须执行的。
-
finalize 标识符
- 是一个Object类中的方法名。
- 这个方法是由垃圾回收器GC负责调用的。
3. finally和return的执行顺序
面试题:try-catch块中存在return语句,是否还执行finally块? 如果执行,说出执行顺序
在try中确定的返回值 后续在finally中对返回值的操作
-
如果是基本数据类型,不影响返回值
-
如果是引用数据类型,影响返回值(String类型除外)
-
执行顺序:
- 先执行try中return
- 执行finally代码块
- 最终再次执行try中的return 使用第一次确定的返回值
public class Test2 {
public static int getNum(){
int num = 10;
try{
num++;
System.out.println(10 / 0);
return num;
}catch(ArithmeticException e){
e.printStackTrace();
return num;
}finally{
num++;
return num;
}
}
public static void main(String[] args) {
System.out.println(getNum()); // 11
}
}
1.只有try语句中里有return语句
private static int m1() {
int num = 10;
try {
num += 10;
return num; //20
} catch (Exception e) {
e.printStackTrace();
num += 10;
} finally {
System.out.println("finally执行");
num += 10;
}
return num;
}
2.try、catch中同时有return语句
private static int m1() {
int num = 10;
try {
num += 10;
System.out.println(10 / 0);
return num;
} catch (Exception e) {
e.printStackTrace();
num += 10;
return num; //30
} finally {
System.out.println("finally执行");
num += 10;
}
}
3.try、catch、finally中有return语句
若出现异常则值为40
private static int m1() {
int num = 10;
try {
num += 10;
System.out.println(10 / 0);
return num;
} catch (Exception e) {
e.printStackTrace();
num += 10;
return num;
} finally {
System.out.println("finally执行");
num += 10;
return num;
}
}
结论:
- 如果try、catch、finally都存在return,则以finally中return返回的为结果
- 如果哪个return出现在最后,就以哪个return返回的为结果
public static int m(){ int i = 100; try { // 这行代码出现在int i = 100;的下面,所以最终结果必须是返回100 // return语句还必须保证是最后执行的。一旦执行,整个方法结束。 return i; } finally { i++; } } /* 反编译之后的效果 public static int m(){ int i = 100; int j = i; i++; return j; } */
java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
java中还有一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的语法!)
第十二章 集合
11.1 集合的概述
定义:集合实际上就是一个容器。可以来容纳其它类型的数据。
存储内容:集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,集合当中存储的都是java对象的内存地址
。(或者说集合中存储的是引用。)list.add(100); //自动装箱Integer
注意
集合在java中本身是一个容器,是一个对象。
集合中任何时候存储的都是“引用”。
11.2 继承结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EqOX5pNc-1661564741205)(JavaSE.assets/1660911825033.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgLxdrJz-1661564741205)(JavaSE.assets/1660911874831.png)]
总结:
- ArrayList:底层是数组。
- LinkedList:底层是双向链表。
- Vector:底层是数组,线程安全的,效率较低,使用较少。
- HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合key部分了。
- TreeSet:TreeSet底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合key部分了。
- HashMap:底层是哈希表。
- Hashtable:底层也是哈希表,只不过是线程安全的,效率较低,使用较少。
- Properties:是线程安全的,并且key和value只能存储字符串String。
- TreeMap:底层是一个二叉树。TreeMap集合的key可以自动按照大小顺序排序 。
特点:
-
List集合存储元素的特点:有序可重复
有序:存进去的顺序和取出来的顺序相同,每一个元素都有下标
可重复:存进去1,可以再存储一个1
-
Set集合存储元素的特点:无序不可重复
无序:存进去的顺序和取出来的顺序不一定相同,另外Set集合中元素没有下标
不可重复:存进去的是1,不能再存1了
-
SortedSet集合存储元素的特点:无序不可重复,但是SortedSet集合中的元素是可排序的
可排序:可以按照大小顺序排列
-
Map集合的key,就是一个Set集合。
-
往Set集合放数据,实际上放到了Map集合的key部分
11.3 Conllection接口
11.3.1 Collection中的常用方法
boolean add(Object e) 向集合中添加元素
int size() 获取集合中元素的个数
void clear() 清空集合
boolean contains(Object o) 判断当前集合中是否包含元素o,包含返回true,不包含返回false
boolean remove(Object o) 删除集合中的某个元素。
boolean isEmpty() 判断该集合中元素的个数是否为0
Object[] toArray() 调用这个方法可以把集合转换成数组。【作为了解,使用不多。】
11.3.2 深入Collection集合的contains、remove方法
contains方法是用来判断集合中是否包含某个元素的方法,那么它在底层是怎么判断集合中是否包含某个元素的呢?底层调用了equals方法进行比对。equals方法返回true,就表示包含这个元素。若存入的是自定义对象,则需要重写equals方法,否则比较的不是内容,而是内存地址。
结论:存放在一个集合中的类型,一定要重写equals方法。contains和remove方法都需要调用equals方法。
public class CollectionTest04 {
public static void main(String[] args) {
// 创建集合对象
Collection c = new ArrayList();
// 向集合中存储元素
String s1 = new String("abc"); // s1 = 0x1111
c.add(s1); // 放进去了一个"abc"
String s2 = new String("def"); // s2 = 0x2222
c.add(s2);
// 集合中元素的个数
System.out.println("元素的个数是:" + c.size()); // 2
// 新建的对象String
String x = new String("abc"); // x = 0x5555
// c集合中是否包含x?结果猜测一下是true还是false?
System.out.println(c.contains(x)); //判断集合中是否存在"abc" true
}
}
public class CollectionTest05 {
public static void main(String[] args) {
// 创建集合对象
Collection c = new ArrayList();
// 创建用户对象
User u1 = new User("jack");
// 加入集合
c.add(u1);
// 判断集合中是否包含u2
User u2 = new User("jack");
// 没有重写equals之前:这个结果是false
//System.out.println(c.contains(u2)); // false
// 重写equals方法之后,比较的时候会比较name。
System.out.println(c.contains(u2)); // true
c.remove(u2);
System.out.println(c.size()); // 0
/*Integer x = new Integer(10000);
c.add(x);
Integer y = new Integer(10000);
System.out.println(c.contains(y)); // true*/
// 创建集合对象
Collection cc = new ArrayList();
// 创建字符串对象
String s1 = new String("hello");
// 加进去。
cc.add(s1);
// 创建了一个新的字符串对象
String s2 = new String("hello");
// 删除s2
cc.remove(s2); // s1.equals(s2) java认为s1和s2是一样的。删除s2就是删除s1。
// 集合中元素个数是?
System.out.println(cc.size()); // 0
}
}
class User{
private String name;
public User(){}
public User(String name){
this.name = name;
}
// 重写equals方法
// 将来调用equals方法的时候,一定是调用这个重写的equals方法。
// 这个equals方法的比较原理是:只要姓名一样就表示同一个用户。
public boolean equals(Object o) {
if(o == null || !(o instanceof User)) return false;
if(o == this) return true;
User u = (User)o;
// 如果名字一样表示同一个人。(不再比较对象的内存地址了。比较内容。)
return u.name.equals(this.name);
}
}
11.3.3 集合迭代器
注意:只要集合的结构发生改变,迭代器就需要重新获取,否则会出现异常。
如往集合里面的添加元素前获取迭代器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rqq9uuov-1661564741206)(JavaSE.assets/1661213939165.png)]
在遍历集合的时候使用集合对象删除元素,也是属于改变了集合结构,没有重新获取迭代器,所以出错。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yHSDk4lI-1661564741206)(JavaSE.assets/1661213989084.png)]
可以用迭代器对象的remove方法删除,删除的一定是迭代器指向的当前元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZz2azOH-1661564741207)(JavaSE.assets/1661246441584.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXq0eWhx-1661564741208)(JavaSE.assets/1661246503835.png)]
11.4 List接口
void add(int index, E element)
E set(int index, E element)
E get(int index)
int indexOf(Object o)
int lastIndexOf(Object o)
E remove(int index)
11.4.1 ArrayList
默认初始化容量10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量10。)
1.常用方法
add() 添加元素
remove() 删除元素
set() 修改元素
get() 查询元素
isEmpty() 是否为空 长度是否为0
size() 获取集合中元素的个数,不是数组的长度
clear() 清空集合
iterator() 获取迭代器方法
2.遍历方式
ArrayList集合遍历
三种方式
1.普通for循环遍历
2.迭代器遍历。注意:集合只要发生改变,迭代器必须重新获取。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wAVX9W22-1661564741208)(JavaSE.assets/1661174759423.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0WXEN5hE-1661564741209)(JavaSE.assets/1661144223982.png)]
3.增强for循环遍历 foreach
增强for循环:右边是需要遍历的集合,左边是每个集合取出来的值,增强for循环底层是用
迭代器实现
。语法格式
for(元素类型 变量名 : 数组或集合){ System.out.println(变量名); } //这里的变量代表数组或集合中的某个元素 //foreach有一个缺点:没有下标
3.数据结构
-
特点:有序、可重复、有下标、允许为null、线程不安全的
-
增删改查:
查询修改
效率高。添加删除
效率低,但不涉及到扩容或移动元素效率也很高向数组末尾添加元素,效率还是很高的。
-
创建对象:当我们使用
无参构造创建
ArrayList对象,底层会维护一个Object[]空数组,长度为0(注释为维护长度为10数组 注释为JDK1.7的注释 与实际JDK1.8代码不符)调用有参构造可以传入一个想要的初始化容量。
-
add():当我们第一次添加元素,将数组扩容,数组长度为10,此时集合中属性elementData数组,将指向新的地址,不再指向原来的{}空数组
-
扩容:比如添加第11个元素,数组的长度扩容为原数组的1.5倍,也就是15
-
remove():底层调用System.arraycopy() 方法,从删除位置后一位开始往前移动元素 ,所有元素移动完毕,最后一位引用赋值为null
4.ArrayList集合底层是数组,怎么优化?
尽可能少的扩容。因为数组扩容效率比较低,建议在使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量。
5.这么多的集合中,你用哪个集合最多?
ArrayList集合。因为往数组末尾添加元素,效率不受影响。另外,我们检索/查找某个元素的操作比较多。
11.4.2 Vector
1.Vector 和ArrayList区别?
两者底层都是数组
Vector无参构造默认初识化长度为10的数组,ArrayList初始化长度为0数组
Vector线程安全所有方法都带有synchronize关键字,ArrayList线程不安全
Vector扩容2倍,ArrayList扩容1.5倍
Vector源自于JDK1.0 ,ArrayList源自于JDK1.2
11.4.3 LinkedList
- 单链表结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dxyq0If9-1661564741209)(JavaSE.assets/1661251388604.png)]
- 双链表结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gccvieGi-1661564741210)(JavaSE.assets/1661252751206.png)]
- 源码分析图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNpycYVI-1661564741210)(JavaSE.assets/1661252593783.png)]
1.常用方法
List Deque
因为LinkedList实现了以上两个接口 所以相当于有双重身份,既属于列表
,又属于队列
常用方法:除了和ArrayList相同的增删改查方法之外,还单独提供了,用于
操作首尾
的方法
void addFirst(E e) 将指定元素插入此列表的开头
void addLast(E e) 将指定元素插入此列表的结尾
E element() 获取列表第一个元素
E getFirst() 获取列表第一个元素
E getLast() 获取列表最后一个元素
E peek() 获取列表第一个元素
E peekFirst() 获取列表第一个元素,列表为空则返回null
E peekLast() 获取列表最后一个元素,列表为空则返回null
E poll()
获取并移除
列表第一个元素E pollFirst()
获取并移除
列表第一个元素,列表为空则返回nullE pollLast()
获取并移除
列表最后一个元素,列表为空则返回nullE pop()
获取并移除
此列表的第一个元素void push(E e) 将该元素插入此列表的开头
2.遍历
因为LinkedList数据结构为双向链表,空间不连续,所以我们根据下标查询某一个元素,必须先找到相邻的元素,所以不推荐使用普通for循环遍历LinkedList集合
3.数据结构
-
特点:有序、可重复、有下标(此下标不是数组中的下标)、允许为null、线程不安全。空间不连续,不需要扩容,没有初始化大小以及上限大小。
-
增删改查:
增加效率高
,因为不需要扩容,没有初始大小,没有上限大小删除效率高
,因为不需要移动元素,直接修改删除元素左右两侧的指针即可查询(根据下标查询单个元素 )效率低
,无法通过下标一次查找到所需要的元素,必须先找到相邻的元素。修改效率低
,因为修改必须先查询 -
LinkedList的优缺点
链表的优点:由于链表上的元素在空间存储上内存地址不连续。所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高。
链表的缺点:不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头节点开始遍历,直到找到为止。所以LinkedList集合检索/查找的效率较低。
11.4.4 ArrayList和LinkedList适用场景分析
ArrayList:把检索发挥到极致。(末尾添加元素效率还是很高的。)
LinkedList:把随机增删发挥到极致。
加元素都是往末尾添加,所以ArrayList用的比LinkedList多。
11.5 Set接口
为什么有些数字存在Set集合能够自动排序呢?
HashSet集合底层实际上是一个HashMap,往HashSet中添加数据实际上是添加到HashMap中的key部分了。而HashMap初始容量为16,底层算法是通过(长度-1)&hash值,来分配这个数据存放在哪个下标位置,这个算法永远不会超过(长度-1),所以实现了自动排序。而如果数字过大会出现乱序,比如&了之后这个数字会存在第0个位置,有的数字也分配在0位,这样就产生了hash碰撞,此时会重新计算hash值,分配到下一个位置。
TreeSet集合实现自动排序的原理是实现compareable接口重写compareTo方法,使用二叉排序树算法实现排序功能。小的放左边,大的放右边,重复的不让添加进来。
11.6 Map接口
Map接口中常用方法:
V put(K key, V value) 向Map集合中添加键值对
V get(Object key) 通过key获取value
void clear() 清空Map集合
boolean containsKey(Object key) 判断Map中是否包含某个key
boolean containsValue(Object value) 判断Map中是否包含某个value
boolean isEmpty() 判断Map集合中元素个数是否为0
V remove(Object key) 通过key删除键值对
int size() 获取Map集合中键值对的个数。
Collection values() 获取Map集合中所有的value,返回一个Collection
Set keySet() 获取Map集合所有的key(所有的键是一个set集合)
Set<Map.Entry<K,V>> entrySet() 将Map集合转换成Set集合
11.6.1 HashMap
哈希表是一个数组
和单向链表
的结合体。
数组:在查询方面效率很高,随机增删方面效率很低。
单向链表:在随机增删方面效率较高,在查询方面效率很低。
哈希表将以上的两种数据结构融合在一起,充分发挥它们各自的优点。
public class HashMap{
// HashMap底层实际上就是一个数组。(一维数组)
Node<K,V>[] table;
// 静态的内部类HashMap.Node
static class Node<K,V> {
// 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
final int hash;
final K key; // 存储到Map集合中的那个key
V value; // 存储到Map集合中的那个value
Node<K,V> next; // 下一个节点的内存地址。
}
}
//哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。(数组和链表的结合体。)
1.常用方法
常用方法:
put() 增
remove() 删
replace() 改
get() 查
containsKey() 是否包含某个键
containsValue() 是否包含某个值
keySet() 获取所有的键
values() 获取所有的值
clear() 清空集合
isEmpty() 判断集合是否为空
size() 获取集合长度
Set<Map.Entry<K,V>> entrySet() 将Map集合转换成Set集合
2.遍历
HashMap遍历方式6种
1.获取所有的键
2.获取所有的值
3.获取所有的键值对
4.获取所有的键的迭代器
5.获取所有的值的迭代器
6.获取所有的键值对的迭代器
public class Test2 {
public static void main(String[] args) {
HashMap<String,String> map =new HashMap<String,String>();
map.put("CN", "中国");
map.put("CN", "中华人民共和国");
map.put("RU", "俄罗斯");
map.put("US", "美国");
map.put("JP", "小日本");
// map.put("KR", "小棒棒");
// 方式1
Set<String> keys = map.keySet();
for(String key : keys){
System.out.println(key + "====" + map.get(key));
}
System.out.println("------------------------------------------");
// 方式2
Collection<String> values = map.values();
for(String value : values){
System.out.println(value);
}
System.out.println("------------------------------------------");
// 方式3 获取到所有键值对组合
Set<Map.Entry<String, String>> entries = map.entrySet();
for(Map.Entry<String,String> entry : entries){
System.out.println(entry);
}
System.out.println("------------------------------------------");
// 方式4
Iterator<String> it1 = map.keySet().iterator();
while(it1.hasNext()){
String key = it1.next();
System.out.println(key + map.get(key));
}
System.out.println("------------------------------------------");
// 方式5
Iterator<String> it2 = map.values().iterator();
while(it2.hasNext()){
System.out.println(it2.next());
}
System.out.println("------------------------------------------");
Iterator<Map.Entry<String, String>> it3 = map.entrySet().iterator();
while(it3.hasNext()){
Map.Entry<String, String> next = it3.next();
System.out.println(next.getKey() + "====" + next.getValue());
}
}
}
3.数据结构
-
特点:无序、不可重复、没有下标、允许key为null和value为null、线程不安全
-
HashMap底层是一个
一维数组
,这个数组中每一个元素是一个单向链表
。 -
数组默认初始化长度为16,默认负载因子为0.75F,表示数组的使用率达到75%的时候,数组开始扩容,扩容两倍
-
当单向链表的长度大于8 并且数组的长度大于64 单向链表转换为红黑树,以提高查询效率。
当链表上的元素个数小于6 则将红黑树转换为单向链表。
-
JDK1.7 数组 + 单向链表
JDK1.8 数组 + 单向链表 + 红黑树
-
HashMap的put()存储数据过程:
根据Key计算出来的hash值与数组长度 - 1 进行&(与)运算,得到一个数值,根据当前数值,去数组中判断当前位置是否为null
如果为null 则直接存放
如果不为null
-
先进行比较hash值 再进行比较key值 如果hash值相同 或者 key值相同 则 直接覆盖
-
如果不同 再判断是否为树节点 为树节点 则添加到红黑树中
如果即不相同 也 不是树节点 则直接添加在单向链表的末尾
-
-
HashMap集合的key部分特点:无序,不可重复。
-
为什么无序? 因为不一定挂到哪个单向链表上,根据hashCode的值通过hash算法计算出存储在哪个数组下标
-
不可重复是怎么保证的? equals方法来保证HashMap集合的key不可重复。如果key重复了,value会覆盖。
放在HashMap集合key部分的元素其实就是放到HashSet集合中了。
所以HashSet集合中的元素也需要同时重写hashCode()+equals()方法。
放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。
-
-
哈希表HashMap使用不当时无法发挥性能!
- 假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表。这种情况我们成为:散列分布不均匀。
- 假设将所有的hashCode()方法返回值都设定为不一样的值,这样的话导致底层哈希表就成为一维数组了,没有链表的概念了。也是散列分布不均匀。
重点记住
:HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存取效率,所必须的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NXBlnjvP-1661564741211)(JavaSE.assets/008-哈希表或者散列表数据结构.png)]
11.6.2 Hashtable
1.Hashtable 和 HashMap区别?
Hashtable是线程安全的,HashMap线程不安全
Hashtable 默认数组长度为11,HashMap默认数组长度为16
Hashtable扩容 2倍 + 1 rehash(),HashMap扩容2倍 resize()
Hashtable 不允许null值 和 null 键 ,HashMap 允许 null值 和 null键
为什么底层是hash表时需要重写两个方法?
当只重写了equals()方法
因为 Person 对象的 hashCode不同,所以它就换算出了不同的位置,让后就会把相关的值放到不同的位置上,就忽略 equlas,所以我们必须覆盖 hashCode 方法
当只重写了hashCode()方法
张三的 hashCode 相同,当两个对象的 equals 不同,所以认为值是以不一样的,那么 java 会随机换算出一个新的位置,放重复数据
当重写了hashCode()、equals()方法
因为覆盖了 equals 和 hashCode,当 hashCode 相同,它会调用 equals 进行比较,如果 equals 比较相等将不把此元素加入到 Set 中,但 equals 比较不相等会重新根据hashCode 换算位置仍然会将该元素加入进去的。
底层实现采用的是 hash 表,所以 Map 的 key 必须覆盖hashcode 和 equals 方法
11.7 Collections工具类
11.7 泛型
11.7.1 为什么使用泛型?
泛型: JDK5.0之后推出的新特性,用于统一,集合/参数/类/接口中的数据类型
泛型能更早的发现错误,如类型转换错误,通常在运行期才会发现,如果使用泛型,那么在编译期将会发现,通常错误发现的越早,越容易调试,越容易减少成本。可以避免ClassCastException,也能减少书写instance关键字,减少了向下转型的操作。
11.7.2 泛型的语法
泛型可以书写位置:集合 参数 返回值 类 接口
泛型可以书写的表示字母:可以写任何字母 甚至是任何拼音、单词
推荐使用
T Type 类型
E Element 元素
K Key 键
V Value 值
P Parameter 参数
R Return 返回值
泛型是不支持类型提升或者类型下降的 也就是不支持多态
11.7.3 泛型的用法
1.使用在类的方法中
public class Test1 {
public static void main(String[] args) {
A<Integer> a1 = new A<Integer>();
a1.m1(110);
System.out.println(a1.m2());
A<String> a2 = new A<String>();
a2.m1("hello");
System.out.println(a2.m2());
// 观察以上代码 我们通过泛型 可以让A类变的灵活 但是又不混乱
}
}
class A<T>{
public void m1(T t){
System.out.println(t);
}
public T m2(){
return null;
}
}
2.使用在接口当中
public class Test1 {
public static void main(String[] args) {
B1 b1 = new B1();
System.out.println(b1.m1(new Double(20)).length());
B2 b2 = new B2();
System.out.println(b2.m1(new Student("aa", 20)));
}
}
interface B<P,R>{
R m1(P p);
}
3.使用在集合当中
class Animal{}
class Dog extends Animal{}
class Penguin extends Animal{}
泛型必须为Animal类型
public static void m1(List<Animal> list){ // 泛型必须为Animal类型
}
泛型必须为Dog类型或其父类
public static void m2(Set<? super Dog> set){ // 可以是Dog类型 或者Dog类的父类作为泛型
}
泛型必须为Animal类型或其子类
public static void m3(List<? extends Animal> list){ // 泛型为 Animal 或者 Animal的子类类型
}
注意:什么时候有具体的类型呢,new了对象才有具体的类型,泛型是实例级别的。不new对象时是没有类型的。
11.7.4 泛型的优缺点
泛型这种语法机制,只在程序编译阶段起作用,只是给编译器参考的。(运行阶段泛型没用!)
1.泛型优点是什么?
第一:集合中存储的元素类型统一了。
第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”!
2.泛型的缺点是什么?
导致集合中存储的元素缺乏多样性!
大多数业务中,集合中元素的类型还是统一的。所以这种泛型特性被大家所认可。
11.8 设计模式
设计模式:GOF Gang Of Four 四人帮 是由四个人所编写的一本书 《设计模式》。软件开发过程中,用于解决特定问题的一个套路。
11.8.1 六大/七大原则
- 单一职责(原则):一个类只描述一个事物(高内聚:与当前事物直接关联的信息描述在本类中 间接关联 或者没有关联不能写在本类中低耦合:将功能尽可能的拆分 以降低程序的耦合度) 一个方法只做一件事
-
依赖倒置原则 :我们程序顶层应该依赖于抽象 而非依赖于具象
-
里氏替换原则:我们程序中所有父类的信息 都可以被子类所替代 实现同样的功能
-
迪米特法则:不要跟陌生人说话 高内聚:与当前事物直接关联的信息描述在本类中 间接关联 或者没有关联不能写在本类中
-
接口隔离原则:接口与接口之间相互隔离 每个接口中的方法功能 单一 纯粹
-
开闭原则:我们编写的代码 对扩展开放 对修改源代码关闭
-
合成复用原则:程序中多用组合 多复用 提高代码重用性
11.8.2 简单/静态工厂
问题分析:目前我们都是通过new关键字创建对象 new关键字直接创建对象 耦合度是非常高的比如,如果我们的程序需要在创建某个对象之前 做一些操作 或者在创建完对象之后做一些操作目前我们直接通过new关键字实现起来比较麻烦
在后续学习过程中 我们使用Spring框架 将不再new对象 统一由Spring IOC容器来创建对象,Spring IOC容器就是一个静态工厂,专门负责帮我们创建程序中的对象,从而可以帮我们做很多操作
11.8.3 工厂方法
使用工厂方法模式,将顶层工厂设计为抽象的,子类工厂作为实现类,针对每一种产品都单独对应一个工厂 这样以提高程序的扩展性、灵活性
11.8.4 单例模式
单例模式 :单个实例,在程序中只能存在某个类的一个实例,且只能存在一个
1.饿汉单例
上来就吃
/**
* @author WHD
* @description TODO
* @date 2022/8/23 14:53
*
* 饿汉单例 上来就吃(上来就创建对象)
*
* Runtime就是饿汉单例 模式
*/
public class HungrySingleton {
// 第二步 创建好一个static修饰的 本类实例
private static HungrySingleton instance = new HungrySingleton();
// 第一步 构造方法私有化
private HungrySingleton(){}
// 第三步 通过公开的方法 获取到本类的实例
public static HungrySingleton getInstance(){
return instance;
}
}
2.懒汉单例
不到饿死不吃
/**
* @author WHD
* @description TODO
* @date 2022/8/23 15:00
* 懒汉单例 不到饿死不吃 (上来并没有创建对象 而是真正在使用的时候再创建对象 )
*/
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
3.懒汉最终版
观察我们之前书写的懒汉单例模式 发现是有线程安全隐患的 如果有多个线程访问 getInstance()方法 将极有可能创建对个对象 从而失去单例的特点
所以我们将原来的代码优化如下:使用同步关键字修饰方法
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton(){}
public synchronized static LazySingleton getInstance(){
if(instance == null){
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
instance = new LazySingleton();
}
return instance;
}
}
第十三章 多线程
13.1 概念
13.1.1 程序
一系列代码指令的集合,属于代码的静态形式,由代码组成即为程序
13.1.2 进程
进行中的应用程序 属于CPU分配资源的最小单位
进程是一个应用程序(1个进程是一个软件)。
一个进程可以启动多个线程。
13.1.3 线程
属于CPU运算的最小单位
线程是一个进程中的执行场景/执行单元。
13.1.4 进程和线程的关系
线程是包含在进程之中的 一个进程至少包含一个线程
线程不是越多越好 要根据实际的硬件环境
13.1.5 单核心/多核心
早期的计算器都是单核心单线程的 单核心单线程依然可以"同时"执行多个任务 只不过这些任务是轮流交替执行的 因为CPU切换任务的频率非常快 所以我们感知不到这个过程 宏观上感知是同时执行的 实际是轮流交替执行
多核心实际上是将一个CPU分成了多个部分 因为制作工艺的提升 多个核心可以同时协作运算 提高用户体验、执行效率
13.1.5 并发和并行
并发:指的是同时发生
并行:真正意义上的同时执行
13.1.6 java中的进程和线程
-
java HelloWorld 回车之后。会先启动JVM,而JVM就是一个进程。JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护,回收垃圾。
-
最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。
-
线程A和线程B,
堆内存和方法区内存共享,但是栈内存独立
,一个线程一个栈。假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。为了提高程序的处理效率。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jOLsexWO-1661564741212)(JavaSE.assets/1661443410742.png)]
13.2 创建线程的方式
13.2.1 继承Thread类
编写一个类,直接继承java.lang.Thread,重写run方法。
// 定义线程类
public class MyThread extends Thread{
public void run(){
}
}
// 创建线程对象
MyThread t = new MyThread();
// 启动线程。
t.start();
13.2.2 实现Runnable接口
编写一个类,实现java.lang.Runnable接口,实现run方法。将实现类对象作为参数构造Thread类实例
// 定义一个可运行的类
public class MyRunnable implements Runnable {
public void run(){
}
}
// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。
13.3 主线程与线程的名字
static Thread currentThread() 获取当前线程对象
void setName(String name) 修改线程名称
String getName() 获取线程名称
子线程默认名称为 Thread-0 Thread-1 以此类推
13.4 调用run方法和start方法区别
调用run方法不会开启新的线程,不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
调用start方法会开启新的线程
- start()方法的作用是:启动一个
分支线程
,在JVM中开辟一个新的栈空间
,这段代码任务完成之后,瞬间
就结束了。线程就启动成功了。- 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
- run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pDQEOFVl-1661564741212)(JavaSE.assets/1661421821192.png)]
13.5 线程的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0N7Abe0N-1661564741213)(JavaSE.assets/1661430464691.png)]
public class MyThread2 extends Thread{
@Override
public void run() { // run方法开始执行 运行状态
System.out.println(Thread.currentThread().getName() +"run方法开始执行");
try {
Thread.sleep(500); // 阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"run方法执行完毕");
}
// 死亡状态
public static void main(String[] args) {
MyThread2 mt2 = new MyThread2(); // 创建状态
mt2.start(); // 就绪状态
}
}
13.6 常用方法
13.6.1 线程的优先级
线程优先级默认为5,最小为1 ,最大为10。
优先级高的线程获取CPU资源的概率大,但是并
不能够保证
一定最先执行void setPriority(int newPriority) 设置线程优先级
int getPriority() 获取线程优先级
static Thread currentThread() 获取当前线程对象
这个线程出现在mian方法获取的就是主线程,哪个对象启动的当前线程就是谁。
出现在run方法当中:要看谁去执行了run方法
- 当t1执行run方法,当前线程就是t1。当t2执行run方法,当前线程就是t2。
13.6.2 线程的休眠
static void sleep(long millis) 当前线程休眠指定毫秒数
static void sleep(long millis, int nanos) 当前线程休眠指定毫秒以及纳秒数
- 作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。- Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
1.中断正在睡眠的线程
void interrupt() 中断正在睡眠的线程,利用了异常处理机制,会让睡眠的那行代码报异常,会被catch捕获
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
// 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
t.interrupt(); // 干扰,一盆冷水过去!
}
}
class MyRunnable2 implements Runnable {
// 重点:run()当中的异常不能throws,只能try catch
// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---> begin");
try {
// 睡眠1年
Thread.sleep(1000 * 60 * 60 * 24 * 365);
} catch (InterruptedException e) {
// 打印异常信息
//e.printStackTrace();
}
//1年之后才会执行这里
System.out.println(Thread.currentThread().getName() + "---> end");
}
}
2.强行终止线程执行
void stop() 强迫线程停止执行。
这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用。
3.正常终止线程的方式(重点)
通过打标记的方式来终止线程
/*
怎么合理的终止一个线程的执行。这种方式是很常用的。
*/
public class ThreadTest10 {
public static void main(String[] args) {
MyRunable4 r = new MyRunable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
// 模拟5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止线程
// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
r.run = false;
}
}
class MyRunable4 implements Runnable {
// 打一个布尔标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++){
if(run){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// return就结束了,你在结束之前还有什么没保存的。
// 在这里可以保存呀。
//save....
//终止当前线程
return;
}
}
}
}
13.6.3 线程的调度
常见的线程调度模型有哪些?
-
抢占式调度模型:
那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。
java采用的就是抢占式调度模型。
-
均分式调度模型:
平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。
平均分配,一切平等。
1. 线程插队
当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
class MyThread1 extends Thread {
public void doSome(){
MyThread2 t = new MyThread2();
t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
}
}
子线程插队主线程 让子线程先执行完毕
void join() 插队 直到当前线程执行完毕
void join(long millis) 插队指定时间
void join(long millis,int nanos) 插队指定时间
2. 线程礼让
static void yield() 线程礼让,但是不能够保证一定会礼让,只是提供一种可能
暂停当前正在执行的线程对象,并执行其他线程,yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪之后,有可能还会再次抢到。
13.7 synchronized解决线程安全问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YY8hRsCF-1661564741213)(JavaSE.assets/1661435908764.png)]
-
什么时候数据在多线程并发的环境下会存在安全问题呢?
三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。满足以上3个条件之后,就会存在线程安全问题。
-
涉及两个专业术语:
-
异步编程模型:异步就是并发。
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发(效率较高。) -
同步编程模型:同步就是排队。
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
两个线程之间发生了等待关系,这就是同步编程模型。
效率较低。线程排队执行。
-
13.7.1 修饰代码块
1.同一时刻只能有一个线程进入synchronized(this)同步代码块
2.当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定
3.当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非同步代码块
1.关于synchronized 代码块中书写的对象
- 通常在实际开发中,我们都会写为this 表示当前对象,具体是指当前Runnable实现类对象,因为此对象我们只需要创建一次,所以所有线程用的都是一个对象
- 如果需要保证两个线程需要同步,那么则需要共享同一个对象,不是必须写this,可以写任何对象,只要保证多个线程锁定的是同一个对象,就可以达到有互斥效果,从而实现线程同步
- 锁的是对象的使用权,而不是锁代码块。表示这个对象在这段代码块中只能有一个线程来访问他。
- 每个java对象都有一把锁,synchronized 会把这个对象的锁占有了。所以当其他线程进入同步代码块时,发现对象锁被另一个线程占有了,只能等待另一个线程对象归还对象锁,当前等待的线程才能拿到这个共享对象的对象锁。
注意
:如果锁定的不是同一个对象,依然不能线程同步,想让哪个对象同步,就写哪个对象
2.synchronized 代码块执行原理
在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)100个对象,100把锁。1个对象1把锁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EMRahjZy-1661564741214)(JavaSE.assets/1661430583216.png)]
写"abc"的话他在字符串常量池当中,全天下人都会等这把锁。而写this时,这个账户可以是不同的账户,同一个账户需要等待,不同账户不用等待。这里的ls就是不同账户,不需要等待。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uVb9AAhY-1661564741214)(JavaSE.assets/1661431986481.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fuueikK2-1661564741215)(JavaSE.assets/1661432024370.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AKube97L-1661564741215)(JavaSE.assets/1661432151318.png)]
以下代码的执行原理?
-
假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
-
假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
-
假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。
13.7.2 修饰实例方法
使用synchronized 修饰方法实现同步效果:表示在同一时间
只能有一个线程
访问此方法
- synchronized出现在
实例方法
上,一定锁的是this。所以这种方式不灵活。- synchronized出现在
实例方法
上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。- 如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。
我们之前接触过的线程安全的类 比如 StringBuffer Hashtable Vector 都是使用synchronized修饰方法实现的线程安全
13.7.3 修饰静态方法
对象锁:1个对象1把锁,100个对象100把锁。类锁:100个对象,也可能只是1把类锁。
synchronized出现在
静态方法
上,表示找类锁。类锁永远只有1把。就算创建了100个对象,那类锁也只有一把。
类锁是保证静态变量的安全。
13.7.4 总结哪些变量会存在线程安全问题
Java三大变量:
-
实例变量:在堆中。
-
静态变量:在方法区。
-
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。因为局部变量不共享。(一个线程一个栈。)局部变量在栈中。所以局部变量永远都不会共享数据。
实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
13.7.5 面试题 synchronized (排他锁)
1.doOther方法执行的时候需要等待doSome方法的结束吗?
不需要,doOther方法执行不需要获取共享对象的对象锁,不需要锁,所以直接就执行。
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
//不需要,因为doOther()方法没有synchronized
public class Exam01 {
public static void main(String[] args) throws InterruptedException {
MyClass mc = new MyClass();
Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
t2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
2.doOther方法执行的时候需要等待doSome方法的结束吗?
需要,因为共享对象锁只有一个,t1线程把共享对象锁占住了,所以t2找不到那把锁只能在锁池里等了。
package com.bjpowernode.java.exam2;
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
//需要
public class Exam01 {
public static void main(String[] args) throws InterruptedException {
MyClass mc = new MyClass();
Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
t2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
3.doOther方法执行的时候需要等待doSome方法的结束吗?
不需要,因为MyClass对象是两个,两把锁。t1把mc1对象拿走了,t2把mc2对象拿走了。
他们不共享对象,因此不需要排队。
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
//不需要,因为MyClass对象是两个,两把锁。
public class Exam01 {
public static void main(String[] args) throws InterruptedException {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new MyThread(mc1);
Thread t2 = new MyThread(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
t2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
4.doOther方法执行的时候需要等待doSome方法的结束吗?
虽然new了两个对象,但是类都是MyClass,而类锁只有一把,所以需要等待。
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
//需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把。
public class Exam01 {
public static void main(String[] args) throws InterruptedException {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new MyThread(mc1);
Thread t2 = new MyThread(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
t2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if(Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if(Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
class MyClass {
// synchronized出现在静态方法上是找类锁。
public synchronized static void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized static void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
13.7.6 死锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j4NJuD4r-1661564741216)(JavaSE.assets/1661435930421.png)]
1. 死锁的概述
属于一种逻辑错误,而导致的多个线程共同竞争一个资源,僵持不下导致程序不能继续执行
2. 死锁的实现
synchronized在开发中最好不要嵌套使用。
package com.bjpowernode.java.deadlock;
/*
死锁代码要会写。
一般面试官要求你会写。
只有会写的,才会在以后的开发中注意这个事儿。
因为死锁很难调试。
*/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1和t2两个线程共享o1,o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
13.7.9 可重入锁/不可重入锁
可重入锁:同一个线程对象 重复获同一个锁对象 不应该产生死锁
不可重入锁 :同一个线程对象 重复获同一个锁对象 会产生死锁
13.7.8 怎么解决线程安全问题?
是一上来就选择线程同步吗?synchronized
答:不是,synchronized会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
对象不共享,就没有数据安全问题了。)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
就只能选择synchronized了。线程同步机制。
13.8 通信(生产者消费者模式)
13.8.1 生产者消费者模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HYwI506l-1661564741216)(JavaSE.assets/1661525983358.png)]
生产者消费者模式,这个所谓的模式不属于设计模式
规则:
1.先生产 再消费
2.持续生产才能持续消费
3.必须保证产品的完整性
4.不能重复消费同一个产品
13.8.2 wait()和notify()
wait():o.wait()让正在o对象上活动的线程t进入等待状态,无期限等待,直到被唤醒为止。并且释放掉t线程之前占有的o对象的锁。
- notify():o.notify()让正在o对象上等待的线程唤醒,不会释放o对象上之前占有的锁。
还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cf4BFwDS-1661564741216)(JavaSE.assets/1661526013101.png)]
13.8.3 代码实现
1.第一种
当前生产者消费者模式,使用 wait() 方法实现线程等待,notify()方法实现线程唤醒,这种方式效率非常低
package com.sz.threadtest02;
/**
* @Auther: 翟文海
* @Date: 2022/8/24/024 15:21
* @Description:
*/
public class Computer {
private String name;
private String screen;
private boolean flag = false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getScreen() {
return screen;
}
public void setScreen(String screen) {
this.screen = screen;
}
public Computer() {
}
}
package com.sz.threadtest02;
/**
* @Auther: 翟文海
* @Date: 2022/8/24/024 15:21
* @Description:
*/
public class Consumer implements Runnable{
private Computer computer;
public Consumer(Computer computer) {
this.computer = computer;
}
@Override
public void run() {
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(computer){
for (int i = 1; i <= 50; i++) {
if (!computer.isFlag()) {
try {
computer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者:" + computer.getName() + ",消费者:" + computer.getScreen());
computer.setFlag(false);
computer.notify();
}
}
}
}
package com.sz.threadtest02;
/**
* @Auther: 翟文海
* @Date: 2022/8/24/024 15:23
* @Description:
*/
public class Producer implements Runnable{
private Computer computer;
public Producer(Computer computer) {
this.computer = computer;
}
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (computer){
for (int i = 1; i <= 50; i++) {
if (computer.isFlag()) {
try {
computer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i % 2 == 0) {
computer.setName("华硕"+i+"号");
computer.setScreen("华硕显示器"+i+"号");
}else {
computer.setName("联想"+i+"号");
computer.setScreen("京东方"+i+"号");
}
System.out.println("生产者:"+computer.getName());
computer.setFlag(true);
computer.notify();
}
}
}
}
2.第二种
package com.sz;
import java.util.ArrayList;
import java.util.List;
/**
* @Auther: 翟文海
* @Date: 2022/8/26/026 21:10
* @Description:
*/
public class productionTest {
public static void main(String[] args) {
List<AppleComputer> lists = new ArrayList<>();
Producer producer = new Producer(lists);
Consumer consumer = new Consumer(lists);
producer.setName("生产者");
consumer.setName("消费者");
producer.start();
consumer.start();
}
}
class Producer extends Thread {
private List<AppleComputer> list;
public Producer(List<AppleComputer> list) {
this.list = list;
}
@Override
public void run() {
synchronized (list) {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() > 0) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
AppleComputer computer = new AppleComputer("IOS-9", "华为笔记本");
list.add(computer);
System.out.println(Thread.currentThread().getName()+"生产一台" + computer);
list.notify();
}
}
}
}
class Consumer extends Thread {
private List<AppleComputer> list;
public Consumer(List<AppleComputer> list) {
this.list = list;
}
@Override
public void run() {
synchronized (list) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 0) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
AppleComputer remove = list.remove(0);
System.out.println(Thread.currentThread().getName()+"消费一台" + remove);
list.notify();
}
}
}
}
class AppleComputer {
private String name;
private String band;
public String getBand() {
return band;
}
public void setBand(String band) {
this.band = band;
}
@Override
public String toString() {
return "AppleComputer{" +
"name='" + name + '\'' +
", band='" + band + '\'' +
'}';
}
public AppleComputer(String name, String band) {
this.name = name;
this.band = band;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public AppleComputer(String name) {
this.name = name;
}
}
3.第三种
程序执行时,生产者或者消费者都有可能抢到list对象锁,另一个线程则会等待被唤醒。
如果消费者线程一开始抢到对象锁,会判断仓库里是否有值,发现仓库里没有,此时会进入if语句执行wait方法让消费者线程进入等待,将list对象锁释放掉。
此时生产者线程会抢到list对象锁,生产者线程就会创建对象并且唤醒消费者线程。
唤醒后,两个线程会继续抢夺list仓库的对象锁,就算生产者又抢到也没有关系,因为此时list仓库里有一个值,if判断会进入if语句执行wait方法,让生产者线程又进入等待。。。
package com.bjpowernode.java.thread;
import java.util.ArrayList;
import java.util.List;
/*
1、使用wait方法和notify方法实现“生产者和消费者模式”
2、什么是“生产者和消费者模式”?
生产线程负责生产,消费线程负责消费。
生产线程和消费线程要达到均衡。
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。
3、wait和notify方法不是线程对象的方法,是普通java对象都有的方法。
4、wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。
5、wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。
6、notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。
7、模拟这样一个需求:
仓库我们采用List集合。
List集合中假设只能存储1个元素。
1个元素就表示仓库满了。
如果List集合中元素个数是0,就表示仓库空了。
保证List集合中永远都是最多存储1个元素。
必须做到这种效果:生产1个消费1个。
*/
public class ThreadTest16 {
public static void main(String[] args) {
// 创建1个仓库对象,共享的。
List list = new ArrayList();
// 创建两个线程对象
// 生产者线程
Thread t1 = new Thread(new Producer(list));
// 消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
// 生产线程
class Producer implements Runnable {
// 仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直生产(使用死循环来模拟一直生产)
while(true){
// 给仓库对象list加锁。
synchronized (list){
if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
try {
// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到这里说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒消费者进行消费
list.notifyAll();
}
}
}
}
// 消费线程
class Consumer implements Runnable {
// 仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直消费
while(true){
synchronized (list) {
if(list.size() == 0){
try {
// 仓库已经空了。
// 消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到此处说明仓库中有数据,进行消费。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒生产者生产。
list.notifyAll();
}
}
}
}
4.队列实现
我们可以使用队列 LinkedBlockingQueue 来实现 接近生活中的模式
13.9 守护线程
13.9.1 线程的分类
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
13.9.2 守护线程的特点
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程。
13.9.3 守护线程用在什么地方呢?
每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。一直在那里看着,没到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
13.9.4 守护线程的实现
void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
如果为 true,则将该线程标记为守护线程。
public class Test {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("执行" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("数据备份");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
13.10 定时器
定时器的作用:
间隔特定的时间,执行特定的程序。
每周要进行银行账户的总账操作。
每天要进行数据的备份操作。
public class TimerTest {
public static void main(String[] args) throws Exception {
// 创建定时器对象
Timer timer = new Timer();
//Timer timer = new Timer(true); //守护线程的方式
// 指定定时任务
//timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2020-03-14 09:34:30");
//timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);
// 每年执行一次。
//timer.schedule(new LogTimerTask() , firstTime, 1000 * 60 * 60 * 24 * 365);
//匿名内部类方式
timer.schedule(new TimerTask(){
@Override
public void run() {
// code....
}
} , firstTime, 1000 * 10);
}
}
// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {
@Override
public void run() {
// 编写你需要执行的任务就行了。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + ":成功完成了一次数据备份!");
}
}
13.11 实现Callable接口
13.11.1 创建线程的方式
-
继承Thread类 重写run方法
-
实现Runnable接口 重写run方法
-
实现Callable接口 重写call方法
13.11.2 继承Thread、实现Runnable接口和Callable接口的区别?
前两种都是重写run方法 因为是重写抽象法方法
所以不能声明异常 因为抽象方法没有声明异常
所以没有返回值 因为抽象方法返回值定义为void
Callable接口可以抛出异常 也可以添加返回值
查看源代码:
1.FutureTask类实现类 RunnableFuture接口 RunnableFuture接口继承自Runnable接口
2.FutureTask中的泛型 在参数为Callable实现类的构造方法中 作为参数的泛型使用 所以要保持Callable实现类
和FutureTask泛型一致
13.11.3 实现步骤
-
创建一个FutureTask对象
-
需要传入一个Callable接口的实现类
-
创建线程对象并传入FutureTask对象
-
通过FutureTask对象的get()方法获取返回值。
注意
:通过get()方法获取返回值时会让导致“当前线程阻塞”。如下也就是mian()方法受阻停下来了,因为call方法不执行结束就不能返回一个值,所以需要等待。
/*
实现线程的第三种方式:
实现Callable接口
这种方式的优点:可以获取到线程的执行结果。
这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
*/
public class ThreadTest15 {
public static void main(String[] args) throws Exception {
// 第一步:创建一个“未来任务类”对象。
// 参数非常重要,需要给一个Callable接口实现类对象。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值
// 线程执行一个任务,执行之后可能会有一个执行结果
// 模拟执行
System.out.println("call method begin");
Thread.sleep(1000 * 10);
System.out.println("call method end!");
int a = 100;
int b = 200;
return a + b; //自动装箱(300结果变成Integer)
}
});
// 创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
// 这里是main方法,这是在主线程中。
// 在主线程中,怎么获取t线程的返回结果?
// get()方法的执行会导致“当前线程阻塞”
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
// main方法这里的程序要想执行必须等待get()方法的结束
// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
// 另一个线程执行是需要时间的。
System.out.println("hello world!");
}
}
13.11.4 使用这种方式的优缺点
这种方式的优点:可以获取到线程的执行结果。
这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞
13.12 线程池
13.12.1 线程池概述
线程池:相当于将多个线程统一管理的一个容器
13.12.2 线程池存在的意义
-
我们之前创建线程三种方式,如果是在多线程场景下,我们之前是这些方式需要重复的创建以及回收线程对象,浪费时间
-
我们之前创建线程三种方式,都属于每个线程相互独立,不能统一管理
-
我们之前创建线程三种方式,不能执行一些定时任务
总结
:以上效果 线程池都能实现
13.12.3 常用创建线程池的方法
newCachedThreadPool() :根据当前任务的需要灵活的创建线程的线程池
newFixedThreadPool(int nThreads):创建指定线程数量的线程池
newScheduledThreadPool(int corePoolSize):创建执行线程数量 并且有定时执行的线程池
newSingleThreadExecutor() :创建只有一个线程的线程池
以上方法都属于Executors类中的静态方法 直接通过类名调用即可
public class Test2 {
public static void main(String[] args) throws InterruptedException {
// 根据当前任务的需要灵活的创建线程的线程池
ExecutorService es1 = Executors.newCachedThreadPool();
es1.execute(new Runnable() {
@Override
public void run() {
System.out.println("execute method run :hello CachedThreadPool");
}
});
es1.submit(new Runnable() {
@Override
public void run() {
System.out.println("submit method run :hello CachedThreadPool");
}
});
Thread.sleep(500);
System.out.println("-------------------------------------------------------");
// 创建指定线程数量的线程池
ExecutorService es2 = Executors.newFixedThreadPool(10);
es1.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "hello FixedThreadPool");
}
});
Thread.sleep(500);
System.out.println("-------------------------------------------------------");
ScheduledExecutorService ses1 = Executors.newScheduledThreadPool(10);
ses1.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "===Hello ScheduledThreadPool");
}
}, 5, TimeUnit.SECONDS); // 实际开发中 跑批 定时任务 比如每天晚上十二点统计今日网站访问量 结合Spring Task框架
Thread.sleep(500);
System.out.println("-------------------------------------------------------");
// 创建只有一个线程的线程池
ExecutorService es3 = Executors.newSingleThreadExecutor();
es3.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "hello SingleThreadExecutor");
}
});
}
}
13.12.4 线程池execute方法和submit方法区别?
execute方法只能接收Runnable实现类
submit方法可以接收Runnable、Callable 实现类
submit底层依然调用execute方法
第十四章 IO流
14.1 IO流的概述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PF7PN9Jy-1661564741217)(JavaSE.assets/1661493609567.png)]
通过IO可以完成硬盘文件的读和写。
I : Input
O : Output
14.2 IO流的分类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUmAGprn-1661564741218)(JavaSE.assets/1661560222920.png)]
14.2.1 根据流的方向进行分类
输入输出是相对内存
而言的,参照物是内存,从内存里面出来就是输出,往内存里面去就叫输入。
以内存作为参照物:
-
往内存中去,叫做输入(Input)。或者叫做读(Read)。
-
从内存中出来,叫做输出(Output)。或者叫做写(Write)。
14.2.2 根据读取数据方式不同进行分类
有的流是按照字节
的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。这种流是万能的,什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频文件等…
-
假设文件file1.txt,采用
字节流
的话是这样读的:a中国bc张三fe
第一次读:一个字节,正好读到’a’
第二次读:一个字节,正好读到’中’字符的一半。
第三次读:一个字节,正好读到’中’字符的另外一半。
有的流是按照字符
的方式读取数据的,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流不能读取:图片、声音、视频等文件。只能读取纯文本文件,连word文件都无法读取。
- 假设文件file1.txt,采用
字符流
的话是这样读的:
a中国bc张三fe
第一次读:'a’字符('a’字符在windows系统中占用1个字节。)
第二次读:'中’字符('中’字符在windows系统中占用2个字节。)
流的分类:输入流、输出流、字节流、字符流
14.2.3 特点
1.四大家族
四大家族的首领:
java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流
注意
:四大家族的首领都是抽象类。(abstract class)
2.共有特点
-
所有的
流
都实现了:
java.io.Closeable接口,都是可关闭的,都有close()方法。流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费(占用)很多资源。养成好习惯,用完流一定要关闭。
-
所有的
输出流
都实现了:
java.io.Flushable接口,都是可刷新的,都有flush()方法。输出流在最终输出之后,一定要记得flush(),刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)刷新的作用就是清空管道。
注意:如果没有flush()可能会导致丢失数据。
3.需要掌握的16个流
文件专属:
java.io.FileInputStream(掌握)
java.io.FileOutputStream(掌握)
java.io.FileReader
java.io.FileWriter转换流:(将字节流转换成字符流)
java.io.InputStreamReader
java.io.OutputStreamWriter缓冲流专属:
java.io.BufferedReader
java.io.BufferedWriter
java.io.BufferedInputStream
java.io.BufferedOutputStream数据流专属:
java.io.DataInputStream
java.io.DataOutputStream标准输出流:
java.io.PrintWriter
java.io.PrintStream(掌握)对象专属流:
java.io.ObjectInputStream(掌握)
java.io.ObjectOutputStream(掌握)
4.总结
- 在java中只要“类名”以
Stream结尾
的都是字节流。以“Reader/Writer
”结尾的都是字符流。
14.3 File类
java.io.File类 提供了有创建文件 查看文件 删除文件等 各种用于操作文件的方法
14.4 字节流
14.4.1 InputStream
这个抽象类是表示输入字节流的所有类的超类。
1. FileInputStream
FileInputStream 字节读取流
int read() 每次读取一个字节 返回值为读取的内容 ASCII码 如未读取到内容 返回值为-1
int read(byte [] data) 每次读取指定数组长度的字节 读取内容保存在byte数组中
返回值为读取字节的个数 如未读取到内容 返回值为-1
使用字节流读取中文
问题:我们使用字节流读取数组 实现读取中文 极有可能将一个中文汉字拆开,出现乱码
解决方案:
1.定义一个比较长的字节数组
2.使用字节流对象提供的available方法作为读取数组的长度
available() 用于获取字节流可读字节数 返回值为int类型
public class TestFileInputStream3 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("A.txt");
System.out.println("当前文件可读字节数:" + fis.available());
int readCount = -1;
byte [] data = new byte[fis.available()];
while((readCount = fis.read(data)) != -1){
System.out.println(readCount);
System.out.println(new String(data,0,readCount));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
// 关闭资源
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
14.4.2 OutputStream
这个抽象类是表示字节输出流的所有类的超类。
1.FileOutputStream
OutputStream
FileOutputStream
write(int data) 每次写入一个字节
FileOutputStream 类构造可以传入一个布尔类型的参数 表示是否追加 默认为false 不追加 传入true为追加
14.5 字符流
14.5.1 Reader
用于读取字符流的抽象类。
1. InputStreamReader
字符流读取类
read() 每次读取一个字符 返回值为读取的内容的ASCII码或者Unicode编码 未读取到返回-1
read(char [] data) 每次读取一个字符数组 返回值为读取字符的个数 读取内容存放在数组中 未读取到返回-1
读取文件如果产生乱码 可以指定读取文件的编码格式