Java基础复习笔记
第01章:Java语言概述
1. Java基础学习的章节划分
第1阶段:Java基本语法
Java语言概述、Java的变量与进制、运算符、流程控制语句(条件判断、循环结构)、break\continue、
IDEA开发工具的使用、数组
第2阶段:面向对象编程(基础、进阶、高级)
第3阶段:Java高级应用
异常处理、多线程、集合框架、File类与IO流、网络编程、日期相关的API与比较器、反射、Java8-17新特征
语言 = 语法 + 逻辑
2. 计算机的构成
- 硬件:CPU、内存、硬盘、输入设备、输出设备、调制解调器
- 软件
3. 软件
- 软件:即一系列按照
特定顺序组织
的计算机数据
和指令
的集合。- 有系统软件和应用软件之分。
- 系统软件:windows、mac os、android、ios、linux
- 应用软件:qq、微信、音乐播放器等
- 有系统软件和应用软件之分。
4. 人机交互方式
-
图形化界面的方式
-
命令行的方式交互
-
DOS命令(掌握)
- cd cd.. cd/ md rd del exit cls等
5. 语言
-
计算机语言的分代
- 第1代:机器语言:0和1
- 第2代:汇编语言:出现了助记符
- 第3代:高级语言:
- 面向过程阶段:C
- 面向对象阶段:C++,Java,C#,Python,JS等
-
没有“最好”的语言,只有在特定场景下相对来说,最适合的语言而已。
6. Java概述
-
Java简史
- 1995诞生
- 1996:jdk1.0版本
- 2004:Java5.0(jdk1.5)--->里程碑式的版本;J2SE->JavaSE、J2EE->JavaEE、J2ME->JavaME
- 2014:Java8.0--->里程碑式的版本;目前,市场占有率仍然很高。(lambda表达式、StreamAPI)
- 后续:Java11、Java17都属于LTS(长期支持版本)
-
SUN、Oracle、Google等
-
Java之父:詹姆斯·高斯林
-
Java的应用场景:
- JavaSE:开发桌面级应用 (不靠谱)
- JavaEE:开发企业级后台应用
- JavaME:开发小型设备的应用(不靠谱)
----> JavaEE、Android应用、大数据开发
7. JDK的下载、安装及环境变量的配置(重点)
- jdk下载:官网下载
- 安装:jdk8.0和jdk17.0 (傻瓜式安装)
- path环境变量的配置(重点)
8. 第1个Java程序
新建java文件:PersonInfo.java
class PersonalInfo{
public static void main(String[] args){
System.out.println("姓名:家琪琪\n");
//System.out.println();//换行操作
System.out.println("性别:女");
System.out.println("住址:成都青创园");
}
}
针对于第1个程序的小结及常见问题的分析
1. HelloWorld程序如下:编写在HelloWorld.java文件中
class HelloJava{
public static void main(String[] args){
System.out.println("HelloWorld!!");
System.out.println("HelloWorld!!");
System.out.println("你好,世界!");
}
}
2. Java程序要想执行成功,需要如下的三个步骤:
第1步:编写:将java源代码编写在.java结尾的源文件中。
第2步:编译:针对于编写好的源文件进行编译操作。格式:javac 源文件名.java
编译以后,会生成一个或多个.class结尾的字节码文件。字节码文件的名称即为源文件中对应的类名
第3步:运行:针对于编译好的字节码文件,进行解释运行操作。格式: java 字节码文件名 或 java 类名
3. 针对于编写过程来说:
3.1 class:是一个关键字,小写,后面跟着一个类名。
3.2 编写的类或方法必须使用一对{}。
3.3
> main()作为程序的入口出现!格式如下:
public static void main(String[] args)
> main()的格式是固定的!大家刚开始学习,可以"死记硬背"一下。
> 但是,可以考虑修改为如下的格式:
方式1:public static void main(String args[])
方式2:public static void main(String[] a) args:是arguments的缩写
3.4 输出语句的编写:
> System.out.println(123); 表示:输出123之后换行
> System.out.print(123); 表示:输出123之后不需换行
3.5 编译过程中的小结:
> 编译源文件。此时要求在源文件所在的路径下执行"javac 源文件名.java"的操作
可能编译时报错的情况:
情况1:如果源文件名写错(不包括大小写不同的情况)或者不是在源文件所在的路径下执行javac操作则会报错。
情况2:编写的程序中有非法的语法或非法的字符。
> 缺少必要的大括号、大小写的问题(Java是严格区分大小写的)、出现的标点符号必须是英文格式下的
3.6 解释运行过程的小结:
> 针对于字节码文件对应的类,执行java.exe命令。格式:java 类名。
> 此操作需要在字节码文件所属的路径下执行。
可能运行时报错的情况:
情况1:执行字节码文件所在的路径不对或字节码文件的名写错了(注意,java严格区分大小写,如果大小写出错了,仍然认为文件名写错了)。
情况2:可以出现运行时异常(放到第9章中讲解)
3.7 说明
1. Java是严格区分大小写的
2. 每一行执行语句必须以;结尾
3. 程序在编写过程中,为了可读性更强,增加必要的缩进,使用tab键即可。
4. 一个源文件中可以声明一个或多个类。
一个源文件中最多只能有一个类声明为public。
声明为public的类的类名必须与源文件名相同。
9. 注释
- 掌握:单行注释、多行注释
- 作用1:对程序中的代码进行解释说明
- 作用2:有助于调试程序
- 熟悉:文档注释 (可以被javadoc解析)
10. API文档
- API:(Application Programming Interface,应用程序编程接口)是 Java 提供的基本编程接口。
- 像String、System都属于API
- API文档:用于解释说明API如何使用的一个文档。
第02章:变量与进制
1. 关键字(keyword)
- 关键字:被Java语言赋予特殊含义的字符串。
- 注意点:关键字都是小写的!
- Java规范了50个关键字(包含了goto、const两个保留字)
- 额外的三个字面量true、false、null虽然不是关键字,但是我们也把他们看做是关键字。
2. 标识符
- 凡是可以自己命名的地方,都是标识符。
- 标识符都有哪些位置?类名、变量名、包名、方法名、接口名、常量名等
- 标识符的命名规则
(如果不遵守,编译不通过。要求大家遵守)
由26个英文字母大小写,0-9 ,_或 $ 组成
数字不可以开头。
不可以使用关键字和保留字,但能包含关键字和保留字。
Java中严格区分大小写,长度无限制。
标识符不能包含空格。
- 标识符的命名规范
(如果不遵守规范,不影响程序的编译和运行。建议大家遵守,否则容易被鄙视)
包名:多单词组成时所有字母都小写:xxxyyyzzz。
例如:java.lang、com.atguigu.bean类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz
例如:HelloWorld,String,System等变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZzz
例如:age,name,bookName,main,binarySearch,getName常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ
例如:MAX_VALUE,PI,DEFAULT_CAPACITY
- 标识符在声明时,要见名知意!
3. 变量的基本使用
- 内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化
- 变量的构成包含三个要素:数据类型 变量名 变量值
- Java中变量声明的格式:数据类型 变量名 = 变量值;
- Java是一门强类型的语言。即每一个变量都规定了具体的类型。
- 使用变量注意:
- Java中每个变量必须先声明,后使用。
- 使用变量名来访问这块区域的数据。
- 变量的作用域:其定义所在的一对{ }内。
- 变量只有在其作用域内才有效。出了作用域,变量不可以再被调用。
- 同一个作用域内,不能定义重名的变量。
4. 基本数据类型的变量
变量按照数据类型来分:
基本数据类型:整型(byte \ short \ int \ long ) 、浮点型(float \ double ) 、字符型char 、布尔型boolean
引用数据类型:类(class)、接口(interface)、数组(array); 注解(annotation)、枚举(enum)、记录(record)
- 整型变量
//1. 整型的使用:
//byte(1个字节=8bit,-128~127) \ short(2字节) \ int(4字节) \ long(8字节)
byte b1 = 12;
b1 = 127;
//①声明变量以后,给变量赋的值必须在变量类型所允许的范围内变化。
//b1 = 128;//因为超出了byte的范围,所以报错
//② 给long类型变量赋值时,要求以"l"或"L"结尾
short s1 = 123;
int i1 = 1234;
long l1 = 12313123L;
System.out.println(l1);
//③ 实际开发中,如果没有特殊需求的话,推荐大家使用int类型来定义整型变量。
//④ 默认情况下,整型常量都是int类型
//int i2 = i1 + 2;
- 浮点类型
//2. 浮点型的使用:
// float(4字节) / double (8字节)
//① float虽然占用的空间比long小,但是表数范围比long大,进而float精度不高。
//② 给float类型变量赋值时,要求以"f"或"F"结尾。否则,编译不通过
double d1 = 123.456;
//d1 = 123.456456456456456456; //体会double的精度也有限
System.out.println(d1);
float f1 = 123.456f;
System.out.println(f1);
//③ 实际开发中,如果没有特殊需求的话,推荐大家使用double类型来定义浮点型变量。
//④ 默认情况下,浮点型常量都是double类型
double d2 = d1 + 12.34; //12.34是常量,是double类型
- char类型(字符类型)
//3.字符类型的使用:char (2字节)
//① 一般情况下,我们使用一对''表示一个具体的字符。
//说明:char定义变量的话,''内有且只能有一个字符
char c1 = 'a';
//编译不通过
//char c2 = '';
//char c3 = 'ab';
//② char类型变量的定义方式
//方式1:最常见的方式
char c4 = '中';
char c5 = '1';
char c6 = 'す';
//方式2:直接使用Unicode值来表示字符型常量
char c7 = '\u0023';
System.out.println(c7);
//方式3:使用转义字符
char c8 = '\n';
char c9 = '\t';
System.out.println("hello" + c8 + "world");
System.out.println("hello" + c9 + "world");
//方式4:使用字符对应的ascii码值进行赋值
char c10 = 'a';
System.out.println(c10 + 1);
char c11 = 97;
System.out.println(c10 == c11);//true
- 布尔类型(boolean)
//① 不谈boolean占用内存空间的大小
//② boolean类型只能取两个值之一:true 、 false
boolean b1 = true;
boolean b2 = false;
//③ 开发中,我们常常在if-else结构、循环结构中使用boolean类型
boolean isMarried = false;
if(isMarried){
System.out.println("很遗憾,不是单身了");
}else{
System.out.println("不错,可以多谈几个女朋友了");
}
5. 基本数据类型变量间的运算规则
5.1 自动类型提升规则
byte、short、char ---> int ---> long ---> float ---> double
说明:
① 容量小的变量和容量大的变量做运算时,运算的结果是容量大的变量的数据类型。
(此时的容量小、容量大指的是存储数据的范围的大小,并非占用内存空间的大小。比如:float的容量要大于long的容量)
② byte、short、char 三者之间的变量做运算,结果是int类型。
③ 不管是自动类型提升规则,还是强制类型转换规则都只针对于基本数据类型中的7种进行操作(除了boolean类型)
5.2 强制类型转换规则
说明:
①看做是自动类型提升规则的逆运算
② 如果需要将容量大类型的变量转换为容量小的类型的变量时,就需要使用强制类型转换
③ 强制类型转换需要使用一对()表示
④ 使用强转符转换时,可能造成精度的损失
6. String与8种基本数据类型变量间的运算
- String的理解
String,即为字符串类型。
声明String类型的变量,可以使用一对""表示。
一对""内可以声明0个、1个或多个字符
- String与基本数据类型变量间的运算
String类型是可以与8种基本数据类型的变量做运算的。
String只能与8种基本数据类型的变量做连接运算:+
连接运算的结果只能是String类型。
7. 进制(了解)
- 计算机中存储和运算的
所有数据
都要转为二进制
。包括数字、字符、图片、声音、视频等。
7.1 常见的几种进制
-
熟悉:
-
十进制(decimal)
- 数字组成:0-9
- 进位规则:满十进一
-
二进制(binary)
- 数字组成:0-1
- 进位规则:满二进一,以
0b
或0B
开头
-
八进制(octal):很少使用
- 数字组成:0-7
- 进位规则:满八进一,以数字
0
开头表示
-
十六进制
- 数字组成:0-9,a-f
- 进位规则:满十六进一,以
0x
或0X
开头表示。此处的 a-f 不区分大小写
-
7.2 二进制与十进制间的转换
熟悉:二进制与十进制间的转换(见ppt)
-
表示二进制整数时,最高位为符号位。0:正数;1:负数。
-
二进制整数在存储时,涉及到原码、反码、补码。
-
正数:三码合一。
-
负数:负数的原码,除符号位外,各个位取反,得到负数的反码。
负数的反码+1,得到负数的补码。
-
-
计算机底层都是以二进制
补码
的形式存储数据的。
7.3 二进制与其它进制间的转换
- 了解:二进制与八进制、十六进制间的转换
第03章:IDEA的安装与使用
1. 认识IDEA的地位、特点
- Java开发中占比第1。
- Eclipse?IDEA?① 符合人体工程学 ② 功能强大
2. IDEA的下载、安装、注册
略
3. IDEA的基本使用
- 在IDEA中能创建一个工程:Project。
- 在工程的src下写一个HelloWorld,并运行
- 安装课件中的第5节中的设置,作必要的修改。
4. 熟悉工程、module中jdk和设置语言级别操作
关于工程:
关于Module:
添加SDK:
5. 熟悉Project-Module-Package-Class的关系
- 上一级和下一级之间是一对多的关系。
- 掌握:新建Project、新建Module、删除Module、导入老师的Module(难点)
6. 关于IDEA的其它操作
- 模板的使用
- 快捷键的使用
- Debug程序
- 插件的使用
第04章:运算符与流程控制
1. 运算符之1:算术运算符
+ - + - * / % ++ -- +
- % : 结果与被模数的符号相同。常用来判别是否能整除一个数。
- (前)++ 与 (后)++ ;(前)-- 与 (后)--
2. 运算符之2:赋值运算符
= += -= *= /= %=
- = : 与 == 的区别。= 的右边是变量或常量。"连续赋值" (int i,j;i = j = 10;)
- += -= *= /= %= :运算后,不会改变变量的类型。(int i = 1; i *= 0.1; )
3. 运算符之3:比较运算符
> < >= <=
== !=
> < >= <=
只能适用于7种基本数据类型(不含boolean)== !=
适用于8种基本数据类型、引用数据类型。- 比较运算符的结果是boolean类型。
4. 运算符之4:逻辑运算符
& && | || ^ !
- 逻辑运算符操作的是boolean类型,结果也是boolean类型
- & 与 && 的区别;| 与 || 的区别
5. 运算符之5:位运算符(非重点)
<< >> >>>
& | ^ ~
- 位运算符操作的整数类型
- << >> >>>的应用场景
6. 运算符之6:条件运算符
(条件表达式)? 表达式1 : 表达式2
规则:
判断条件表达式是true还是false,如果是true,则执行表达式1;如果是false,则执行表达式2
如果将运算的结果赋给一个变量的话,要求:表达式1与表达式2的类型一致。(相同 或 满足自动类型提升的规则即可)
- 案例:获取两个数的较大值;获取三个数的最大值
- 与分支结构的if-else对比:
凡是可以使用条件运算符的地方,都可以改写为if-else。反之,不一定。
在既可以使用条件运算符,又可以使用if-else的场景下,推荐使用条件运算符。因为条件运算符效率稍高。
7. 运算符的优先级
-
我们在开发中,如果希望某个运算符优先运算的话,主动的添加一对()。
-
常见的一些运算符优先级谁高谁低呢?基本上是
如你所想
。int x = 10; boolean y = false; if(x++ == 10 && y = true){...}
-
大家在开放时,如果涉及到多个运算符做运算,建议可以
分行写
。
8. 流程控制语句概述
顺序结构:略,即代码从上往下依次执行
分支结构:if-else 、 switch-case
循环结构:for、while、do-while
foreach放到集合章节中讲解
9. 分支结构1:if-else
- 格式
格式1:
if(条件表达式){
语句块;
}
格式2:"二选一"
if(条件表达式) {
语句块1;
}else {
语句块2;
}
格式3:"多选一"
if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
}else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}
- 说明
说明1:
> 如果多个条件表达式之间是"互斥"关系(或没有交集的关系),则多个条件表达式谁写在上面,谁写在下面都可以。
> 如果多个条件表达式之间是包含关系,则通常需要将条件表达式范围小的声明在条件表达式范围大的上面。
说明2:
> 我们可以在程序使用if-else的嵌套结构
> 如果if-else中一对大括号内的语句块只有一行执行语句,则此一对大括号可以省略。但是,不建议大家省略!
说明3:
> 开发中,在一些具体的题目中,可以在if-else if -else if -... -else 结构中省略else结构。
10. 分支结构2:switch-case
- 格式
switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
// ...
[default:
语句块n+1;
break;
]
}
- 说明
1. switch-case的执行过程:
根据switch中表达式的值,依次匹配一对{}内的case结构。一旦表达式与某个case的常量值相等,则执行此case中的语句块。
执行完此语句块之后,如果此case中包含break,则结束当前switch-case结构。
如果此case中不包含break,则会继续执行其后的case中的语句块(case穿透的场景)。直到遇到break或执行完default,才会结束switch-case结构。
2. 说明:
> 在switch-case结构中可以使用break关键字,一旦执行,表示终止(或退出)当前switch-case结构。
> 开发中,在使用switch-case的场景中,不加break的情况要多于加break的情况。
> switch中的表达式只能是特定的如下类型的变量:
byte \ short \ char \ int ; 枚举类型(jdk5.0新增) \ String(jdk7.0新增)
> case后的常量值,需要与switch中表达式的值进行==的判断。如果返回true,则执行此case中的语句块。返回false,则不执行。
> default类似于if-else结构中else。 可选的,且位置是灵活的。
- if-else 与 switch-case的对比
> 针对的变量的类型来讲,if-else没有限制,而switch-case有类型的限制,且建议case匹配的情况有限、不多的场景。
> 二者的转换:凡是使用switch-case结构的,都可以转换为if-else。反之,不成立。
> 开发中,在既可以使用switch-case,又可以使用if-else的情况下,推荐使用switch-case。因为其效率稍高。
> if-else的主要优势:涉及到任何的分支结构,都可以使用if-else实现
switch-case的主要优势:在可以使用if-else和switch-case的情况下,效率稍高。
case穿透。
11. 循环结构1:for
- 循环的概述
凡是循环结构,都有如下的4个要素:
> ① 初始化条件部分
> ② 循环条件部分 -->是boolean类型
> ③ 循环体部分
> ④ 迭代条件部分
- 格式
for(①;②;④){
③
}
执行过程:① - ② - ③ - ④ - ② - ③ - ④ - ...- ②
- 说明
1. 注意:循环条件部分必须是boolean类型。
2. break关键字的使用
> break可以使用在循环结构中 (复习:还可以使用在switch-case中)
> 一旦执行,就跳出当前循环结构。
12. 循环结构2:while
- 格式
①
while(②){
③
④
}
- 执行过程
① - ② - ③ - ④ - ② - ③ - ④ - ...- ②
- 说明
for循环和while循环一定可以相互转换。
for、while循环的区别:初始化条件的作用域不同。while循环的初始化条件在while循环结束后,仍然有效。
13.循环结构3:do-while
- 格式
①
do{
③
④
}while(②);
- 执行过程
① - ③ - ④ - ② - ③ - ④ - 。。。- ②
- 说明
do-while相较于其他循环的区别:至少执行一次循环体。
在循环条件第1次判断时,如果是true的情况下,三个循环结构可以相互转换的。
- 使用场景
for循环:有明确的循环、遍历次数时。比如:遍历100以内的自然数、遍历数组
while循环:没有明确的循环、遍历的次数时。比如:使用迭代器遍历集合。
do-while循环:确保至少执行一次。
14. "无限"循环
- 结构
while(true) 、 for(;;)
- 使用场景
不确定循环的次数时,使用此结构
- 结束循环的方式
在循环内部,满足某个条件的情况下,执行break。
- 注意:必须确保此循环可以结束。否则就是死循环!我们开发中要避免死循环
15. 嵌套循环
- 格式
外层循环{
内层循环{
}
}
-
说明:上述的外层循环、内存循环可以是for、while、do-while
-
技巧:
- 外层循环执行m次,内层循环执行n次。意味着内层循环的循环体执行 m * n次
- 外层控制行数,内层控制列数
16. break、continue关键字的使用
相同点:① 都可以使用在循环结构中 ② 其后不能编写执行语句
不同点:① 结束循环结构;结束当次循环 ②使用范围:break:switch-case结构中使用
17. Scanner的使用、随机数的获取
- Scanner的使用
1. 如何从键盘获取数据? 使用Scanner类
2. 如何使用Scanner类,从键盘获取数据呢? (掌握)
步骤1:导包
import java.util.Scanner
步骤2:创建Scanner的对象(或实例)
Scanner scanner = new Scanner(System.in);
步骤3:通过Scanner的对象,调用Scanner类中声明的方法,从键盘获取指定类型的变量
scanner.nextXxx()
步骤4:关闭Scanner
scanner.close();
3. Scanner类中提供了如下的获取不同类型变量的方法:
获取byte: nextByte();
获取short: nextShort();
获取int: nextInt();
获取long: nextLong();
获取float: nextFloat();
获取double: nextDouble();
获取boolean: nextBoolean();
注意,没有提供获取字符的方法。我们可以通过获取字符串的方法,来获取字符。
获取String: next() / nextLine()。
如何获取一个字符:next().charAt(0)
- 如何获取随机数
1. 调用Math类中的random(),可以获取一个[0.0,1.0)范围内的随机浮点数。
2. 如何获取[0,9]范围的随机整数:(int)(Math.random() * 10);
如何获取[1,10]范围的随机整数:(int)(Math.random() * 10) + 1;
如何获取[0,100]范围的随机整数:(int)(Math.random() * 101);
如何获取[10,100]范围的随机整数:(int)(Math.random() * 91) + 10; //[10,100]
公式:如何获取[a,b]范围的随机整数:(int)(Math.random() * (b - a + 1) + a)
18. 阶段项目1:谷粒记账软件
略
第05章:数组
1. 数组的概述(理解)
1. 数组的理解
概念:
数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,
并通过编号的方式对这些数据进行统一管理。
简称:多个相同类型的数据的组合
Java中的容器:数组、集合框架(用于存储不同特点的多个数据)
2. 几个相关的概念
> 数组名(即为容器的名称)
> 元素 (即为数组中具体的一个个的数据)
> 数组的长度(容器中元素的个数)
> 数组的角标、下标、下角标、索引、index (即为数组中元素的具体位置。从0开始)
3. 数组的特点:
- 数组本身是`引用数据类型`,而数组中的元素可以是`任何数据类型`,包括基本数据类型和引用数据类型。
- 创建数组对象会在内存中开辟一整块`连续的空间`。占据的空间的大小,取决于数组的长度和数组中元素的类型。
- 数组中的元素在内存中是依次紧密排列的,有序的。
- 数组,一旦初始化完成,其长度就是确定的。
- 数组的`长度一旦确定,就不能修改`。
- 我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。
- 数组名中引用的是这块连续空间的首地址。
4. 复习:变量按照数据类型的分类
4.1 基本数据类型:byte \ short \ int \ long ;float \ double ;char ;boolean
4.2 引用数据类型:类、数组、接口; 枚举类型、注解类型、记录类型(Record)
5. 数组的分类
5.1 按照元素的类型:基本数据类型元素的数组、引用数据类型元素的数组
5.2 按照数组的维数来分:一维数组、二维数组、....
2. 一维数组的使用(重点)
(6个基本点)
> 数组的定义:静态初始化、动态初始化
> 数组元素的表示:使用角标,角标从0开始,到数组的长度-1结束。
> 数组的长度:length
> 遍历数组:for循环
> 数组元素的默认值:记住。后续类中属性的默认值也如此。
> 数组的内存解析(难点)---> 具体图示见chapter06章节的module中即可。
3. 二维数组的使用(熟悉)
- 二维数组的理解
> 角度1:一个一维数组又作为了另一个数组arr的元素。则数组arr就称为二维数组。
> 角度2:一个数组arr1的元素,仍是是一个数组,则arr1称为二维数组
> 数组,属于引用数据类型;数组的元素也可以是引用数据类型。--> 数组的元素,还可以是数组。
> 说明:其实Java中不存在二维、三维、..数组,只是将一个上述的arr或arr1称为是二维数组。
> 区分:外层元素、内层元素
- 基本内容
二维数组的使用(6个基本点)
> 数组的定义
> 数组元素的调用
> 数组的长度
> 数组的遍历
> 数组元素的默认初始化值(稍难)
> 数组的内存解析(难点)---> 具体图示见chapter06章节的module中即可。
- 数组元素的默认值
1. 二维数组元素的默认初始化值
1.1 动态初始化方式1:(比如:int[][] arr = new int[3][4])
外层元素:存储的是地址值。(具体来说,就是外层元素指向的一维数组的地址值)
内层元素:与一维数组元素的默认值相同。
> 整型:0
> 浮点型:0.0
> 字符型:0 或 '\u0000'
> 布尔型:false
> 引用类型:null
1.2 动态初始化方式2:(比如:int[][] arr = new int[3][])
外层元素:null
内层元素:不存在。一旦调用会报异常(NullPointerException)
4. 数组的常用算法(熟练)
- 算法常用操作1
1. 数值型数组特征值统计
这里的特征值涉及到:平均值、最大值、最小值、总和等
2. 数组元素的赋值(实际开发中,遇到的场景比较多)
3. 数组的复制、赋值
4. 数组的反转
- 算法常用操作2
1. 数组的扩容与缩容
2. 数组元素的查找(或搜索)
顺序查找:
> 优点:简单,好理解,数组没有任何的前提限制。(比如:有序)
> 缺点:相较于二分法查找更慢一些。
二分法查找:
> 优点:相较于顺序查找,更快。O(logN)
> 缺点:必须此数组有序。
3. 排序算法
3.1 排序算法的衡量标准:
> 时间复杂度:更为关心的标准。
Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n^2)<Ο(n^3)<…<Ο(2^n)<Ο(n!)<O(n^n)。
> 空间复杂度:常出现以空间换时间的做法。
> 稳定性
3.2 排序的分类:内部排序、外部排序
内部排序的具体算法:十种。
我们需要关注的几个排序算法:
> 冒泡排序:简单、容易实现;企业笔试中容易考。时间复杂度:O(n^2)。要求大家可以手写。
> 快速排序:快速、开发中需要排序情况下的首选。时间复杂度:O(nlogn)。要求大家至少可以说明其实现思路。
5. Arrays:数组的工具类(熟悉)
1. Arrays类所在位置
java.util.Arrays
2. 作用:
封装了针对数组的常用操作。比如:排序、二分查找、比较数组是否相等、遍历等。
3. 常用方法:
sort(int[] arr) / binarySearch(int[] arr,int target) / toString(int[] arr)
6. 小结:数组中的常见异常
1. 数组的使用中常见的异常小结
> ArrayIndexOutOfBoundsException:数组角标越界异常
> NullPointerException:空指针异常
2. 出现异常会怎样?如何处理?
> 一旦程序中出现异常,且没有处理的情况下,程序就终止执行。
> 目前大家编程时,如果出现上述异常。回来根据异常的提示,修改代码,确保后续运行不再出现。
第06章:面向对象-基础
面向对象内容的三条主线:
> 类及类的内部成员:属性、方法、构造器;代码块、内部类
> 面向对象的三大特征:封装性、继承性、多态性
> 其它关键字的使用:package、import、this、super、static、final、abstract、interface等
1. 理解:面向过程vs面向对象
简单的语言描述二者的区别
> 面向过程:以`函数`为组织单位。是一种“`执行者思维`”,适合解决简单问题。扩展能力差、后期维护难度较大。
> 面向对象:以`类`为组织单位。是一种“`设计者思维`”,适合解决复杂问题。代码扩展性强、可维护性高。
2.2 二者关系:在面向对象的编程中,具体的方法中,仍然会体现面向过程的思想。所以二者是合作关系。
2. 面向对象的要素:类、对象
-
区分类与对象
- 类:抽象的、概念上的定义
- 对象:具体的,实实在在存在的,由类派生出来的
-
设计类,就是设计类的成员:属性、方法
-
面向对象完成具体功能的操作的三步流程(非常重要)
步骤1:创建类,即设计类的内部成员(属性、方法) 步骤2:创建类的对象。 步骤3:通过"对象.属性" 或 "对象.方法"的方式,完成相关的功能。
-
对象的内存解析
- JVM内存分配:虚拟机栈、堆、方法区(目前用不到)、程序计数器(略)、本地方法栈(略)
- 虚拟机栈:存放的是方法对应的栈帧,每个栈帧中存放方法中声明的局部变量。
- 堆:new出来的"东西":数组实体、对象实体(含成员变量)
- 创建类的1个对象、创建类的多个对象(内存解析图建议大家都自己画画)
- JVM内存分配:虚拟机栈、堆、方法区(目前用不到)、程序计数器(略)、本地方法栈(略)
3. 类的成员之一:属性(重点)
1.变量的分类:
- 角度一:按照数据类型来分:基本数据类型(8种)、引用数据类型(数组、类、接口;注解、枚举、记录)
- 角度二:按照变量在类中声明的位置来分:成员变量、局部变量
2. 成员变量的几个称谓:
成员变量 <=> 属性 <=> field(字段、域)
3. 区分成员变量 vs 局部变量
3.1 相同点:(了解)
> 都有三个要素(数据类型、变量名、变量值)
> 声明的格式相同:数据类型 变量名 = 变量值
> 变量都是先声明后使用
> 变量都有作用域,在其作用域内是有效的
3.2 不同点:
① 类中声明的位置的不同:
> 成员变量:声明在类内部、方法等结构的外部。
> 局部变量:声明在方法内部、方法的形参、构造器的内部、构造器的形参、代码块的内部等
② 在内存中分配的位置不同:
> 成员变量:随着对象实体在堆空间进行分配而分配(或存储)
> 局部变量:存储在栈空间。
③ 生命周期:
> 成员变量:随着对象的创建而产生,随着对象的消亡而消亡
> 局部变量:(以方法为例)随着方法的调用而产生,随着方法的调用结束而消亡。
> 拓展:每一个方法的执行,都对应着一个栈帧加载进栈中。局部变量就存储在每个方法对应的栈帧中。
当方法执行结束时,对应的栈帧就弹出栈,进而栈帧中的局部变量也弹出,进而消亡。
④ 作用域:
> 成员变量:在整个类的内部是有效的。---> 类的方法中是可以调用类中的成员变量的。
> 局部变量:以方法为例,作用域仅限于方法内部。
⑤ 是否可以有权限修饰符进行修饰:(超纲)
> 成员变量:可以被不同的权限修饰符进行修饰。(后面讲封装性时,具体说:private、public、protected、缺省)
> 局部变量:不可以被权限修饰符进行修饰。一旦修饰,编译不通过。
⑥ 是否有默认值:
> 成员变量:都有默认值
默认值的情况与不同类型的一维数组的元素的默认值相同。
> 整型:0
> 浮点型:0.0
> 字符型:0
> 布尔型:false
> 引用类型:null
> 局部变量:没有默认值。
意味着在调用之前必须要显示赋值。如果不赋值,就报错。
> 特别的:方法的形参在方法调用时赋值即可。
4. 类的成员之二:方法(重点)
4.1 方法的使用
1. 使用方法的好处
将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码。
2. 使用举例
- Math.random()的random()方法
- Math.sqrt(x)的sqrt(x)方法
- System.out.println(x)的println(x)方法
- new Scanner(System.in).nextInt()的nextInt()方法
- Arrays类中的binarySearch()方法、sort()方法、equals()方法
3. 方法声明的格式
举例:public void eat()
public void sleep(int hour)
public String getName()
public String playGame(String game)
格式:
权限修饰符 返回值类型 方法名(形参列表){
方法体
}
4. 具体的方法声明的细节
4.1 权限修饰符:体现此方法被调用时,是否能被调用的问题。(主要放到封装性的时候讲解)
暂时,大家在声明方法时,先都使用public修饰即可。
4.2 返回值类型:(难点)
> 分类:有具体的返回值的类型(指明具体的数据类型) 、 没有返回值类型(使用void)
> 情况1:有具体的返回值的类型的要求:既然有返回值的类型,则要求此方法在执行完时,一定要返回
满足此类型的一个变量或常量。
> 内部使用"return 变量(或常量)"的方法,返回数据
> 情况2:没有返回值类型:内部就不需要使用return结构了。
> (难点)其实,我们在此方法中也可以使用return,仅表示结束此方法。
开发中,设计一个方法时,是否需要设计返回值类型?
> 根据题目的要求设计。
> 具体问题具体分析:调用完此方法之后,是否需要一个结果性的数据,供之后使用。如果有必要,就设计有返回值类型的场景即可。
4.3 方法名:属性标识符,定义时需要满足标识符的命名规则、规范、"见名知意"。
4.4 形参列表:(难点)
> 在一个方法的一对小括号中可以声明形参列表,形参的个数可以为0个、1个或多个。
> 如果有形参的话,格式为: (数据类型1 形参名1,数据类型2 形参名2,...)
开发中,设计一个方法时,是否需要提供形参呢?
> 根据题目的要求设计。
> 具体问题具体分析:调用此方法时,是否存在不确定性的数据。如果有,则以形参的方法传入此不确定的数据。
4.5 方法体:即为调用方法时,执行的代码。可以使用当前方法声明的形参,使用类中的成员变量。
5. 注意点
> Java里的方法`不能独立存在`,所有的方法必须定义在类里。
> 方法内可以使用类中的成员变量
> 方法内不可以定义方法,但是方法内可以调用本类中的其它方法。 ---> 递归方法中谈方法内自己调用自己。
> 类中不可以定义多个相同的方法。---> 方法的重载
4.2 return关键字
1. return的作用
> 作用1:结束当前方法的执行
> 作用2:"return + 变量/常量"结构在方法结束的同时,还可以返回一个数据。
2. 使用注意点:
与break、continue类似,其后不能声明执行语句。
5. 内存的分配使用
5.1 方法调用的内存解析
- 形参:方法声明时,一对小括号内声明的参数,简称:形参
- 实参:方法调用时,实际赋值给形参的值,称为:实参
过程概述:
每当调用一个方法时,方法就以栈帧的方法加载进虚拟机栈中。方法中声明的局部变量存放在栈帧中。
当方法执行结束时,栈帧就会弹出栈。栈帧中存放的局部变量也随之消亡。
5.2 目前为止,内存分析(重要)
- 基本原则
1、JVM中内存划分
- 栈:以栈帧为基本单位(每个方法对应一个栈帧);栈帧里存放局部变量。
- 堆:new 出来的"东西":数组实体(含数组元素)、对象实体(含成员变量)
2、区分清成员变量(类内部、方法外声明的)、局部变量(方法的形参、方法内定义的变量、构造器内定义的变量、构造器的形参、代码块内部等)
3、值传递机制:
- 如果参数是基本数据类型,传递的是基本数据类型变量存储的
数据值
- 如果参数是引用数据类型,传递的是引用数据类型变量存储的
地址值
6. 再谈方法
6.1 方法的重载(overload )
1. 定义:
在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。满足这样特点的多个方法彼此之间称为
方法的重载。
2. 总结为:"两同一不同":同一个类、相同的方法名;形参列表不同(参数的个数不同,参数的类型不同)
> 重载与否与形参名没有关系、返回值类型没有关系、权限修饰符没有关系
3. 举例
> Arrays中的重载的binarySearch(xxx) \ equals(xxx,xxx) \ toString(xxx)
> System.out的多个重载的println();
4. 如何判断两个方法是相同的呢?(换句话说,编译器是如何确定调用的某个具体的方法呢?)
> 在同一个类中,只要两个方法的方法名相同,且参数列表相同(参数的个数相同且参数类型相同),
则认为这两个方法是相同的。
> 与方法的权限修饰符、返回值类型、形参名都没有关系。
> 在同一个类,不能编写两个相同的方法的。
后续会讲:方法的重写(overwrite / override)
面试题:方法的重载与重写的区别?
throw \ throws
Collection \ Collections
final \ finally \ finalize
String \ StringBuffer \ StringBuilder
ArrayList \ LinkedList
。。。
== 、equals()
抽象类、接口
6.2 可变个数形参的方法
1. 使用场景
JDK5.0的新特性。
如果方法在调用时,参数的类型是确定的,但是参数的个数不确定,则可以考虑使用可变个数形参的方法。
2. 格式:类型 ... 变量名
3. 说明:
> 可变个数形参的方法在调用时,可以传入0个,1个或多个参数。
> 可变个数形参的方法与参数是其它类型的同名方法构成重载。
> 可变个数形参的方法与参数是同样类型的数组参数构成的方法,在方法名相同的情况下,不构成重载。即两个方法不能
同时存在。
> 可变个数的形参在编译器看来就是同一个类型的数组参数
> 规定:可变个数的形参需要声明在方法形参列表的最后
> 一个方法的形参位置,最多只能有一个可变个数的形参
/*
String sql1 = "update customers set name = ?,salary = ? where id = ?";
String sql2 = "delete from customs where id = ?";
public void update(String sql,Object ... objs){
//使用可变形参objs中的各个元素值给形参sql中的?赋值
}
*/
6.3方法的参数传递机制(难点、重点)
1. 对于方法内声明的局部变量来说:
> 如果此局部变量是基本数据类型的,则将基本数据类型变量保存的数据值传递出去
> 如果此局部变量是引用数据类型的,则将引用数据类型变量保存的地址值传递出去
2. 方法的参数的传递机制:值传递
2.1 概念(复习)
形参:方法声明时,一对小括号内声明的参数,简称:形参
实参:方法调用时,实际赋值给形参的值,称为:实参
2.2 规则
> 如果此形参是基本数据类型的,则将基本数据类型的实参保存的数据值传递给形参
> 如果此形参是引用数据类型的,则将引用数据类型的实参保存的地址值传递给形参
3. 面试题:Java中的参数传递机制是什么? 值传递机制。
6.4 递归方法(熟悉)
1. 何为递归方法?
方法自己调用自己的现象就称为递归。
2. 递归方法分类
直接递归、间接递归。
3. 使用说明:
- 递归方法包含了一种`隐式的循环`。
- 递归方法会`重复执行`某段代码,但这种重复执行无须循环控制。
- 递归一定要向`已知方向`递归,否则这种递归就变成了无穷递归,停不下来,类似于`死循环`。最终发生`栈内存溢出`。
7. 对象数组(难点)
1. 何为对象数组?如何理解?
数组中的元素,如果存储的是对象的话,则称此数组为对象数组。
2. 举例:
String[] arr = new String[10];
arr[0] = "hello";
arr[1] = new String("abc");
Person[] arr1 = new Person[10];
arr1[0] = new Person();
Phone[] arr2 = new Phone[10];
3. 内存解析:
数组名(比如:stus)存储在栈空间
创建的20个学生对象,存储在堆空间中。学生对象的地址值存储在数组的每个元素中。
8. 关键字:package、import
- package:包,指明了Java中的类、接口等结构所在的包。声明在文件的首行
- import:导入。指明在当前类中使用的其它包中的结构。声明在package下,类的声明之前。
一、package关键字的使用
1. 说明
- package,称为包,用于指明该文件中定义的类、接口等结构所在的包。
- 一个源文件只能有一个声明包的package语句
- package语句作为Java源文件的第一条语句出现。若缺省该语句,则指定为无名包。以后声明源文件时,不要使用无名包。
- 包名,属于标识符,满足标识符命名的规则和规范(全部小写)、见名知意
- 包名推荐使用所在公司域名的倒置:com.atguigu.xxx。
- 大家取包名时不要使用"`java.xx`"包,否则运行会报错
- 包对应于文件系统的目录,package语句中用 “.” 来指明包(目录)的层次,每.一次就表示一层文件目录。
- 同一个包下可以声明多个结构(类、接口),但是不能定义同名的结构(类、接口)。不同的包下可以定义同名的结构(类、接口)
2. 包的作用
- 包可以包含类和子包,划分`项目层次`,便于管理
- 帮助`管理大型软件`系统:将功能相近的类划分到同一个包中。比如:MVC的设计模式
- 解决`类命名冲突`的问题 ---> 不同包下可以命名同名的类。
- 控制`访问权限` ---> 讲了封装性,大家就清楚了。
二、import关键字的使用
- import:导入,后面跟一个具体包下的类或接口等结构。
-为了使用定义在其它包中的Java类,需用import语句来显式引入指定包下所需要的类。
相当于`import语句告诉编译器到哪里去寻找这个类`。
- import语句,声明在包的声明和类的声明之间。
- 如果需要导入多个类或接口,那么就并列显式多个import语句即可
- 如果使用`a.*`导入结构,表示可以导入a包下的所有的结构。
举例:可以使用java.util.*的方式,一次性导入util包下所有的类或接口。
- 如果导入的类或接口是java.lang包下的,或者是当前包下的,则可以省略此import语句。
- 如果已经导入java.a包下的类,那么如果需要使用a包的子包下的类的话,仍然需要导入。
- 如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是哪个类。
- (了解)`import static`组合的使用:调用指定类或接口下的静态的属性或方法
9. 面向对象的特征一:封装性
- 什么是封装性?
在Java实现项目时,将不用功能的代码封装进不同的方法。使用Java给我们提供的4种权限修饰对类及类的内部成员进行修饰。
体现被修饰的结构在调用时的可见性的大小。
- 如何体现封装性?
> 举例1:类中的属性私有化,提供公共的get()和set()方法,用于获取或设置此属性的值。
> 举例2:如果类中存在一些方法,这些方法只在类的内部使用,不希望对外暴露,则可以将这些方法声明为私有的。
> 举例3:单例设计模式。(后面讲static的时候说)
- 为什么需要封装性?
- `高内聚`:类的内部数据操作细节自己完成,不允许外部干涉;
- `低耦合`:仅暴露少量的方法给外部使用,尽量方便外部调用。
- 通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。
10. 类的成员之三:构造器
1. 构造器的理解
体会1: Scanner scan = new Scanner(System.in);
Person per = new Person();
体会2:
construct : v. 建设、建造
construction: n. 建设、建造 CCB 中国建设银行 ICBC
constructor : n.建设者,建造者
2. 构造器的作用
>作用1:搭配new关键一起,用于对象的创建
>作用2:用于初始化对象中的成员变量
3. 构造器的使用说明
> 一个类中,如果没有显式提供构造器的话,则JVM会默认提供一个空参的构造器。(其权限修饰符与类的权限修饰符相同)
> 声明格式:权限修饰符 类名(形参列表){}
> 一个类的多个构造器,彼此构成重载
> 如果一个类中,一旦显式的声明了构造器,则JVM不再提供默认的空参的构造器了。
> 结论:凡是类,都有构造器(自始至终都是对的)
11. 其它几个小知识
11.1 类中实例变量的赋值位置及顺序
0.实例变量:属于属性(或成员变量),不使用static修饰即可。
1. 在类的属性中,可以有哪些位置给属性赋值?
> ① 默认初始化 ---> 只执行一次
> ② 显式初始化 ---> 只执行一次
> ③ 构造器中初始化 ---> 只执行一次
*********************************
> ④ 创建对象以后,通过"对象.属性" 或"对象.方法"的方式,给属性赋值 ---> 可以多次执行
2. 这些位置执行的先后顺序是怎样?
① - ② - ③ - ④
3. 以上操作在对象创建过程中可以执行的次数如何?
①、②、③:只执行一次
④:可以多次执行
11.2 JavaBean
所谓JavaBean,是指符合如下标准的Java类:
- 类是公共的
- 有一个无参的公共的构造器
- 有属性,且有对应的get、set方法
11.3 UML类图
理解
11.4 匿名对象
//匿名对象
System.out.println(new Circle(2.5).findArea());
//知识点1:如上写法的匿名对象,只能被调用一次。
System.out.println(new Circle(2.5).getRadius());
//知识点2:开发中,常常将匿名对象作为参数传递给方法的形参。
Test4_5 test = new Test4_5();
test.show(new Circle(3.4));
第07章:面向对象-进阶
1. 关键字:this
- this可以调用属性、方法;构造器。
- 记住:this必须使用的场景:属性与形参同名时;调用重载的构造器
2. 面向对象特征二:继承性
-
为什么需要继承性?
- 继承的出现减少了代码冗余,提高了代码的复用性。
- 继承的出现,更有利于功能的扩展。
- 继承的出现让类与类之间产生了
is-a
的关系,为多态的使用提供了前提。
-
什么是继承性?
-
class B extends A{}
继承中的基本概念:
A类:父类、SuperClass、超类、基类
B类:子类、SubClass、派生类
-
-
继承性的基本使用
1. 有了继承性以后:
> 子类继承父类以后,就获取了父类中声明的所有的属性和方法。 ----> 刻画是否存在此属性、方法
但是,由于封装性的影响,可能导致子类不能调用。 ----> 刻画能否调用此属性、方法
> extends: 继承。还可以理解为“扩展、延展”。意味着子类在继承父类的基础上,还可以扩展自己特有的属性、方法。
父类、子类的关系不同于集合、子集的关系。
2. 默认的父类:
如果一个类显式声明了父类,则其父类为指定声明的父类。
如果一个类没有显式声明其父类,则默认继承于java.lang.Object类。
3. 补充说明:
> 一个父类可以被多个子类继承。
> 一个子类只能声明一个父类。----> Java中类的单继承性。
> Java中的类支持多层继承。
> 子类、父类是相对的概念。
> 概念:直接父类、间接父类
> Java中的任何类(除了java.lang.Object类)都直接或间接的继承于java.lang.Object类。
3. 方法的重写
1. 为什么需要方法的重写?
子类继承父类以后,父类中的方法在权限允许的情况下,子类可以直接调用。但是我们在一些场景中发现,父类
中的方法不适用于子类。怎么处理呢?需要使用方法的重写。
举例(银行账户):
class Account{ //账户
double balance; //余额
//取钱
public void withdraw(double amt){
if(balance >= amt){
balance -= amt;
System.out.println("取款成功");
}
}
//...
}
class CheckAccount extends Account{ //信用卡账户
double protectedBy; //可透支额度
//取钱
public void withdraw(double amt){
if(balance >= amt){
balance -= amt;
System.out.println("取款成功");
}else if(protectedBy >= amt - balance){
protectedBy -= amt - balance;
balance = 0;
System.out.println("取款成功");
}else{
System.out.println("取款失败");
}
}
}
2. 何为方法的重写?
子类继承父类以后,对父类中继承过来的方法进行覆盖、覆写的操作。此操作就称为方法的重写。
3. 方法重写应遵循的规则
[复习]方法声明的格式:权限修饰符 返回值类型 方法名(形参列表){ 方法体 }
具体规则:称谓:父类被重写的方法;子类重写父类的方法
> 子类重写父类的方法 与 父类被重写的方法的方法名、形参列表相同。
> 子类重写父类的方法的权限修饰符不小于父类被重写的方法的权限修饰符
> 返回值类型:
> 父类被重写的方法的返回值类型为void,则子类重写父类的方法的返回值类型必须为void
> 父类被重写的方法的返回值类型为基本数据类型,则子类重写父类的方法的返回值类型必须为同类型的基本数据类型
> 父类被重写的方法的返回值类型为引用数据类型,则子类重写父类的方法的返回值类型与父类的相同,或是父类的类型的子类。
技巧:建议子类重写父类的方法时,我们将权限修饰符、返回值类型都声明为与父类的方法相同的。
注意点:
> 子类不能重写父类中声明为private权限的方法。
4. 面试题:区分方法的重载(overload)与重写(override / overwrite)
重载:"两同一不同"
重写:子类在继承父类以后,可以对父类中的同名同参数的方法进行覆盖、覆写。此操作即为方法的重写。
具体的规则为:....。
4. 关键字:super
-
super调用父类的属性、方法;构造器
-
使用场景:子父类中出现同名属性;子类重写了父类的方法时。
super调用构造器,体现加载父类的结构。
-
5. 子类对象实例化的全过程(了解)
1. 从结果的角度来看:---->体现为类的继承性。
当子类继承父类以后,子类就获取了父类(直接父类、所有的间接父类)中声明的所有的属性、方法。
当我们创建了子类对象以后,在堆空间中就保存了子类本身及其所有的父类中声明的属性。同时,子类对象在权限允许
的情况下,可以调用子类及其所有的父类中声明的方法。
2. 从过程的角度来看:
当我们通过子类的构造器创建对象时,一定会直接或间接的调用到其直接父类的构造器,其直接父类的构造器同样会
直接或间接的调用到其父类的构造器,...,以此类推,最终一定会调用到java.lang.Object类的构造器为止。
因为我们调用过所有的父类的构造器,进而所有的父类就需要加载到内存中,进而堆空间中就有所有父类中声明的属性。
以及可以在权限允许的情况下,调用父类中声明的方法。
问题:在创建子类对象的过程中,一定会调用父类中的构造器吗? yes!
3. 问题:创建子类的对象时,内存中到底有几个对象?
只有1个!
6. 面向对象特征三:多态性
6.1 向上转型:多态
- Java中的多态性体现为:子类对象的多态性(狭义上理解)。即父类的引用指向子类的对象。
- 应用场景:当通过父类的引用调用方法时,实际执行的是子类重写父类的方法。
- 好处:多态性常使用在方法的形参位置。多态的出现,极大的减少了方法的重载,同时有利于程序的扩展。
- 举例:① equals(Object obj) ② Account - Customer : setAccount(Account acct) ③ 凡是代码中出现了抽象类、接口,都可以体现为多态性。
- 共识:Java中的多态性(广义上理解):1、子类对象的多态性。 2、方法的重写。
6.2 向下转型:多态的逆过程
- Student s = (Student)new Person(); //编译通过,运行不通过。
- 如何向下转型:使用强转符:()
- 可能出现的问题:可能会出现ClassCastException异常
- 如何解决?建议在强转前进行instanceof的判断。
7. Object类的使用
1. Object类的说明
> java.lang.Object类是所有Java类(除了自己以外)的根父类。
> java.lang.Object类中没有声明属性,声明有一个空参的构造器:Object(){}
下面重点关注java.lang.Object类中声明的方法。在权限允许的情况下,任何一个类的对象都可以调用。
2. 常用方法
重点方法:equals(Object obj) \ toString()
熟悉方法:clone() \ finalize()
目前不需要关注:getClass() \ hashCode() \ wait() \ wait(xxx) \ notify() \ notifyAll()
7.1 equals()方法
区分 == 和 equals()
1. == : 运算符,适用于基本数据类型、引用数据类型
equals():方法,适用于引用数据类型
2. 针对于引用数据类型, == :用来比较两个引用变量的地址值是否相等。(或判断两个引用是否指向同一个对象)
equals(): 需要区分此方法是否被重写过。具体见3
3.
3.1 像String、Date、包装类、File等类,它们都重写了Object类中的equals()方法,用于比较对象的实体内容
是否相等。如果相等,就返回true。
3.2 对于自定义的类,如果没有重写Object类中的equals()方法,则仍然比较两个对象的地址值是否相等。
如果我们重写Object类中的equals()方法的话,通常也是用来比较两个对象的实体内容是否相等。
int i = 65;
int j = 65;
sout(i == j);//true
char c = 'A';
sout(i == c);//true
float f = 65.0F;
sout(i == f);//true
7.2 toString()方法
1. Object类中toString()的定义:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
2. 开发中的使用场景
> 像String、Date、包装类、File等类,它们都重写了Object类中的toString(),用于返回当前对象的实体内容。
> 对于自定义的类,如果没有重写Object类中的toString()方法,则仍然返回当前对象的类型及地址
如果重写了Object类中的toString()方法,通常也都是返回当前对象的实体内容。
3. 开发中使用说明:
对于自定义的类,当我们调用对象的toString()方法时,习惯上希望输出对象的实体内容。所以,需要重写Object
类中的toString(),否则就是返回当前对象的类型及地址了。
8. 项目二:拼电商客户管理系统
- 项目中主要的类:
- (重点)封装客户数据的类:Customer
- (重点)管理多个客户对象的类:CustomerList
- 与用户交互的管理界面操作的类:CustomerView
- 封装Scanner的工具类:CMUtility
第08章:面向对象-高级
1. 关键字:static
2. static 用来修饰的结构:属性、方法;代码块、内部类
3. static修饰属性
3.1 复习:变量的分类
方式1:按照数据类型: 基本数据类型、引用数据类型
方式2:按照类中声明的位置:
成员变量(或属性):以是否使用static修饰
> 使用static修饰 : 类变量(或静态变量)
> 不使用static修饰 : 实例变量(或非静态变量)
局部变量:方法内声明的变量、方法形参、构造器内声明的变量、构造器形参、代码块内声明的变量等。
3.2 静态变量:类中的属性使用static进行修饰。
对比静态变量与实例变量:
① 个数
>静态变量:内存中只存在一份。与具体对象的个数,以及是否存在对象都无关。
>实例变量:归属于具体的对象所有。进而创建过几个对象,就存在一个实例变量。
② 内存位置
>静态变量:jdk6:存放在方法区。 从jdk7开始,存放在堆空间中。(注意:不在具体的对象内部)
>实例变量:堆空间存储了对象实体。在具体的对象实体中,保存着实例变量。
③ 加载时机
>静态变量:随着类的加载而加载。(即类加载完成时,此静态变量就分配好了内存空间)
>实例变量:随着对象的创建,在堆空间此对象内部,分配内存存储具体的实例变量。
④ 调用者
>静态变量:可以被类调用,也可以被类的对象调用。
>实例变量:只能被类的对象调用
⑤ 判断是否可以调用 ---> 从生命周期的角度解释
类变量 实例变量
类 yes no
对象 yes yes
⑥ 消亡时机
>静态变量:随着类的卸载而消亡。
>实例变量:随着对象的消亡而消亡。
4. static修饰方法:(类方法、静态方法)
> 随着类的加载而加载
> 静态方法,可以使用"类.静态方法"的方式进行调用
同时,还可以使用"对象.静态方法"的方式进行调用。--->从生命周期的角度解释
> 判断是否可以调用
类方法 实例方法
类 yes no
对象 yes yes
> 静态方法中只能调用当前类中的静态的变量、静态的方法。(即不能调用非静态的变量、非静态的方法)
非静态的方法中既可以调用当前类中非静态的变量、非静态的方法,也可以调用静态的变量、静态的方法。
> 静态方法中不能使用this、super关键字。
5. 开发中,什么时候需要将属性声明为静态的?
> 是否适合被类的多个对象所共享,同时多个对象对应的此变量值是相同的。
> 开发中,常常将一些常量声明为静态的。比如:Math的PI。
什么时候需要将方法声明为静态的?
> 操作静态变量的方法,通常设置为静态方法
> 开发中,工具类中的方法常常设置为static的。
2. 单例模式
1. 设计模式概述:
设计模式是在大量的`实践中总结`和`理论化`之后优选的代码结构、编程风格、以及解决问题的思考方式。
设计模式免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。
2. 何为单例模式(Singleton):在整个软件系统中,针对于某个类来讲,只存在该类的唯一的一个实例。则此类的设计
即为单例模式。
3. 如何实现单例模式(掌握):
饿汉式、懒汉式
4. 对比两种模式(特点、优缺点)
特点:
饿汉式:随着类的加载,当前类的实例就创建成功。
懒汉式:只有在首次调用get()方法时,才会创建单例对应类的实例。
饿汉式:(缺点)类一加载对象就创建成功,占用内存时间较长。(优点)线程安全的。
懒汉式:(优点)延迟了对象的创建,节省内存空间。(缺点)线程不安全的。 --->后续多线程章节中,将此方式改为线程安全的。
【面试题】 写一个(线程安全的)单例模式。
3. main()的理解(了解)
1. 理解1:程序的入口。
理解2:看做是一个普通的有形参的静态方法。
2. 与控制台交互(了解即可)
方式1:使用Scanner类及其内部的nextXxx()
方式2:使用main(),将从控制台获取的数据存储在其形参String[] args中。
4. 类的内部成员之四:代码块
4.1 代码块的基本使用
1. 代码块(或初始化块)的作用:用来对类或对象进行初始化操作的。
2. 代码块的修饰:只能使用static修饰。
3. 代码块的分类:静态代码块、非静态代码块
4. 具体使用:
4.1 静态代码块:
> 随着类的加载而执行。主要用来初始化类。
> 因为类只加载一次,进而静态代码块也只会执行一次
> 内部可以有输出语句、声明变量等操作
> 内部可以调用当前类中静态的结构(属性、方法),不能调用非静态的结构
> 如果一个类中声明了多个静态代码块,按照声明的顺序先后执行
> 静态代码块的执行要先于非静态代码块的执行
4.2 非静态代码块:
> 随着对象的创建而执行。主要用来初始化对象。
> 每创建一个对象,非静态代码块就执行一次。
> 内部可以有输出语句、声明变量等操作
> 内部可以调用当前类中静态的结构(属性、方法),能调用非静态的结构
> 如果一个类中声明了多个非静态代码块,按照声明的顺序先后执行
4.2 属性赋值位置、过程
1. 可以给类的非静态的属性(即实例变量)赋值的位置有:
① 默认初始化
② 显式初始化 / ③ 代码块中初始化
④ 构造器中初始化
***************************
⑤ 有了对象以后,通过"对象.属性"或"对象.方法"的方式给属性赋值
2. 执行的先后顺序:
① - ②/③ - ④ - ⑤
3. (超纲)关于字节码文件中的<init>\<clinit>的简单说明:
<clinit> : 系统自动生成的,内部包含了针对于静态属性的显式赋值、代码块中赋值操作。
如果类中的静态属性没有显式赋值、没有静态代码块,则不会自动生成<clinit>方法。
> 内部显式赋值、代码块中赋值操作的执行先后顺序取决于声明的先后顺序。
<init> : 系统自动生成的,内部包含了针对于非静态属性的显式赋值、代码块中赋值、构造器中赋值操作。
> 一个字节码文件中至少包含一个<init>。换句话说,一个字节码文件中,包含几个<init>方法
取决于类中声明了几个构造器。
> 内部显式赋值、代码块中赋值操作的执行先后顺序取决于声明的先后顺序;构造器中赋值操作是最后执行的。
5. 关键字:final
1. final的理解:最终的
2. final可以用来修饰的结构:类、方法、变量
3. 具体说明:
3.1 final修饰类:此类不能被继承。
> 比如:String、StringBuffer、StringBuilder类都使用了final修饰。
3.2 final修饰方法:此方法不能被重写。
> 比如:Object类中的getClass()
3.3 final修饰变量(重点关注):表示此变量一旦赋值就不可更改,即此变量理解为是一个常量。
> final修饰成员变量:此变量即为一个常量。
可以有哪些位置给常量赋值呢?① 显式赋值 ② 代码块中赋值 ③ 构造器中赋值。
> final修饰局部变量:此变量即为一个常量。
此局部变量只能被赋值一次。针对于形参来讲,使用final修饰以后,在调用此方法时给此常量形参赋值。
4. final与static搭配:用来修饰一个属性,此属性称为:全局常量。
比如: Math类中的PI。
6. 关键字:abstract
1. abstract的概念:抽象的
2. abstract可以用来修饰:类、方法
3. 具体的使用:
abstract修饰类:抽象类
> 不能实例化!
> 抽象类中一定声明有构造器,只是不能创建对象而已。---> 此时的构造器,用来给子类对象实例化时调用的。
> 抽象类中的方法可以是抽象方法,也可以是普通的非抽象方法。
abstract修饰方法:抽象方法
> 不包含方法体的方法,并且使用abstract修饰。
> 抽象类中可以没有抽象方法,但是抽象方法所属的类一定是抽象类。
> 子类继承抽象父类以后,如果重写了父类中的所有的抽象方法,此子类方可实例化。
如果子类没有重写父类中所有的抽象方法的话,则此子类必须也声明为抽象类。
4. abstract不能使用的场景
4.1 abstract 不能修饰哪些结构?属性、构造器、代码块等
4.2 abstract 不能与哪些关键字共用?
不能用abstract修饰私有方法、静态方法、final的方法、final的类。
5. 注意:
抽象类在使用时,如果出现在方法的形参位置。则在调用方法时,一定要使用多态了。
7. 与类并列的结构:接口(interface)
1. 定义接口的关键字: interface
2. 接口的理解:
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要...则必须能...”的思想。
继承是一个"是不是"的is-a关系,而接口实现则是 "能不能"的`has-a`关系。
3. 接口内部结构的说明:
> 可以声明:
jdk8之前:只能声明全局常量(public static final)、抽象方法(public abstract)
******************************************************
jdk8中:增加了静态方法、默认方法(default)
jdk9中:增加了私有方法。
> 不可以声明:构造器、代码块等结构。
4. 接口与类的关系 :实现关系(implements)
5. 满足此关系之后,说明:
> 实现类实现相应的接口以后,就获取了接口中声明的全局常量和抽象方法。
> 如果实现类重写了接口中声明的所有的抽象方法,则此实现类可以实例化
如果实现类没有重写完接口中声明的所有的抽象方法,则此实现类仍为一个抽象类。
> 一个类可以实现多个接口。--->一定程度上缓解了Java中类的单继承性的局限性。
6. 格式:
class SubA extends SuperA implements A,B,C{}
7. 接口与接口的关系:继承关系,而且是多继承的。
interface A{
void method1();
}
interface B{
void method2();
}
interface C extends A,B{} //多继承
8. 接口的多态性(重要)
9. 面试题:区分抽象类和接口
角度1:
共性:都不能实例化
不同点:抽象类:有构造器
接口:没有构造器
角度2:抽象类中可以声明抽象方法;接口中(jdk8之前)方法只能是抽象的。
角度3:类与类之间是继承关系,是单继承的;接口与接口之间是继承关系,是多继承的;类与接口之间是实现关系,是多实现的。
角度4:jdk8及之后的新特性:接口中可以声明静态方法、默认方法,包含方法体。
jdk9:新增私有方法。
8. 类的内部成员之五:内部类
> 内部类的分类(参照变量的分类)
> 如何创建成员内部类的对象
> 从两个角度来认识成员内部类(作为类、作为外部类的成员)
> 内部类如何调用外部类的成员(属性、方法)
> 在出现同名的属性、方法时,使用"外部类.this.结构"的方式显式调用父类的结构。
> 谈谈局部内部类开发中的使用场景
9. 枚举类
> 枚举类的特点:一个类中的对象个数是有限的、可数个的。
> (了解)jdk5.0之前,枚举类的定义方式。
> jdk5.0中新增了enum的方式定义枚举类。 ----需要掌握
> 自定义的枚举类的父类:Enum类。此类中声明的常用方法。
> values() \ valueOf(String objName) \ toString() ; name() \ ordinal()
> 枚举类实现接口。
10. 注解
> 注解的作用:与注释的区别。注解的作用
> Java基础中三个常见的注解
> 如何自定义注解
> 元注解:对现有的注解进行修饰作用的注解。
> 体会:框架的理解:框架 = 注解 + 反射 + 设计模式
> 掌握如何使用:单元测试方法。
11. 包装类
> 理解:为什么需要包装类?
> add(Object obj) / equals(Object obj)
> 基本数据类型以及对应的包装类
> 重点:基本数据类型、包装类、String三者之间的转换
> 基本数据类型 < --- > 包装类:自动装箱、自动拆箱
> 基本数据类型、包装类 ---> String: 调用String的valueOf(); +
> String ---> 基本数据类型、包装类:调用包装类的parseXxx(String str)
12.IDEA的使用
- IDEA常用的快捷键
- IDEA的debug功能
第09章:异常处理
1. 异常的概述、理解
1. 什么是异常?
指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。
2. 异常的抛出机制 ---> 万事万物皆对象
Java中把不同的异常用不同的类表示,一旦发生某种异常,就`创建该异常类型的对象`,并且抛出(throw)。
然后程序员可以捕获(catch)到这个异常对象,并处理;如果没有捕获(catch)这个异常对象,那么这个异常
对象将会导致程序终止。
3. 如何对待异常
> 态度1:一是遇到错误,不进行任何的处理,终止程序的运行。
> 态度2:如果之前的测试中出现了异常的情况,则修改代码,保证之后尽量不要出现同样的异常。
> 态度3:在编写程序时,就充分考虑到各种可能发生的异常和错误,极力预防和避免。实在无法避免的,
要编写相应的代码进行异常的检测、以及`异常的处理`
2. 常见的异常(重点)
java.lang.Throwable
|---java.lang.Error:错误
> Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。
> 一般不编写针对性的代码进行处理。
> 常见的Error:StackOverFlowError,OutOfMemoryError
|---java.lang.Exception:异常
> 其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,
使程序继续运行。
> 分类:编译时异常 、 运行时异常
> 举例:
运行时异常:
NullPointerException
ArrayIndexOutOfBoundsException
ClassCastException
NumberFormatException
ArithmeticException
InputMismatchException
编译时异常:
ClassNotFoundException
FileNotFoundException
IOException
3. 异常的处理(重点)
3.1 try-catch-finally
- try-catch的使用
1. 方式一(抓抛模型):
过程1:“抛”:Java程序的执行过程中如果出现异常,会生成一个对应异常类的对象,并将此对象抛出。
过程2:“抓”:针对于上一个过程中抛出的异常类的对象,进行的捕获(catch)行为。
2. 基本结构:
try{
//可能出现异常的代码
}catch(异常类型1 e){
//异常的处理方式
}catch(异常类型2 e){
//异常的处理方式
}
...
finally{
//一定会被执行的代码
}
3. 使用细节:
> finally是可选的。暂时先不考虑
> try中包裹的是可能出现异常的代码。如果在执行过程中,没有出现异常,则程序正常结束,不会考虑执行多个catch结构
如果try中出现了异常,则会自动创建对应异常类的对象,并将此对象抛出。
如果抛出的异常对象匹配某个具体的catch结构,则进入相应的catch中进行处理。一旦执行结果,就跳出当前结构,继续执行其后的代码
如果没有匹配到相应的catch结构,则相当于没有捕获异常,会导致程序的终止。
> 如果多个catch中的异常类型有子父类关系,则必须将子类异常的捕获声明在父类异常捕获的上面。
> try中声明的变量,在出了try的一对{}之后,就失效了。
> catch中异常处理的方式:
方式1:自己自定义输出语句
方式2:调用异常类的现有方法:方法1:调用printStackTrace(),用于打印异常出现的堆栈信息。(推荐)
方法2:调用getMessage(),返回一个异常的字符串信息
4. 开发体会:
> 对于运行时异常:实际开发中,我们通常都不再处理运行时异常。
> 对于编译时异常:实际开发中,我们是必须要提前给出异常方案。否则,编译不通过。
- finally的使用
1. finally的理解
> 将一定会被执行的代码声明在finally中
> finally结构是可选的。
> 不管try、catch中是否存在未被处理的异常,不管try、catch是否执行了return语句;finally是一定要被执行的结构。
2. 什么样的代码我们一定要声明在finally中呢?
> 开发中会涉及到相关的资源(流、数据库连接)的关闭的问题,如果相关的资源没有及时关闭,会出现内存泄漏。
为了避免出现内存泄漏,我们必须将其关闭操作声明在finally中,确保在出现异常的情况下,
此关闭操作也一定会被执行。
3. 面试题
final 、 finally 、finalize 的区别
3.2 throws
1. 格式:
在方法的声明处,使用"throws 异常类型1,异常类型2,..."
2. 举例:
public void method1() throws FileNotFoundException, IOException{}
3. 是否真正处理了异常?
> 从是否能通过编译的角度来说:使用throws的方法声明了可能出现的异常的类型,使得编译能通过。
> 从是否真正意义上解决了可能抛出的异常对象:并没有。只是将可能出现的异常对象继续向上抛出。
只有使用try-catch-finally的方式才是真正意义上处理了异常。
4. 方法的重写的要求:
针对于编译时异常:
子类重写父类中的方法,要求子类重写的方法抛出的异常类型不大于父类被重写方法抛出的异常类型。
比如:父类被重写的方法throws 异常类型1,子类可以throws异常类型1或异常类型1的子类。
针对于运行时异常:没有这样的要求。 ----> 开发中,针对于运行时异常,也不会进行try-catch或throws的处理。
- 如何选择两种处理方式?
开发中,如何选择异常处理的两种方式?(重要、经验之谈)
> 情况1:如果程序中使用了相关的资源,为了确保这些资源在出现异常的情况仍然能被执行关闭操作。
建议使用:try-catch-finally。将资源的关闭操作声明在finally中。
> 情况2:如果父类中被重写的方法没有使用throws的结构,则子类重写父类的方法中如果出现编译时异常,只能
使用try-catch-finally的方式进行处理。
> 情况3:如果在方法1中依次调用了方法2,3,4,而且此时的方法2,3,4是递进调用的关系。则通常方法2,3,4中
出现异常的情况下,选择使用throws的方式进行异常的处理,在方法1中使用try-catch-finally进行处理。
4. 手动抛出异常的对象(熟悉)
1. 为什么需要手动抛出异常?
在实际开发中,为了满足实际问题的需要,必要时需要手动的throw一个异常类的对象。
比如:要求分子、分母都不能为负数。如果出现负数了就报错。如何体现报错呢?手动抛出异常类的对象。
比如:给学生的id赋值,要求此id不能为负数。如果赋值为负数,就报错。如何体现报错呢?手动抛出异常类的对象。
2. 如何理解"自动 vs 手动"抛出异常对象?
过程1:“抛”:Java程序的执行过程中如果出现异常,会生成一个对应异常类的对象,并将此对象抛出。
情况1:自动抛出(throw)
情况2:手动抛出(throw):在方法体内使用
过程2:“抓”:针对于上一个过程中抛出的异常类的对象,进行的捕获(catch)行为。
广义上"抓"理解为异常处理的方式:
> 方式1:try-catch-finally
> 方式2:throws:使用在方法的声明处
类比:上游排污、下游治污。
3. 如何实现手动抛出异常?
在方法体的内部,满足某个条件的情况下,使用“throw + 异常类的对象”。
4. 注意点:throw后的代码不能被执行,编译不通过。
[面试题] 区分throw和throws
5. 如何自定义异常类(熟悉)
1. 如何自定义异常类? (参照着Exception、RuntimeException进行设计即可)
① 继承于现有的异常体系结构中的某一个类。比如继承于RuntimeException、Exception
② 提供几个重载的构造器
③ 提供一个全局常量serialVersionUID,用于唯一的标识当前类
2. 如何使用自定义异常类?
在满足相应情况的条件下,方法体内使用"throw + 自定义异常类的对象"的方式使用。
3. 为什么需要自定义异常类?
我们在开发中,针对于出现的异常,比较关心的是异常的名称。通过异常名,就可以直接定位出现的异常的问题。
所以,我们在开发中,具体到项目的具体要求时,我们都可以抛出自己定义的异常类型的对象。
五个关键字:
try-catch-finally
throws
throw
第10章:多线程
1. 相关概念
- 掌握:程序、进程、线程
- 熟悉:线程的调度机制:分时调度、抢占式调度
- 了解:单核CPU、多核CPU
- 了解:并行与并发
2. 创建多线程的两种经典方式(重点)
1. 线程的创建方式一:
1.1 步骤:
① 创建一个继承于Thread类的子类
② 重写Thread类的run()方法:将此线程要执行的操作编写在此方法体中。
③ 创建Thread类的子类的对象
④ 调用start()方法: 1、启动线程 2、调用线程的run()
1.2 例题:创建一个分线程1,用于遍历100以内的偶数
【拓展】 再创建一个分线程2,用于遍历100以内的偶数
2. 线程的创建方式二:
2.1 步骤:
① 创建实现Runnable接口的实现类
② 实现接口中的抽象方法run():将此线程要执行的操作编写在此方法体中。
③ 创建此实现类的对象
④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
⑤ 通过Thread类的对象调用start():1、启动线程 2、调用线程的run()
2.2 例题:创建分线程遍历100以内的偶数
3. 对比两种方式?
共同点:① 创建的线程都是Thread类或其子类的对象
② 启动线程,调用的都是Thread类中的start()
不同点:一种是继承的方式,一种是实现的方式(推荐);
推荐实现的方式的原因: ① 类的单继承的局限性 ②实现的方式更适合、方便的用来处理共享数据的场景。
联系:
public class Thread implements Runnable
3. 线程的常用方法、生命周期
一、线程的常用结构
1. 线程中的构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
2.线程中的常用方法:
> run():在继承的方式中,需要被重写的方法。
> start():要想启动一个线程,必须要调用此方法:①启动线程 ② 调用线程的run()
> static currentThread():获取当前执行的代码所属的线程。
> getName():获取线程名
> setName(String name):设置线程名
> yield():一旦线程执行此方法,当前线程就释放cpu的执行权
> join(): 在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b执行结束以后,线程a才可以从被阻塞的位置继续执行
> static sleep(long millis):指定线程"睡眠"多少毫秒
> isAlive() : 判断当前线程是否存活
过时方法:
> stop():强行结束一个线程的执行,直接进入死亡状态。
> suspend() / resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。
3. 线程的优先级:
3.1 线程的优先级的范围:[1,10]
int MAX_PRIORITY = 10; //最大优先级
int MIN_PRIORITY = 1; //最小优先级
int NORM_PRIORITY = 5; //默认优先级
3.2 如何设置/获取优先级:
setPriority(int priority):设置线程的优先级
getPriority() : 获取线程的优先级
-
生命周期
- jdk5.0之前:
-
jdk5.0
4. 线程的安全问题与同步机制(重点)
线程的安全问题与线程的同步机制
1. 多线程卖票,出现的问题:出现了重票、错票
2. 什么原因导致的?一个线程在没有操作完ticket的情况下,其他线程参与进来,导致出现了重票、错票
3. 如何解决?
应该包装一个线程在操作完共享数据ticket的情况下,其它线程才能参与进来继续操作ticket。
4. Java是如何解决线程的安全问题的? 同步机制
方式1:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
> 需要被同步的代码,即为操作共享数据的代码。
> 什么是共享数据:即为多个线程共同操作的数据。比如:ticket
> 使用synchronized将操作共享数据的代码包起来,确保这部分代码作为一个整体出现。只有当一个线程操作完此部分代码
之后,其他线程才有机会操作同样的这部分代码。
> 同步监视器,俗称锁。哪个线程获取了同步监视器,这个线程就能执行操作共享数据的代码。没有获取同步监视器的线程就只能等待。
注意:
> 操作共享数据的代码,不能包多了,也不能包少了。
> 同步监视器:任何一个类的对象,都可以充当同步监视器。但是,多个线程必须共用同一个同步监视器。
> 实现Runnable的方式中,使用的同步监视器可以考虑this。
继承Thread类的方式中,使用的同步监视器慎重this,可以考虑使用当前类。
方式2:同步方法
如果操作共享数据的代码完整的声明在一个方法中。我们也可以考虑将此方法声明为同步方法。
说明:
> 非静态的同步方法,其默认的同步监视器是:this
> 静态的同步方法,其默认的同步监视器是:当前类本身
5. synchronized好处:解决了线程的安全问题
弊端:串行的执行,是得多线程的性能受限
5. 同步机制的相关问题
5.1 解决懒汉式的线程安全问题(重点)
package com.atguigu04.threadsafemore.singleton;
/**
* ClassName: BankTest
* Description:
*
* @Author 尚硅谷-宋红康
* @Create 2023/2/24 11:50
*/
public class BankTest {
static Bank b1 = null;
static Bank b2 = null;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
b1 = Bank.getInstance();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b2 = Bank.getInstance();
}
});
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(b1);
System.out.println(b2);
System.out.println(b1 == b2);
}
}
//懒汉式
class Bank{
private Bank(){}
private static Bank bank = null;
//方式1:使用同步方法
// public static synchronized Bank getInstance(){
//
// if(bank == null){
//
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// bank = new Bank();
// }
// return bank;
//
// }
//方式2:使用同步代码块
public static Bank getInstance(){
synchronized (Bank.class) {
if(bank == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bank = new Bank();
}
return bank;
}
}
//思考:使用同步代码块,存在指令重排
// public static Bank getInstance(){
//
// if(bank == null){
//
// synchronized (Bank.class) {
// if(bank == null){
//
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// bank = new Bank();
// }
//
// }
// }
//
// return bank;
// }
}
5.2 死锁问题
线程的同步机制带来的问题:死锁
1. 如何看待死锁?
> 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
> 一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
> 我们编程中,要避免出现死锁
2. 诱发死锁的原因?
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待
以上4个条件,同时出现就会触发死锁。
3. 如何避免死锁?
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
5.3 JDK5.0新增解决安全问题的方式:Lock
除了使用synchronized同步机制处理线程安全问题之外,还可以使用jdk5.0提供的Lock锁的方式
1. 步骤:
步骤1. 创建ReentrantLock的实例,必须保证多个线程共用一个。
步骤2. 调用lock(),锁住共享数据的代码
步骤3. 调用unlock(),解锁共享数据的代码
2. 面试题:
synchronized同步的方式 与Lock的对比 ?
> synchronized同步机制,利用同步监视器,确保同步监视器的唯一性。
> 同步代码块、同步方法对应的一对{}中的代码是需要被同步的,只能有一个线程执行。
> Lock,确保Lock的实例的唯一性
> 在lock()和unlock()方法之间的操作,确保只有一个线程在执行。
官方文档:
Lock implementations provide more extensive locking operations
than can be obtained using synchronized methods and statements.
6. 线程的通信
1. 线程间的通信
为什么需要线程间的通信?
当我们`需要多个线程`来共同完成一件任务,并且我们希望他们`有规律的执行`,那么多线程之间需要一些通信机制,
可以协调它们的工作,以此实现多线程共同操作一份数据。
2. 涉及到三个方法的使用:
wait(): 一旦执行此方法,对应的线程就进入阻塞状态,并释放同步监视器的调用
notify():唤醒被wait的线程中优先级最高的那一个。如果被wait的多个线程优先级相同,则会随机唤醒其中被wait的线程。
notifyAll():唤醒所有被wait的线程。
3. 注意点:
> 此三个方法的调用者必须是同步监视器
> 此三个方法声明在java.lang.Object类中
> 此三个方法的使用,必须在同步方法或同步代码块中。
---> 在Lock方式解决线程安全问题的前提下,不能使用此三个方法。在Lock的情况下,使用Condition实现通信。
4. 案例:
案例1:使用两个线程打印 1-100。线程1, 线程2 交替打印
案例2:生产者&消费者
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有
固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品
了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来
取走产品。
5. 【面试题】wait() 和 sleep()的区别?
> 所属的类:wait()存在于Object类的非静态方法;sleep()存在于Thread类的静态方法
> 使用环境:wait() 必须使用在同步代码块或同步方法中;sleep():在调用时没有任何限制
> 都使用在同步代码块或同步方法情况下,区别:wait()一旦调用会释放同步监视器;sleep()不会释放同步监视器
> 相同点:二者一旦执行都可以使得当前线程进入阻塞状态
> 但是结束阻塞的方式不同:wait()的线程需要被notify()/notifyAll();sleep()的线程在指定时间结束后就结束阻塞。
7. JDK5.0新增两种创建多线程的方式
- 使用Callable接口
- 使用线程池
1. 创建多线程的方式三:实现Callable (jdk5.0新增的)
与之前的方式的对比:对比Runnable
> call()方法有返回值类型,比run()灵活
> call()声明有throws结构,内部有异常的话,不必非要使用try-catch
> Callable接口使用了泛型,call()的返回值类型更加灵活。(超纲)
2. 创建多线程的方式四:使用线程池
此方式的好处:
> 提高了程序执行的效率
> 提高了资源的重用率
> 设置相关的参数,实现对线程的管理
联想:与后续讲的数据库连接池的好处是相同的。
第11章:常用类与基础API
1、String
- String的声明:final 、实现Comparable接口等
- String内部的属性:final char[] value (jdk8中);final byte[] value(jdk9中)
- String的声明方式1:使用字面量的方式。需要使用字符串常量池。
- String的不可变性
- String的声明方式2:new
- String的特殊运算: +
- 常量 + 常量;变量+ 常量 ; 变量+变量;concat();intern()
- String的构造器、常用方法
2、与String相关的类:StringBuffer、StringBuilder
- String、StringBuffer、StringBuilder三个的异同。
- StringBuffer、StringBuilder的常用方法:增、删、改、查、插、长度、反转
- 三者添加数据方面的执行效率:StringBuilder > StringBuffer > String
3、比较器
开发中只要涉及到对象比较大小,都跟比较器打交道。
3.1 自然排序:Comparable
实现步骤:
1. 待排序的对象所说的类要实现Comaprable接口
2. 实现接口中的抽象方法:compareTo(Object obj),指明比较大小的规则
3. 创建待排序的多个对象,在相关的逻辑下进行排序操作即可。比如:Arrays.sort(Object[] objs)
3.2 定制排序:Comparator
实现步骤:
1. 创建一个实现了Comparator接口的类SubComparator
2. 此SubComparator类重写compare(Object o1,Object o2)
此方法中指明要比较类A的实例对象o1,o2的大小。
3. 创建多个类A的对象,在相关的逻辑下进行排序操作即可。比如:Arrays.sort(A[] objs,SubComparator的对象)
对比两种方式:
Comparable:在声明好类的同时,就指明了默认的排序的方式。
一旦声明,一劳永逸
Comparator: 比较灵活,可以在需要的具体场景下,灵活的指定排序的方式。
每次在需要的时候,都得现创建一个Comparator实现类的对象。
4、日期时间相关的API
4.1 jdk8之前相关API
- System的currentTimeMillis()
- java.util.Date 和 java.sql.Date
- getTime() \ toString()
- SimpleDateFormat:用来格式化、解析日期
- 格式化:String format(Date date)
- 解析:Date parse(String str)
- Calendar:日历类,抽象类
- 实例化:getInstance()
- 方法:get(int field) 、set(int field,...)、add(...),Date getTime() 、 setTime(Date date)
4.2 jdk8新增的API
- LocalDate、LocalTime、LocalDateTime --> 类似于Calendar
- Instant : 瞬时 ---> 类似于Date
- DateTimeFormatter :类似于SimpleDateFormat。针对LocalDate、LocalTime、LocalDateTime的格式化或解析操作
5、其他api的使用
1. System类
2. Runtime类:单例设计模式,对应着一个java进程中运行时内存环境。
3. Math类:跟数学操作相关的api
4. BigInteger类和BigDecimal类
BigInteger类:如果在程序中,需要使用的整型数据超出了long的范围(最大值为2^63 -1 ),则可以使用BigInteger替换,
可以表示任意精度范围的整数。
BigDecimal类:如果在程序中,想表示任意精度的浮点型值,则使用BigDecimal类替换double的使用。
5. Random类
第12章:集合框架
1、集合与数组的对比
- 数组的特点、弊端
数组存储多个数据方面的特点:
> 数组中存储的多个元素是有序的、可以重复的的数据,紧密排列的
> 数组在内存中使用一整块连续的内存空间进行存储
> 数组一旦初始化,其长度就确定了。
> 数组一旦声明,其元素的类型就确定了。不能添加非此类型的元素。
Person[] arr = new Person[4];
Object[] arr1 = new Object[5];
数组存储多个数据方面的弊端:
> 数组一旦初始化,其长度就不可变。
> 数组可用的方法基本没有。涉及到的增删改查操作都需要自己编写代码
> 对于无序的、不可以重复的多个数据,就不适合使用数组存储
- 集合框架结构
Java集合框架体系(java.util包下)
java.util.Collection接口:存储一个一个的数据
|----java.util.List子接口:有序的、可以重复的数据 ("动态"数组)
|--- ArrayList(主要实现类) \ LinkedList \ Vector
|----java.util.Set子接口:无序的、不可以重复的数据 (高中讲的集合)
|--- HashSet(主要实现类) \ LinkedHashSet \ TreeSet
java.util.Map接口:存储一对一对(key-value)的数据 (高中讲的映射、函数 (x1,y1)、(x2,y2) )
|---- HashMap(主要实现类) \ LinkedHashMap \ TreeMap \ Hashtable \ Properties
2、Collection中的常用方法
1. 常用方法:(Collection中定义了15个抽象方法。这些方法需要大家熟悉!)
2. 集合与数组的相互转换:
Collection集合 ----> 数组: toArray()
数组 ----> Collection集合的子接口:List : 调用Arrays的静态方法asList(Object ... objs)
3. 向Collection中添加元素的要求:
添加的元素所在的类要重写equals(Object obj)
原因:因为Collection中的一些方法在调用时,要使用到元素所在类的equals()。
比如:constais(Object obj) / remove(Object obj) ..
学习的程度把握:
> 第1层次:针对要存储的多个数据的特点,选择相关的接口的主要实现类,完成对象的创建、相关方法的调用
> 第2层次:需要熟悉接口的不同的实现类之间的区别,进而熟悉不同的场景下应该选择哪个实现类。
> 第3层次:熟悉不同的实现类的底层源码实现。--->间接考查数据结构。放到14章中讲。
比如:HashMap、ArrayList/LinkedList/Vector、LinkedHashMap、HashSet
3、迭代器Iterator
1. 迭代器(Iterator)的作用?
用于遍历Collection集合元素。
2. 如何获取迭代器(Iterator)对象?
使用集合的iterator(),返回一个迭代器的对象
3. 如何实现遍历(代码实现)
while(iterator.hasNext()){
//next():①指针下移 ② 将下移以后集合位置上的元素返回
Object obj = iterator.next();
System.out.println(obj);
}
4. 增强for循环(foreach循环)的使用(jdk5.0新特性)
4.1 作用
用于遍历集合元素、遍历数组元素
4.2 格式:
增强for循环格式:for(集合元素的类型 临时变量 : 要遍历的集合)
4.3 说明:
我们不要使用增强for循环来修改集合或数组中的元素。因为此操作常常失败。
4、Collection的子接口1:List接口
List及其实现类特点
java.util.Collection接口:存储一个一个的数据
|----java.util.List子接口:有序的、可以重复的数据 ("动态"数组)
|--- ArrayList:主要实现类;线程不安全的,效率高;底层使用Object[]存储
对于频繁的查找、尾部添加,性能较高,时间复杂度O(1)
|--- LinkedList:使用双向链表存储数据;
对于频繁的删除、插入操作,性能较高,时间复杂度为O(1)
|--- Vector:古老的实现类;线程安全的,效率低;底层使用Object[]存储
小结:
增:add(Object obj) / addAll(Collection coll)
删:remove(Object obj) / remove(int index)
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele) / addAll(int index, Collection eles)
长度:size()
遍历:① 迭代器 ② 增强for ③ 一般的for
5、Collection的子接口2:Set接口
- Set的实现类的特点
1. Set及其实现类特点
java.util.Collection接口:存储一个一个的数据
|----java.util.Set子接口:无序的、不可以重复的数据 (高中讲的集合)
|--- HashSet:主要实现类;底层使用数组+链表+红黑树结构进行存储(jdk8.0)
|--- LinkedHashSet:是HashSet的子类;在底层使用Haset结构进行存储之外,又增加了一对
双向链表,用于记录添加元素的先后顺序。对应频繁的遍历操作,性能较高。
|--- TreeSet:底层使用红黑树进行存储。可以按照添加的元素的指定属性的大小顺序进行遍历。
2. 开发中的使用频率及场景:
> 使用频率较低;
> 使用场景:用于过滤重复数据
- Set中的常用方法
Set中常用方法
就是Collection中定义的15个方法。
- Set的实现类:HashSet
1. Set中无序性、不可重复性的理解(以HashSet及其子类为例说明)
> 无序性:!= 随机性, != 添加的顺序和遍历的顺序不一致。
> 不可重复性:哈希算法。
以两个元素的比较来说,
如果两个元素根据hashCode()方法计算得到的哈希值相同,且equals()判断时也返回true,则认为两个元素是相同的。
如果两个元素根据hashCode()方法计算得到的哈希值不同,或者哈希值相同,但equals()判断时返回false,则认为两个元素是不同的。
2. 添加到HashSet/LinkedHashSet中元素的要求:
元素所在的类要重写两个方法:equals() 、 hashCode()。 使用IDEA自动生成即可。
重写时,要尽量保证equals() 、 hashCode()的一致性。
- Set的实现类:TreeSet
1. 底层的数据结构:红黑树
2. 添加数据后的特点:可以按照添加的元素的指定的属性的大小顺序进行遍历
3. 向TreeSet中添加的元素的要求:
> 添加的多个元素,必须是同一个类的对象,即不能是不同类的对象。
4. 判断数据是否相同的标准
> 不再是equals() 和 hashCode()了。
> 应该是:
自然排序,实现了Comparable接口,是否相同的标准在于compareTo()是否返回0
定制排序,实现了Comparator接口,是否相同的标准在于compare()是否返回0
6、Map接口的使用
- Map的实现类的对比
java.util.Map接口:存储一对一对(key-value)的数据 (高中讲的映射、函数 (x1,y1)、(x2,y2) )
|---- HashMap:主要实现类;线程不安全的,效率高;jdk7:数组+单向链表,jdk8:数组+单向链表+红黑树
可以添加null的key或value
|---- LinkedHashMap:继承于HashMap;在底层使用HashMap数据结构的基础上,又增加了一对双向链表,用于
记录添加元素的先后顺序。当遍历此集合时,就可以按照添加的顺序实现遍历。
对于频繁的遍历操作,建议使用此类。
|---- TreeMap:底层使用红黑树结构存储;可以按照添加的key-value对的key的指定的属性的大小进行排序。
进而遍历时,也是按照key的指定的属性的大小顺序进行遍历的。
|---- Hashtable:古老的实现类;线程安全的,效率低;数组+单向链表
不可以添加null的key或value
|---- Properties:继承于Hashtable。key、value都是String类型,常用来处理属性文件。
[面试题]
HashMap的底层源码实现
HashMap和Hashtable的区别
HashMap、LinkedHashMap的区别
- HashMap的存储数据的特点
HashMap中元素的特点
> HashMap中的所有的key彼此之间不相同,且无序。多个key构成一个Set。--->key所在的类要重写equals()、hashCode()
> HashMap中的所有的value彼此之间可以相同,且无序。多个value构成一个Collection。--> value所在的类要重写equals()
> HashMap中的一个key-value构成一个Entry。
> HashMap中的所有的entry彼此之间不相同,且无序。多个entry构成一个Set。
- Map中的常用方法
Map中的常用方法
- 添加、修改操作:
- Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
- void putAll(Map m):将m中的所有key-value对存放到当前map中
- 删除操作:
- Object remove(Object key):移除指定key的key-value对,并返回value
- void clear():清空当前map中的所有数据
- 元素查询的操作:
- Object get(Object key):获取指定key对应的value
- boolean containsKey(Object key):是否包含指定的key
- boolean containsValue(Object value):是否包含指定的value
- int size():返回map中key-value对的个数
- boolean isEmpty():判断当前map是否为空
- boolean equals(Object obj):判断当前map和参数对象obj是否相等
- 元视图操作的方法:
- Set keySet():返回所有key构成的Set集合
- Collection values():返回所有value构成的Collection集合
- Set entrySet():返回所有key-value对构成的Set集合
小结:
增:put(Object key,Object value)
删:remove(Object key)
改:put(Object key,Object value)
查:get(Object key)
长度:size()
遍历:keySet() \ values() \ entrySet()
- Map的实现类:TreeMap
TreeMap的使用
> 可以按照添加的key-value对的key的指定的属性的大小进行排序。
进而遍历时,也是按照key的指定的属性的大小顺序进行遍历的。
> 针对于key-value对中的key进行自然排序或定制排序即可。
- Map的实现类:Hashtable与Properties
Hashtable与Properties的使用
> Properties:继承于Hashtable。key、value都是String类型,常用来处理属性文件。
7、操作集合的工具类:Collections
1. Collections概述
Collections操作集合框架(Collection、Map)的工具类。
2. 常用方法
3. 面试题:区分Collection 和 Collections
Collection:集合框架中提供的一个用于存储一个一个数据的顶级接口。下面提供了List和Set等子接口。
第13章:jdk5.0新特性:泛型
1、在集合中使用泛型前后的对比
1. 什么是泛型?
所谓泛型,就是允许在定义类、接口时通过一个`标识`表示类中某个`属性的类型`或者是某个方法的`返回值或参数的类型`。
这个类型参数将在使用时(例如,继承或实现这个接口、创建对象或调用方法时)确定(即传入实际的类型参数,也称为
类型实参)。
2. 在集合中使用泛型之前可能存在的问题
问题1:添加的数据类型不安全
问题2:繁琐:必须要使用向下转型。 还可能会报ClassCastException
@Test
public void test2(){
// List<Integer> list = new ArrayList<Integer>();
ArrayList<Integer> list = new ArrayList<>(); //jdk7.0新特性:类型推断
//添加学生的成绩
list.add(78);
list.add(87);
list.add(66);
list.add(99);
list.add(66);
//1.如下的操作,编译不通过
// list.add("AA");
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
//2.不需要使用向下转型
Integer score = iterator.next();
System.out.println(score);
}
}
@Test
public void test3(){
HashMap<String,Integer> map = new HashMap<>();
map.put("Tom",78);
map.put("Jerry",88);
map.put("Jack",55);
map.put("Rose",89);
// map.put(56,"Tony");//编译不通过
Set<Map.Entry<String,Integer>> entrySet = map.entrySet();
Iterator<Map.Entry<String,Integer>> iterator = entrySet.iterator();
while(iterator.hasNext()){
Map.Entry<String,Integer> entry = iterator.next();
System.out.println(entry.getKey() + "--->" + entry.getValue());
}
}
2、在其它结构中使用泛型
- 比较器:Comparable
public class Employee implements Comparable<Employee>{
private String name;
private int age;
private MyDate Birthday;
//省略get、set、构造器、toString()
@Override
public int compareTo(Employee o) {
// if (this == o){
// return 0;
// }
return this.name.compareTo((o.name));
}
}
- 比较器:Comparator
//定制排序
@Test
public void test2() {
Employee e1 = new Employee("Tom", 23, new MyDate(1999, 12, 3));
Employee e2 = new Employee("Jerry", 33, new MyDate(1990, 2, 3));
Employee e3 = new Employee("Peter", 22, new MyDate(2000, 3, 5));
Employee e4 = new Employee("NiPing", 23, new MyDate(2000, 12, 5));
Employee e5 = new Employee("Fengyi", 20, new MyDate(2002, 9, 9));
Comparator<Employee> comparator = new Comparator<>() {
@Override
public int compare(Employee e1, Employee e2) {
if (e1 == e2) {
return 0;
}
int yearDistance = e1.getBirthday().getYear() - e2.getBirthday().getYear();
if (yearDistance != 0) {
return yearDistance;
}
int monthDistance = e1.getBirthday().getMonth() - e2.getBirthday().getMonth();
if (monthDistance != 0) {
return monthDistance;
}
return e1.getBirthday().getDay() - e2.getBirthday().getDay();
}
};
TreeSet<Employee> treeSet = new TreeSet<>(comparator);
treeSet.add(e1);
treeSet.add(e2);
treeSet.add(e3);
treeSet.add(e4);
treeSet.add(e5);
Iterator<Employee> iterator = treeSet.iterator();
while (iterator.hasNext()) {
Employee e = iterator.next();
System.out.println(e);
}
}
3、如何自定义泛型类、泛型接口、泛型方法
1. 自定义泛型类\接口
1.1 格式
class A<T>{}
interface B<T>{}
1.2 使用说明
> 声明的泛型类,在实例化时可以不使用类的泛型。
> 声明泛型类以后,可以在类的内部结构中,使用类的泛型参数。比如:属性、方法、构造器
> 何时指明具体的类的泛型参数类型呢?① 类的实例化 ② 提供子类时
> 泛型参数类型只能是引用数据类型,不能使用基本数据类型。
> 一旦指定类的泛型参数的具体类型以后,则凡是使用类的泛型参数的位置,都确定为具体的泛型参数的类型。
如果实例化时未指定泛型参数的具体类型,则默认看做是Object类型。
> 泛型类中,使用泛型参数的属性、方法是不能声明为static的。
2. 自定义泛型方法
2.1 问题:在泛型类的方法中,使用了类的泛型参数。那么此方法是泛型方法吗?
2.2 格式
权限修饰符 <T> 返回值类型 方法名(形参列表){}
2.3 举例
public <E> ArrayList<E> copyFromArrayToList(E[] arr){
2.4 说明
> 泛型方法所在的类是否是一个泛型类,都可以。
> 泛型方法中的泛型参数通常是在调用此方法时,指明其具体的类型。
一旦指明了其泛型的类型,则方法内部凡是使用方法泛型参数的位置都指定为具体的泛型类型。
> 泛型方法可以根据需要声明为static
4、泛型在继承上的体现
1. 类A是类B的父类,则G<A> 与 G<B>的关系:没有继承上的关系。
比如:ArrayList<String>的实例就不能赋值给ArrayList<Object>
2. 类A是类B的父类或接口,A<G> 与 B<G>的关系:仍然满足继承或实现关系。意味着可以使用多态。
比如:将ArrayList<String>的类型的变量赋值给List<String>的变量,是可以的。
5、通配符、有条件限制的通配符的使用
1. 通配符: ?
2. 使用说明:
2.1 举例:
List<?> list1 = null;
List<Object> list2 = new ArrayList<Object>();
List<String> list3 = null;
list1 = list2;
list1 = list3;
2.2 说明:可以将List<?>看做是List<Object> 、 List<String>结构共同的父类
3. 读写数据的特点
> 读取:可以读数据,但是读取的数据的类型是Object类型。
> 写入:不能向集合中添加数据。特例:null
4. 有限制条件的通配符
List<? extends A> : 可以将List<A> 或 List<SubA>赋值给List<? extends A>。其中SubA是A类的子类
List <? super A> : 可以将List<A> 或 List<SuperA>赋值给List<? super A>。其中SuperA是A类的父类
5. 有限制条件的统配符的读写操作(难、了解)
见代码
第14章:数据结构与集合源码
1、数据结构
- 数据结构的研究对象
1. 数据结构概念:
数据结构,就是一种程序设计优化的方法论,研究数据的`逻辑结构`和`物理结构`以及它们之间相互关系,
并对这种结构定义相应的`运算`,目的是加快程序的执行速度、减少内存占用的空间。
2. 数据结构的研究对象
研究对象1:数据间逻辑关系
> 集合结构
> 线性结构:一对一关系
> 树形结构:一对多关系
> 图形结构:多对多关系
研究对象2:数据的存储结构
> 结构1:顺序结构
> 结构2:链式结构
> 结构3:索引结构
> 结构4:散列结构
开发中更关注存储结构:
> 线性表:数组、单向链表、双向链表、栈、队列等
> 树:二叉树、B+树
> 图:无序图、有序图
> 散列表:HashMap、HashSet
研究对象3:
- 分配资源,建立结构,释放资源
- 插入和删除
- 获取和遍历
- 修改和排序
- 常见的数据存储结构
3. 常见存储结构之:数组
4. 常见存储结构之:链表
4.1 单向链表
class Node{
Object data;
Node next;
public Node(){}
public Node(Object data){
this.data = data;
}
public Node(Object data,Node next){
this.data = data;
this.next = next;
}
}
举例:
Node node1 = new Node("AA");
Node node2 = new Node("BB");
node1.next = node2; //尾插法
node2.next = node1;//头插法
或
Node node2 = new Node("BB",node1);
4.2 双向链表
class Node{
Object data;
Node prev;
Node next;
public Node(Object data){
this.data = data;
}
public Node(Node prev,Object data,Node next){
this.prev = prev;
this.data = data;
this.next = next;
}
}
举例:
Node node1 = new Node("AA");
Node node2 = new Node(node1,"BB",null);
Node node3 = new Node(node2,"CC",null);
node1.next = node2;
node2.next = node3;
5. 常见存储结构之:二叉树
class Node{
Object data;
Node left;
Node right;
public Node(Object data){
this.data = data;
}
public Node(Node left,Object data,Node right){
this.left = left;
this.data = data;
this.right = right;
}
}
举例:
Node node1 = new Node("AA");
Node node2 = new Node("BB");
Node node3 = new Node("CC");
node1.left = node2;
node1.right = node3;
或者:
class Node{
Object data;
Node left;
Node right;
Node parent;
public Node(Object data){
this.data = data;
}
public Node(Node left,Object data,Node right){
this.left = left;
this.data = data;
this.right = right;
}
public Node(Node parent,Node left,Object data,Node right){
this.parent = parent;
this.left = left;
this.data = data;
this.right = right;
}
}
举例:
Node node1 = new Node("AA");
Node node2 = new Node(node1,null,"BB",null);
Node node3 = new Node(node1,null,"CC",null);
node1.left = node2;
node1.right = node3;
6. 常见存储结构之:栈 (先进后出、后进先出、FILO、LIFO)
(ADT:abstract data type,栈可以使用数组、链表生成)
class Stack{
Object[] values;
int size;//记录添加的元素个数
public Stack(int capacity){
values = new Object[capacity];
}
//入栈
public void push(Object ele){
if(size >= values.length){
throw new RuntimeException("栈已满,添加失败");
}
values[size] = ele;
size++;
}
//出栈
public Object pop(){
if(size <= 0){
throw new RuntimeException("栈已空,弹出栈操作失败");
}
Object returnValue = values[size - 1];
values[size - 1] = null;
size--;
return returnValue;
}
}
7. 常见存储结构之:队列(先进先出、FIFO)
(ADT:abstract data type,栈可以使用数组、链表生成)
class Queue{
Object[] values;
int size;//记录添加的元素个数
public Queue(int capacity){
values = new Object[capacity];
}
//添加元素
public void add(Object ele){
if(size >= values.length){
throw new RuntimeException("队列已满,添加失败");
}
values[size] = ele;
size++;
}
//获取元素
public Object get(){
if(size <= 0){
throw new RuntimeException("队列已空,获取数据失败");
}
Object returnValue = values[0];
for(int i = 0;i < size - 1;i++){
values[i] = values[i + 1];
}
values[size - 1] = null;
size--;
return returnValue;
}
}
2、集合源码
- List的实现
一、ArrayList
1. ArrayList的特点:
主要实现类;线程不安全的,效率高;底层使用Object[]存储
对于频繁的查找、尾部添加,性能较高,时间复杂度O(1)
2. ArrayList源码解析:
2.1 jdk7版本:(以jdk1.7.0_07为例)
ArrayList list = new ArrayList(); //底层创建了一个长度为10的Object[]:Object[] elementData = new Object[10];
list.add("AA");//elementData[0] = "AA";
...
当添加第11个元素时:由于容量不够,需要扩容,默认扩容为原来的1.5倍。
2.2 jdk8版本:(以jdk1.8.0_271为例)
ArrayList list = new ArrayList(); //底层并没有创建长度为10的Object[]数组,而是Object[] elementData = {};
list.add("AA");//当首次添加元素时,底层创建长度为10的数组,赋给elementData,同时elementData[0] = "AA";
...
当添加第11个元素时:由于容量不够,需要扩容,默认扩容为原来的1.5倍。
类比:1.7类似于饿汉式;1.8类似于懒汉式。
二、Vector
1. Vector的特点:
古老的实现类;线程安全的,效率低;底层使用Object[]存储
2. Vector源码解析:(以jdk1.8.0_271为例)
Vector在jdk1.8中初始化时就创建了长度为10的Object[]数组。当添加元素到满的时候,默认扩容为原来的2倍。
三、LinkedList
1. LinkedList的特点:
使用双向链表存储数据;
对于频繁的删除、插入操作,性能较高,时间复杂度为O(1)
2. LinkedList在jdk8中的源码解析:
LinkedList list = new LinkedList(); //底层没有做什么操作
list.add("AA");//内部创建一个Node对象a,LinkedList内部的属性first、last都指向对象a
list.add("BB");//内部创建一个Node对象b,对象a的next指向对象b,对象b的prev指向对象a,last指向对象b。
内部声明的Node如下:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
3. LinkedList是否存在扩容问题?没有!
四、启示与开发建议
> Vector基本不用,效率低,使用ArrayList替换
> 对于频繁的删除、插入操作,使用LinkedList替换ArrayList
> 除此之外,我们首推ArrayList。
> new ArrayList() / new ArrayList(int capacity) (推荐,避免出现不必要的多次扩容)
- HashMap的底层实现
一、HashMap
1. HashMap中元素的特点
> HashMap中的所有的key彼此之间不相同,且无序。多个key构成一个Set。--->key所在的类要重写equals()、hashCode()
> HashMap中的所有的value彼此之间可以相同,且无序。多个value构成一个Collection。--> value所在的类要重写equals()
> HashMap中的一个key-value构成一个Entry。
> HashMap中的所有的entry彼此之间不相同,且无序。多个entry构成一个Set。
2. HashMap源码解析
2.1 jdk7中创建对象和添加数据过程(以JDK1.7.0_07为例说明):数组+单向链表
HashMap map = new HashMap();//底层创建了长度为16的数组:Entry[] table = new Entry[16];
map.put("AA",67);//添加过程如下。
将(key1,value1)添加到map中:
1)通过key1所在的hashCode(),计算得到key1的哈希值1,此哈希值1经过某种算法(hash())以后得到哈希值2。
此哈希值2经过某种算法(indexFor())以后,得到key1-value1在数组table中的存储位置i。
2)判断table[i] 是否为空。
如果为空,key1-value1添加成功。 --->添加成功1
如果不为空,假设已有元素(key0,value0),则需要继续比较。 ----> 哈希冲突
3) 比较key1的哈希值2与key0的哈希值2是否相等。
如果两个哈希值2不相等。则认为key1-value1与key0-value0不相同。key1-value1添加成功。 --->添加成功2
如果两个哈希值2相同,则需要继续比较。
4) 调用key1所在类的equals(),将key0放入equals()的形参中。看返回值。
如果返回值为false,则key1和key0不同,则key1-value1添加成功。 --->添加成功3
如果返回值为true,则认为key1和key0相同,则value1替换value0。理解为修改成功。
说明:
添加成功1:将key1-value1封装在entry的对象中,将此对象放入数组的位置
添加成功2、3:key1-value1封装在entry的对象1中,与key0-value0封装的entry对象0构成单向链表的结构。
jdk7中是entry对象1指向entry对象0,entry对象1放在数组里。
...
不断的添加,添加到什么情况时会扩容呢?一旦达到临界值(且索引i的位置上恰好还有元素),就考虑扩容。默认扩容为原来的2倍。
(源码为:if ((size >= threshold) && (null != table[bucketIndex])) 条件下扩容)。
2.2 jdk8与jdk7的不同之处(以jdk1.8.0_271为例):
1)HashMap map = new HashMap(); 底层并没有创建长度为16的数组。
2)调用put(k,v)添加元素。如果是首次添加,则底层默认创建长度为16的table[]
3)jdk8中HashMap内部使用Node[]替换Entry[]。
4)如果要添加的key1-value1在经过一系列判断后,确定能添加到索引i的位置。此时,采用尾插法。
即原有的此索引i位置上的链表的最后一个元素指向新要添加的key1-value1。 "七上八下"
5)如果索引i位置上的元素达到8了,且数组的长度达到64的情况下,索引i位置上的多个元素要改为使用红黑树存储。
目的:为了提升查找的效率。(链表情况下查找的复杂度:O(n),红黑树的查找的复杂度:O(logN))
当索引i位置上的元素个数少于6个时,会将此索引i位置上的红黑树改为单向链表。
2.3 属性字段:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表
//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量64
transient Node<K,V>[] table; //数组
transient int size; //记录有效映射关系的对数,也是Entry对象的个数
int threshold; //阈值,当size达到阈值时,考虑扩容
final float loadFactor; //加载因子,影响扩容的频率
二、LinkedHashMap
1. LinkedHashMap 与 HashMap 的关系:继承关系。在HashMap的Node的基础上,增加了一对双向链表,记录
添加的先后顺序。
2. 底层结构:
重写了如下的方法:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
其中:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
拓展:
HashSet的底层实现原理?
第15章:File类与IO流
1. File类的使用
1. File类的理解
> File声明在java.io包下的。
> File类的对象可以表示一个文件或一个文件目录。
> File类中包含了关于文件、文件目录的新建、删除、重命名、查询所在路径、获取文件大小等方法。
但是不包含读写文件内部内容的方法。要想读写文件内容,我们需要使用IO流。
> File类的对象常作为IO流读写数据的端点出现:常作为IO流的构造器的形参出现。
2. 内部api使用说明
2.1 构造器
* `public File(String pathname) ` :以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。
* `public File(String parent, String child) ` :以parent为父路径,child为子路径创建File对象。
* `public File(File parent, String child)` :根据一个父File对象和子文件路径创建File对象
2.2 方法
1、获取文件和目录基本信息
* public String getName() :获取名称
* public String getPath() :获取路径
* `public String getAbsolutePath()`:获取绝对路径
* public File getAbsoluteFile():获取绝对路径表示的文件
* `public String getParent()`:获取上层文件目录路径。若无,返回null
* public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
* public long lastModified() :获取最后一次的修改时间,毫秒值
2、列出目录的下一级
* public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
* public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
3、File类的重命名功能
- public boolean renameTo(File dest):把文件重命名为指定的文件路径。
4、判断功能的方法
- `public boolean exists()` :此File表示的文件或目录是否实际存在。
- `public boolean isDirectory()` :此File表示的是否为目录。
- `public boolean isFile()` :此File表示的是否为文件。
- public boolean canRead() :判断是否可读
- public boolean canWrite() :判断是否可写
- public boolean isHidden() :判断是否隐藏
5、创建、删除功能
- `public boolean createNewFile()` :创建文件。若文件存在,则不创建,返回false。
- `public boolean mkdir()` :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
- `public boolean mkdirs()` :创建文件目录。如果上层文件目录不存在,一并创建。
- `public boolean delete()` :删除文件或者文件夹
删除注意事项:① Java中的删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
3. 概念:
绝对路径:在windows操作系统中,以盘符开始的路径。
相对路径:相较于某个指定路径下的具体路径。
单元测试方法:相较于当前的module
main():相较于当前的工程
2. IO流的概述
- 流的分类
- 流的流向:输入流、输出流
- 操作的数据单位:字节流、字符流
- 角色的不同:节点流、处理流
- 4个抽象基类
- 整个流这一章涉及到的具体的流的使用,操作的步骤都是标准规范的。
- 步骤1:创建File的对象
- 步骤2:创建流的对象,构造器中需要传入File的对象
- 步骤3:读取、写出操作的细节
- 步骤4:关闭资源
3. 文件流的使用
-
FileInputStream与FileOutputStream、FileReader与FileWriter
-
注意点1:
注意点:
> 因为涉及到资源的关闭,所有异常的处理需要使用try-catch-finally结构替换throws
> 对于输入流来讲,File对象对应的物理磁盘上的文件必须存在。否则,报FileNotFoundException
对于输出流来讲,File对象对应的物理磁盘上的文件可以不存在。
> 如果不存在,则在输出的过程中,会自动创建指定名的文件
> 如果存在,如果使用的是FileWriter(File file)或FileWriter(File file,false)构造器,则在输出的过程中,会覆盖已有的文件
如果存在,如果使用的是FileWriter(File file,true)构造器,则在输出的过程中,会在现有文件末尾追加内容。
> 务必记得关闭资源,否则出现内存泄漏
- 注意点2:
> FileReader \ FileWriter :主要用来处理文本文件
对于非文本文件的处理是失败的。
> FileInputStream \ FileOutputStream:主要用来处理非文本文件。
> 文本文件:.txt、.java、.c、.cpp、.py
非文本文件:.doc、.jpg、.png、.avi、.mp3、.mp4、.ppt
4. 处理流之一:缓冲流
-
BufferedInputStream与BufferedOutputStream、BufferedReader与BufferedWriter
-
作用:加快文件的读写效率。
-
原理:内部提供了缓冲区(数组实现的),减少和磁盘交互的次数。
-
4个缓冲流 使用的方法 处理非文本文件的字节流: BufferedInputStream read(byte[] buffer) BufferedOutputStream write(byte[] buffer,0,len) \ flush() 处理文本文件的字符流: BuffferedReader read(char[] buffer)\readLine() BufferedWriter write(char[] buffer,0,len) \ flush() 3. 实现的步骤 第1步:创建File的对象、流的对象(包括文件流、缓冲流) 第2步:使用缓冲流实现 读取数据 或 写出数据的过程(重点) 读取:int read(char[] cbuf/byte[] buffer) : 每次将数据读入到cbuf/buffer数组中,并返回读入到数组中的字符的个数 写出:void write(String str)/write(char[] cbuf):将str或cbuf写出到文件中 void write(byte[] buffer) 将byte[]写出到文件中 第3步:关闭资源
5. 处理流之二:转换流
-
InputStreamReader和OutputStreamWriter
-
基本使用
-
1. 复习 字符编码:字符、字符串 ----> 字节、字节数组。对应着编码集 字符解码:字节、字节数组 ---> 字符、字符串。对应着解码集 2. 如果希望程序在读取文本文件时,不出现乱码,需要注意什么? 使用的解码集必须与当初保存文本文件使用的编码集一致。
6. 处理流之三:对象流
-
了解:数据流:DataInputStream 、DataOutputStream
- 读写8种基本数据类型的变量、String、字节数组
-
掌握:ObjectInputStream、ObjectOutputStream
- 读写8种基本数据类型的变量、对象(readObject();writeObject(Object obj))
-
掌握:对象的序列化机制
-
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
-
序列化过程:将内存中的Java对象转换为二进制流,保存在文件中或通过网络传输出去。 使用ObjectOutputStream 反序列化过程:将文件中或者通过网络接收到的二进制流转换为内存中的Java对象 使用ObjectInputStream
-
-
熟悉:自定义类要想实现序列化机制,需要满足:
> 必须实现接口Serializable。 (此接口中没有抽象方法,称为标识接口) > 类中必须显式声明一个全局常量serialVersionUID,用于唯一标识当前类本身。 如果不显式声明,系统会自动分配一个serialVersionUID,但是此属性在类修改的情况下,可能被改变。不建议使用默认情况。 > 自定义类的所有属性也必须是可以序列化的。满足上述的两个条件。 特别的:基本数据类型、String类型本身已经是可以序列化的了。 6.注意点: > 类中声明为static或transient的变量,不能实现序列化。
7. 其它流的使用
- 标准的输入、输出流
System.in: 默认的输入设备:键盘
System.out: 默认的输出设备:显示屏
- 打印流
PrintStream和PrintWriter
-
使用第三方框架
- apache-common包
第16章:网络编程
1. 网络编程概述
1. 要想实现网络通信,需要解决的三个问题:
- 问题1:如何准确地定位网络上一台或多台主机
- 问题2:如何定位主机上的特定的应用
- 问题3:找到主机后,如何可靠、高效地进行数据传输
2. 实现网络传输的三个要素:(对应解决三个问题)
> 通信要素1:IP地址。对应着解决定位网络上主机的问题
> 通信要素2:端口号。区分同一台主机上的不同进程。
> 通信要素3:通信协议。规范通信的规则,进而实现可靠、高效地进行数据传输
2. 要素1:InetAddress类的使用
1. 作用:准确地定位网络上一台或多台主机
2. IP地址分类
> IP地址分类方式1 :IPv4 和 IPv6
> IP地址分类方式2:公网地址( 万维网使用)和 私有地址( 局域网使用)
> 192.168.开头的就是私有地址
3. 本地回路地址:127.0.0.1 ---> localhost
4. 域名: www.atguigu.com www.baidu.com www.jd.com
www.mi.com www.vip.com
5. InetAddress的使用
5.1 作用:InetAddress类的一个实例表示一个具体的ip地址。
5.2 实例化方式与常用方法
> 实例化:getByName(String host) / getLocalHost()
> 方法:getHostName() / getHostAddress()
3. 要素2:端口号
> 唯一标识设备中的进程(应用程序)
> 不同的进程,需要使用不同的端口号
> 用两个字节表示的整数,它的取值范围是0~65535
4. 要素3:网络通信协议
1. 网络通信协议的目的:实现双方可靠、高效的数据传输。
2. 网络参考模型
> OSI参考模型(7层,过于理想化)
> TCP/IP参考模型
> 应用层:HTTP、FTP
> 传输层:TCP、UDP
> 网络层:IP
> 物理+数据链路层
5. TCP网络编程、UDP网络编程
- TCP、UDP的对比
- 熟悉:TCP的三次握手、四次挥手。
- 例题
-
例题1:客户端发送内容给服务端,服务端将内容打印到控制台上。
-
例题2:客户端发送文件给服务端,服务端将文件保存在本地。
-
例题3:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。并关闭相应的连接。
-
6. URL网络编程
URL(Uniform Resource Locator):
1. 作用:
统一资源定位符,它表示 Internet 上某一资源的地址。
2. URL的格式:
http://127.0.0.1:8080/examples/ym.png
应用层协议 ip地址 端口号 资源地址
3. URL类的实例化及常用方法
4. 下载指定的URL的资源到本地(了解)
第17章:反射机制
1. Class 的使用
- 掌握:Class的理解
- 掌握:获取Class的实例(前三种)
/*
* 获取Class实例的几种方式(掌握前三种)
* */
@Test
public void test1() throws ClassNotFoundException {
//1.调用类的静态属性(.class)
Class clazz1 = User.class;
System.out.println(clazz1);
//2. 通过对象调用getClass()
User u1 = new User();
Class clazz2 = u1.getClass();
System.out.println(clazz2);
System.out.println(clazz1 == clazz2); //true
//3. 调用Class的静态方法forName(String className)
Class clazz3 = Class.forName("com.atguigu02._class.User");
System.out.println(clazz1 == clazz3);//true
//4. (了解)使用类的加载器,调用loadClass(String className)
Class clazz4 = ClassLoader.getSystemClassLoader().loadClass("com.atguigu02._class.User");
System.out.println(clazz1 == clazz4);
}
- 熟悉:Class可以指向哪些具体的结构?
- 类、接口、注解、枚举、基本数据类型、数组、void等
2. 类的加载与类的加载器
- 类的加载过程:
![image-20230307084855264](https://s2.loli.net/2023/06/21/Oc4mCoGwhvH3yju.png)
过程1:装载(Loading)
将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成。
过程2:链接(Linking)
①验证Verify:确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。
②准备Prepare:正式为类变量(static)分配内存并`设置类变量默认初始值`的阶段,这些内存都将在方法区中进行分配。
③解析Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
过程3:初始化(Initialization)
- 执行`类构造器<clinit>()方法`的过程。`类构造器<clinit>()方法`是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
- 虚拟机会保证一个`类的<clinit>()方法`在多线程环境中被正确加锁和同步。
- 了解:类的加载器
1. 常见的类的加载器
> 引导类加载器(Bootstrap ClassLoader):负责系统核心api的加载。
> 扩展类加载器(ExtClassLoader):jdk目录下的jre/lib/ext目录下的api
> 应用程序类加载器(或系统类加载器 AppClassLoader):用户自定义的类的默认的类的加载器
2. 相互之间的关系:不是继承关系。
但是我们经常说:应用程序类加载器的父类加载器是扩展类加载器;扩展类加载器的父类加载器是引导类加载器。
为什么习惯这样称谓呢?涉及到类的加载机制:双亲委派机制。
class SystemClassLoader{
ExtClassLoader loader;
public SystemClassLoader(ExtClassLoader loader){
this.loader = loader;
}
}
3.(掌握)使用类的加载器获取流,并读取配置文件信息
/*
* (掌握)使用类的加载器获取流,并读取配置文件信息
* */
@Test
public void test3() throws Exception {
Properties pros = new Properties();
//方式1:默认的文件所在当前module下
// FileInputStream is = new FileInputStream(new File("jdbc.properties"));
FileInputStream is = new FileInputStream(new File("src/jdbc1.properties"));
//方式2:默认的文件所在当前module的src下
// ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// InputStream is = systemClassLoader.getResourceAsStream("jdbc1.properties");
pros.load(is);
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println(name + ":" + password);
}
3. 反射的应用1:创建运行时类的对象(掌握)
- jdk8及之前:Class的newInstance()
1.1 如何实现?
调用Class的方法:newInstance(),返回一个运行时类的实例。
1.2 要想创建对象成功,需要满足:
> 对应的运行时类中必须提供一个空参的构造器
> 对应的运行时类的空参的构造器的访问权限必须够
1.3 回忆:JavaBean中要求给当前类提供一个公共的空参的构造器。有什么用?
> 子类继承父类以后,子类的构造器中在没有显式声明this(形参列表)或super(形参列表)的情况下,默认调用父类空参的构造器
> 提供空参的构造器,便于通过反射的方式动态的创建指定类的对象。
1.4 在jdk9中标识为过时,替换成什么结构:
clazz.getDeclaredConstructor().newInstance()进行替换。
- jdk8之后:调用指定的构造器,设置构造器的访问权限,创建对象
4. 反射的应用2:获取运行时类的完整结构
- 了解:获取所有的属性、所有的方法、所有的构造器、声明的注解等。
- 熟悉:获取运行时类的父类、实现的接口们、所在的包、带泛型的父类、父类的泛型等
5. 反射的应用3:调用指定的属性、方法、构造器
(掌握)反射的应用3:调用指定的结构:指定的属性、方法、构造器
1.1 调用指定的属性:
步骤1:获取指定名称的属性:调用Class类中的getDeclaredField(String fieldName)
步骤2:确保此属性是可访问的:调用Field类中的setAccessible(true);
步骤3:获取此属性的值:调用Field类的get(Object obj)
设置此属性的值:调用Field类的set(Object obj,Object fieldValue)
1.2 调用指定的方法
步骤1:获取运行时类中指明的方法:调用Class类的getDeclaredMethod(String methodName,Class ... paramTypes)
步骤2:确保此方法是可访问的:调用Method类中的setAccessible(true);
步骤3:调用此方法:调用Method类的invoke()方法,此方法的返回值即为invoke方法调用者对应的方法的返回值。
1.3 调用指定的构造器
步骤1:获取指定的构造器:调用Class类的getDeclaredConstructor(Class ... paramTypes)
步骤2:确保此构造器是可访问的:调用Constructor类中的setAccessible(true);
步骤3:调用此构造器,创建运行时类的对象:调用Constructor类中的newInstance(Object ... values);
6. 反射的应用4:注解的使用(了解)
略。为了后期学习框架做准备
7. 体会反射的动态性
见课后练习题。
第18章:JDK8-17新特性
1. 新特性概述
> 角度1:新的语法规则 (多关注)
比如:lambda表达式、enum、annotation、自动拆箱装箱、接口中的默认方法和静态方法、switch表达式、record等
> 角度2:增加、过时、删除API
比如:新的日期时间的API、Optional、String、HashMap、Stream API
> 角度3:底层的优化、JVM参数的调整、GC的变化、内存结构(永久代--->元空间)
2. JDK8:lambda表达式、方法引用等
重点!
3. JDK8:Stream API的使用
重点!
4. 其它新特性
- 熟悉:Optional的使用
- try-catch、switch表达式、var、instanceof模式匹配、密封类等。