— 第一阶段 —

第一章 内容介绍

第二章 Java概述

2.1 Java历史

  • 1995年,sun公司正式发布Java第一个版本
  • 2009年,甲骨文公司收购sun

2.2 Java 技术体系平台

版本 说明
Java SE(Standard Edition)标准版 支持面向桌面级应用(如Windows下的应用程序)的Java平台,提供了完整的Java核心API,此版本以前称为J2SE
Java EE(Enterprise Edition)企业版 是为开发企业环境下的应用程序提供的一套解决方案。该技术体系中包含的技术如:Servlet、Jsp等,主要针对于Web应用程序开发。版本以前称为J2EE
Java ME(Micro Edition)小型版 支持Java程序运行在移动终端(手机、PDA)上的平台,对Java API有所精简,并加入了钛对移动终端的支持,此版本以前称为J2ME

2.3 Java语言特点

  • Java 语言是面向对象的(oop)
  • Java 语言是健壮的。Java 的强类型机制、异常处理、垃圾的自动收集等是 Java 程序健壮性的重要保障
  • Java 语言是跨平台性的。[即: 一个编译好的.class 文件可以在多个系统下运行,这种特性称为跨平台]
  • Java 语言是解释型的

2.4 Java虚拟机及运行过程

  • JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理数据、内存、寄存器,包含在JDK 中.
  • 程序运行过程
1
2
graph LR
A[Test.java]==编译javac==>B[Test.class]==运行java==>c{JVM}
graph LR A[Test.java]==编译javac==>B[Test.class]==运行java==>c{JVM}

2.5 JDK JRE

  • JDK 的全称(Java Development Kit Java 开发工具包)

JDK = JRE + 开发工具集(例如 Javac,java 编译工具等)

  • JRE(Java Runtime Environment Java 运行环境)

JRE = JVM + Java SE 标准类库(java 核心类库)

2.6 Java注释

  • 单行注释 //

  • 多行注释 /* */

  • 文档注释 /** */

    • 文档注释内容可以白JDK提供的工具javadoc所解析,生成一套以网页文件形式体现的该程序的说明文档。一般写在类和方法前。

    • javadoc -d d:\temp -author -version Test.java

    • javadoc标签

      1
      2
      3
      4
      /**
      * @author Durango
      * @version 1.0
      */

2.7 Java代码规范

  • 类、方法的注释,要以javadoc的方式来写。
  • 非Java Doc的注释,往往是给代码的维护者看的,着重告述读者为什么这样写,如何修改,注意什么问题等
  • 使用tab操作,实现缩进,默认整体向右边移动,时候用shift+tab整体向左移4、运算符和=两边习惯性各加一个空格。比如:2 + 4 * 5 + 345 - 89
  • 源文件使用utf-8编码
  • 行宽度不要超过80字符
  • 代码编写次行风格(推荐)和行尾风格

第三章 基本数据类型

3.1 数据类型(必背)

1
2
3
4
5
6
7
8
9
10
11
graph LR
A[Java数据类型]-->B[基本数据类型]
A-->C[引用数据类型]
B-->D[数值型]
D-->d1["整型: byte【1】,short【2】,int【4】,long【8】"]
D-->d2["浮点型: float【4】,double【8】"]
B-->E["字符型: char【2】"]
B-->F["布尔型: boolean【1】"]
C-->G[类class]
C-->H[接口interface]
C-->I["数组[ ]"]
graph LR A[Java数据类型]-->B[基本数据类型] A-->C[引用数据类型] B-->D[数值型] D-->d1["整型: byte【1】,short【2】,int【4】,long【8】"] D-->d2["浮点型: float【4】,double【8】"] B-->E["字符型: char【2】"] B-->F["布尔型: boolean【1】"] C-->G[类class] C-->H[接口interface] C-->I["数组[ ]"]

3.2 整数类型

  • 表示范围
类型 占用空间 范围
byte 1字节 -128 ~ 127
short 2字节 -2^15 ~ 2^15-1
int 4字节 -2^31 ~ 2^31-1
long 8字节 -2^63 ~ 2^63-1

  • 注意细节

​ java的整数常量默认为int,声明long型常量时,须后加’L’或‘l’

1
2
long a = 12L
int b = 12L // 报错,long转化为int会有精度损失

3.3 浮点类型

  • 表示范围
类型 占用空间 范围
float(单精度) 4 -3.403E38 ~ 3.403E38
double(双精度) 8 -1.798E308 ~ 1.798E308
  • 说明:浮点数=符号位+指数位+尾数位
  • 注意细节:
    • java的浮点型常量默认为double型,声明float型常量时,须后加‘F’或‘f’
    • 通常情况下使用double,它比float更加精确
  • 两种表示形式
    • 十进制:5.12 0.512f .512(0可以省略)
    • 科学计数法:5.12e2 (5.12*10的2次方) 5.12E-2 (5.12/10的2次方)
  • 浮点数使用陷阱
1
2
3
4
5
6
7
8
9
10
11
12
13
// 一个重要的使用点: 当我们对运算结果是小数, 进行相等判断时要小心,应该是以两个数的差值的绝对值,在某个精度范围类判断
double num11 = 2.7;
double num12 = 2.7; //8.1 / 3; //2.7
System.out.println(num11);//2.7
System.out.println(num12);//接近 2.7 的一个小数,而不是 2.7
// 错误写法
if( num11 == num12) {
System.out.println("num11 == num12 相等");
}
// 正确写法
if(Math.abs(num11 - num12) < 0.000001 ) {
System.out.println("差值非常小,到我的规定精度,认为相等...");
}

3.4 字符类型

  • 使用细节

    • java中,char的本质是一个整数,在输出时,是Unicode码对应的字符
    • char类型时可以进行运算法
  • Java API

  • 字符型的本质

    • 存储:‘a’ ==> 码值97 ==> 二进制(110 0001)==> 存储
    • 读取:二进制(110 0001)==> 码值97 ==> ‘a’
  • 常见编码

    • ASCII (ASCII编码表,一个字节表示,一共128个字符, 实际上一个字节可以表示256个字符,只用128个)

    • Unicode (Unicode编码表固定大小的编码使用两个字节来表示字符,字母和汉字统一都是占用两个字节,这样浪费空间)

    • utf-8(编码表,大小可变的编码字母使用1个字节,汉字使用3个字节)gbk(可以表示汉字,而且范围广,字母使用1个字节,汉字2个字节)gb2312(可以表示汉字,gb2312 <gbk)

    • big5码(繁体中文,台湾,香港)

3.5 布尔类型

  • 占1个字节
  • true or false
  • java中不可以用0或非0的整数替代false或true。(C语言中可以)

3.6 数据类型转换

3.6.1. 自动类型转换(必背)

1
2
3
graph LR
char-->int-->long-->float-->double
byte-->short-->int
  • 注意和细节

​ (1)有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大的那种数据类型,然后再进行计算。

​ (2)当我们把精度(容量)大的数据类型赋值给精度(容量)小的数据类型时,就会报错,反之就会进行自动类型转换。

​ (3)(byte,short)和char之间不会相互自动转换。

​ (4)byte, short,char 他们三者可以参与计算,在计算时首先转换为int类型

​ (5)boolean不参与转换

​ (6)自动提升原则:表达式结果的类型自动提升为操作数中最大的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//自动类型转换细节
public class AutoConvertDetail {
//编写一个 main 方法
public static void main(String[] args) {
//细节 1: 有多种类型的数据混合运算时,
//系统首先自动将所有数据转换成容量最大的那种数据类型,然后再进行计算
int n1 = 10; //ok
float d1 = n1 + 1.1;//错误 n1 + 1.1 => 结果类型是 double
double d1 = n1 + 1.1;//对 n1 + 1.1 => 结果类型是 double
float d1 = n1 + 1.1F;//对 n1 + 1.1 => 结果类型是 float

//细节 2: 当我们把精度(容量)大 的数据类型赋值给精度(容量)小 的数据类型时,
//就会报错,反之就会进行自动类型转换。
int n2 = 1.1;//错误 double -> int

//细节 3: (byte, short) 和 char 之间不会相互自动转换
//当把具体数赋给 byte 时,(1)先判断该数是否在 byte 范围内,如果是就可以
byte b1 = 10; //对 , -128-127
int n2 = 1; //n2 是 int
byte b2 = n2; //错误,原因: 如果是变量赋值,判断类型
char c1 = b1; //错误, 原因 byte 不能自动转成 char

//细节 4: byte,short,char 他们三者可以计算,在计算时首先转换为 int 类型
byte b2 = 1;
byte b3 = 2;
short s1 = 1;
short s2 = b2 + s1;//错, b2 + s1 => int
int s2 = b2 + s1;//对, b2 + s1 => int
byte b4 = b2 + b3; //错误: b2 + b3 => int

//细节5boolean 不参与转换
boolean pass = true;
//int num100 = pass;// 错误,boolean 不参与类型的自动转换

//自动提升原则: 表达式结果的类型自动提升为 操作数中最大的类型
//看一道题
byte b4 = 1;
short s3 = 100;
int num200 = 1;
float num300 = 1.1F;
double num500 = b4 + s3 + num200 + num300; //float -> double

3.6.2 强制类型转换

  • 当进行数据从高精度到低精度转换时,需要使用强制转换
  • 强制转换符 如(int) (byte)等
  • char类型可以用int常量值赋值,但是不能用int变量值赋值
  • byte, short, char类型在参与运算时,当做int类型处理

3.6.3 与字符串转换

  • 基本数据类型 -> 字符串

    1
    2
    int n1 = 100;
    String s1 = n1 + "";
  • 字符串 -> 基本数据类型

    1
    2
    3
    4
    5
    6
    7
    8
    String s5 = "123";
    int num1 = Integer.parseInt(s5);
    double num2 = Double.parseDouble(s5);
    float num3 = Float.parseFloat(s5);
    long num4 = Long.parseLong(s5);
    byte num5 = Byte.parseByte(s5);
    boolean b = Boolean.parseBoolean("true");
    short num6 = Short.parseShort(s5)

第四章 运算符

4.1 算数运算符

image-20230909141542866

  • 注意事项

  1. 对于除号”/“,它的整数除和小数除是有区别的: **整数之间做除法时,只保留整数部分而舍弃小数部分**。例如: int x= 10/3,结果是3
  2. 当对一个数取模时,可以等价a%b = a-a/b*b,这样我们可以看到取模的一个本质运算。
  3. 当自增当做一个独立语言使用时,不管是 ++i 还是 i++ 都是一样的
  4. 当自增当做一个表达式使用时 j=++i 等价于 i=i+1; j=i
    当自增当做一个表达式使用时 j=i++ 等价 j=i; i=i+1

1
2
3
4
5
6
7
8
9
10
11
12
// % 取模 ,取余
// 在 % 的本质 看一个公式!!!! a % b = a - a / b * b
// -10 % 3 => -10 - (-10) / 3 * 3 = -10 + 9 = -1
// 10 % -3 = 10 - 10 / (-3) * (-3) = 10 - 9 = 1
// -10 % -3 = (-10) - (-10) / (-3) * (-3) = -10 + 9 = -1

System.out.println(10 % 3); //1
System.out.println(-10 % 3); // -1
System.out.println(10 % -3); //1
System.out.println(-10 % -3);//-1

System.out.println(10 / 3); //3

4.2 关系运算符

image-20230909142820955

4.3 逻辑运算符

image-20230909143251548

  • && 和 & 的区别

    • &&短路与:如果第**一个条件为 false,则第二个条件不会判断**,最终结果为 false,效率高

    • & 逻辑与:不管第一个条件是否为 false,第二个条件都要判断,效率低

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      //对于&&短路与而言,如果第一个条件为 false ,后面的条件不再判断
      //对于&逻辑与而言,如果第一个条件为 false ,后面的条件仍然会判断
      int a = 4;
      int b = 9;
      if(a < 1 && ++b < 50) {
      System.out.println("ok300");
      }
      System.out.println("a=" + a + " b=" + b);// 4 9

      if(a < 1 & ++b < 50) {
      System.out.println("ok300");
      }
      System.out.println("a=" + a + " b=" + b);// 4 10
  • || 和 | 的区别

    • ||短路或:如果**第一个条件为 true,则第二个条件不会判断**,最终结果为 true,效率高
    • | 逻辑或:不管第一个条件是否为 true,第二个条件都要判断,效率低
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //(1)||短路或:如果第一个条件为 true,
    //则第二个条件不会判断,最终结果为 true,效率高
    //(2)| 逻辑或:不管第一个条件是否为 true,第二个条件都要判断,效率低
    int a = 4;
    int b = 9;
    if( a > 1 || ++b > 4) { // 可以换成 | 测试
    System.out.println("ok300");
    }
    System.out.println("a=" + a + " b=" + b); //4 9

    if( a > 1 | ++b > 4) {
    System.out.println("ok300");
    }
    System.out.println("a=" + a + " b=" + b); //4 10

4.4 赋值运算符

  • 复合赋值运算符会进行类型转换
1
2
3
byte b = 3;
b += 2; //等价于 b = (byte)(b + 2)
b ++; //等价于 b = (byte)(b + 1)

4.5 三元运算符

条件表达式 ? 表达式 1: 表达式 2;

运算规则:

  1. 如果条件表达式为 true,运算后的结果是表达式 1;

  2. 如果条件表达式为 false,运算后的结果是表达式 2;

1
2
3
4
5
6
7
8
9
10
int a = 10;
int b = 99;
// 解读
// 1. a > b 为 false
// 2. 返回 b--, 先返回 b 的值,然后在 b-1
// 3. 返回的结果是 99
int result = a > b ? a++ : b--;
System.out.println("result=" + result); // 99
System.out.println("a=" + a); // 10
System.out.println("b=" + b); // 98
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//案例:实现三个数的最大值
int n1 = 553;
int n2 = 33;
int n3 = 123;
//思路
//1. 先得到 n1 和 n2 中最大数 , 保存到 max1
//2. 然后再 求出 max1 和 n3 中的最大数,保存到 max2
int max1 = n1 > n2 ? n1 : n2;
int max2 = max1 > n3 ? max1 : n3;
System.out.println("最大数=" + max2);

//使用一条语句实现, 推荐使用上面方法
//老师提示: 后面我们可以使用更好方法,比如排序
// int max = (n1 > n2 ? n1 : n2) > n3 ?
// (n1 > n2 ? n1 : n2) : n3;
// System.out.println("最大数=" + max);

4.6 标识符

  • 标识符命名规则

  1. 由26个英文字母大小写, 0-9,或 $ 组成

  2. 数字不可以开头。int 3ab = 1;//错误

  3. 不可以使用关键字和保留字,但能包含关键字和保留字。

  4. Java中严格区分大小写,长度无限制。int totalNum = 10; int n = 90;

  5. 标识符不能包含空格。int a b = 90;

  • 标识符命名规范

  1. 包名:多单词组成时所有字母都小写:aaa.bbb.ccc //比如 com.hsp.crm

  2. 类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz [大驼峰]

  3. 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZz[小驼峰, 简称 驼峰法]

  4. 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ

4.7 键盘输入

  1. next():接收用户输入字符串

  2. next().charAt(0):接收用户输入的一个字符

  3. nextInt():接收用户输入的int

  4. nextDouble():接收用户输入的Double

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Scanner;//表示把 java.util 下的 Scanner 类导入
public class Input {
public static void main(String[] args) {
//步骤
//Scanner 类 表示 简单文本扫描器,在 java.util 包
//1. 引入/导入 Scanner 类所在的包
//2. 创建 Scanner 对象 , new 创建一个对象
Scanner myScanner = new Scanner(System.in);
//3. 接收用户输入了,使用相关的方法
System.out.println("请输入名字");
//当程序执行到 next 方法时,会等待用户输入~~~
String name = myScanner.next(); //接收用户输入字符串
System.out.println("请输入年龄");
int age = myScanner.nextInt(); //接收用户输入 int
System.out.println("请输入薪水");
double sal = myScanner.nextDouble(); //接收用户输入 double
System.out.println("人的信息如下:");
System.out.println("名字=" + name
+ " 年龄=" + age + " 薪水=" + sal);
}
}

4.8 进制(基本功)

4.8.1 进制介绍

  • 对于整数,有四种表示方式:
    • 二进制:0,1 ,满 2 进 1. 0b0B 开头
    • 十进制:0-9 ,满 10 进 1。
    • 八进制:0-7 ,满 8 进 1. 以数字 以数字 0 开头表示 开头表示
    • 十六进制:0-9 及 A(10)-F(15),满 16 进 1. 0x 或 0X 开头表示。此处的 A-F 不区分大小写。

4.8.2 进制转化

  • k进制转十进制

​ 从最低位(右边)开始,将每个位上的数提取出来,乘以 k的(位数-1)次方 后再求和

  • 十进制转k进制

    除k取余法

  • 二进制转八进制(十六进制)

​ 从低位开始, 将二进制数(十六进制数)每三位(四位)一组,转成对应的八进制数(十六进制数)即可。

  • 八进制(十六进制)转二进制

​ 将八进制数(十六进制)每 1 位,转成对应的一个 3 位(4位)的二进制数即可。

4.8.3 源码、反码、补码(必背)

  1. 二进制的最高位是符号位: 0表示正数,1表示负数
  2. 正数的原码,反码,补码都一样(三码合一)
  3. 负数的反码=它的原码符号位不变, 其它位取反(0->1,1->0)
  4. 负数的补码=它的反码+1,负数的反码=负数的补码-1,0的反码,补码都是0
  5. java没有无符号数,换言之,java中的数都是有符号的
  6. 在计算机运算的时候,都是以补码的方式来运算的.
  7. 当我们看运算结果的时候,要看他的原码(重点)

4.9 位运算

  • 按位与&:两位全为1,结果为1,否则为0
  • 按位或:两位有一个为1,结果为1,否则为0
  • 按位异或^:两位一个为0,一个为1,结果为1,否则为0
  • 按位取反~:0->1,1->0
  • 算术右移 >>:低位溢出,符号位不变,并用符号位补溢出的高位
  • 算术左移 <<:符号位不变,低位补 0
  • 无符号右移>>>:运算规则是: 低位溢出,高位补 0
  • 没有无符号左移
  • 操作过程:先得到原码,再得到补码,在补码上进行操作,最后还原为原码
1
2
3
4
5
6
7
8
9
10
11
12
// 1. 先得到-2的原码(4字节) 10000000 00000000 00000000 00000010
// 2. -2的反码 11111111 11111111 11111111 11111101
// 3. -2的补码 11111111 11111111 11111111 11111110
// 4. ~(-2)操作 00000000 00000000 00000000 00000001 运算后的补码
// 5. 运算后的补码为正数,源码也为 00000000 00000000 00000000 00000001 => 1
System.out.println(~(-2))

// 1. 先得到2的原码(4字节) 00000000 00000000 00000000 00000010 (正数)
// 2. ~2操作 11111111 11111111 11111111 11111101 运算后的补码(负数)
// 3. -2补码的反码(补码-1) 11111111 11111111 11111111 11111100
// 3. ~2补码的原码 10000000 00000000 00000000 00000011 => -3
System.out.println(~2)
1
2
3
4
5
6
// 00000000 00000000 00000000 00000001
// 00000000 00000000 00000000 00000000
System.out.println(1>>2)
// 00000000 00000000 00000000 00000001
// 00000000 00000000 00000000 00000100
System.out.println(1>>2)

第五章 程序控制结构

5.1 顺序结构

5.2 分支控制

  • 单分支

    1
    2
    3
    if(条件表达式) {
    执行代码块;
    }
  • 双分支

    1
    2
    3
    4
    5
    6
    if(条件表达式) {
    执行代码块1;
    }
    else {
    执行代码块2;
    }
  • 多分支

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    if(条件表达式) {
    执行代码块1;
    }
    if else(条件表达式2) {
    执行代码块2;
    }
    ...
    if else(条件表达式n) {
    执行代码块n;
    }
    else { //可以没有else
    执行代码块n+1;
    }
  • switch分支

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    switch(表达式) {
    case 常量1
    语句块1;
    break;
    case 常量2
    语句块2;
    break;
    ...
    case 常量n:
    语句块n;
    break;
    defalut:
    default语句块;
    break;
    }
    • 特别注意:当前一个case没有break时,会直接进入下一个case的语句块,而不需要判断

    image-20230910160242133

    • 使用细节

    1. 表达式数据类型,应和case后的常量类型一致,或者是可以自动转换成可以相互比较的类型,比如输入的是字符,而常量是int
    2. switch(表达式)中表达式的返回值必须是: byte, short, int, char, enum[枚举], String
      double c = 1.1; switch(c)//错误
    3. case子句中的值必须是常量,而不能是变量
    4. default子句是可选的,当没有匹配的case时,执行default
    5. break语句用来在执行完一个case分支后使程序跳出switch语句块; 如果没有写break,程序会顺序执行到switch结尾,除非遇到break; (穿透现象)

  • switch和if的比较
    • 如果判断的具体数值不多,而且符合 byte、 short 、int、 char, enum[枚举], String 这 6 种类型。虽然两个语句都可以使用,建议使用 swtich 语句。
    • 其他情况:对区间判断,对结果为 boolean 类型判断,使用 if,if 的使用范围更广

5.3 循环控制

5.3.1 for循环

1
2
3
for (循环变量初始化; 循环条件; 循环变量迭代) {
循环操作;
}

image-20230911162316011

  • for 有四要素: (1)循环变量初始化 (2)循环条件 (3)循环操作 (4)循环变量迭代

  • for(; ;)表示无限循环

  • for(; 循环判断条件; ) 中的初始化和变量迭代可以写到其它地方,但是两边的分号不能省略。

    1
    2
    3
    4
    5
    int i = 1;
    for( ; i <= 10; ) {
    System.out.println(i);
    i++
    }
  • 循环初始值可以有多条初始化语句,但要求类型一样,并且中间用逗号隔开,循环变量迭代也可以有多条变量迭代语句,中间用逗号隔开.

    1
    2
    3
    for (int i = 0, j = 0; i < 100; i++, j += 2) {
    System.out.println("i=" + i + "j=" + j)
    }

5.3.2 while循环

1
2
3
4
5
循环变量初始化;
while(循环条件) {
循环体;
循环变量迭代;
}

image-20230911163633782

5.3.3 do while循环

1
2
3
4
5
循环变量初始化;
do{
循环体(语句); // 至少执行一次循环体
循环变量迭代;
}while(循环条件);

image-20230911163844949

5.3.4 嵌套循环

编程思想:化繁为简,先死后活

化繁为简:将复杂的程序简化为多个简单的步骤

先死后活:先考虑常量的情况,再考虑变量的情况

  • 打印九九乘法表

    1
    2
    3
    4
    5
    6
    7
    for(int i = 1; i <= 9; i++) {
    for (int j = 1; j <= i; j++) {
    System.out.print(j + "*" + i +"=" + j * i);
    System.out.print("\t");
    }
    System.out.println();
    }

    image-20230911165707995

  • 打印空心金字塔

    • 化繁为简
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // 第一版
    for(int i = 1; i <= 5; i++) {
    for(int j = 1; j <= 2 * i - 1; j++) {
    System.out.print("*");
    }
    System.out.println();
    }

    // 第二版
    for(int i = 1; i <= 5; i++) {
    // 空格数 = 层数 - 当前层数
    for (int k = 1; k <= 5 - i; k++) {
    System.out.print(" ");

    }
    for(int j = 1; j <= 2 * i - 1; j++) {
    System.out.print("*");
    }
    System.out.println();
    }

    // 第三版
    for(int i = 1; i <= 5; i++) {
    for (int k = 1; k <= 5 - i; k++) {
    System.out.print(" ");

    }
    for(int j = 1; j <= 2 * i - 1; j++) {
    // 每行只有第一个位置和最后一个位置打印*
    // 最后一行全部输出
    if(j == 1 || j == 2 * i - 1 || i == 5) {
    System.out.print("*");
    } else { //其他位置打印空格
    System.out.print(" ");
    }
    }
    System.out.println();
    }
    • 先死后活
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 终版
    Scanner myScanner = new Scanner(System.in);
    System.out.println("请输入金字塔层数:");
    int level = myScanner.nextInt();
    // 第三版
    for(int i = 1; i <= level; i++) {
    for (int k = 1; k <= level - i; k++) {
    System.out.print(" ");

    }
    for(int j = 1; j <= 2 * i - 1; j++) {
    // 每行只有第一个位置和最后一个位置打印*
    // 最后一行全部输出
    if(j == 1 || j == 2 * i - 1 || i == level) {
    System.out.print("*");
    } else { //其他位置打印空格
    System.out.print(" ");
    }
    }

    image-20230912104055452

5.3.5 break跳转

  • 跳出最近的循环体

  • break语句出现在多层嵌套的语句块中时,可以通过**标签**指明要终止的是哪一层语句块。

  • label1: { …{break label1}… }

  • label1是标签,名字由程序员指定,可以是任意如abc1,ABC2

  • 在实际开发中尽量不要使用标签

  • 如果break没有指定标签,默认退出最近的循环体

5.3.6 continue跳转

  • 结束本次循环,继续执行下一次循环
  • continue 语句出现在多层嵌套的循环语句体中时,可以通过**标签**指明要跳过的是哪一层循环 , 这个和前面的标签的使用的规则一样.

第六章 数组

6.1 数组初始化

6.1.1 静态初始化

  • 语法:数据类型[]  数组名 = {元素值1,元素值2,….}数据类型 数组名[] = {元素值1,元素值2,….}数据类型 数组名[] = new 数据类型[ ]{元素值1,元素值2,….} .

  • ```java
    int a[] = {1, 2, 3, 4};
    int[] b = {5, 6, 7, 8};

    1
    2
    3
    4
    5

    - ```java
    int a[] = {1, 2, 3, 4};
    double b[] = {1.1, 2.2, 3.4};
    char c[] = {'a', 'b', 'c'};
  • ```java
    int a[] = new int[]{1, 2, 3, 4};
    double b[] = new double[]{1.1, 2.2, 3.4};
    char c[] = new char[]{‘a’, ‘b’, ‘c’};

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12



    ### 6.1.2 动态初始化

    - 语法:<font color='red'>数据类型[] 数组名 = new 数据类型[大小]</font> 或 <font color='red'>数据类型 数组名[] = new 数据类型[大小]</font>
    - ```java
    int a[] = new int[4];
    int[] b = new int[4];

    int c[]; // int[] c;
    c = new int[4];
  • ```java
    char[] table = new char[26];
    for(int i = 0; i < table.length; i++) {

    // 'a' + i是int类型,需要强制转换为char
    table[i] = (char)('a' + i);
    

    }
    System.out.println(“==table数组===”);
    for(int i = 0; i < table.length; i++) {

    System.out.print(table[i] + " ");
    

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26



    ## 6.2 使用细节

    - 数组是多个**相同类型数据**的组合,实现对这些数据的统一管理
    - 数组中的**元素可以是任何数据类型**,包括基本类型和引用类型,但是不能混用。
    - 数组创建后,如果没有赋值,有<a id="myid"> **默认值** </a>**int 0,short 0, byte 0, long 0, float 0.0,double 0.0,char \u0000,boolean false,String null**
    - 数组的**下标是从** **0** **开始的**。
    - 获取数组长度:<font color='red'>数组名.length</font>
    - 数组下标必须在指定范围内使用,否则报:下标越界异常
    - 数组属**引用类型**,数组型数据是对象(object)代码



    ## 6.3 数组赋值机制

    - 基本数据类型是<font color='red'>值传递</font>,赋值类型是<font color='red'>值拷贝</font>

    ```java
    // b的变化不会影响到a
    int n1 = 10;
    int n2 = a;
    n2 = 80;
    System.out.println(n1); // 10
    System.out.println(n2); // 80
  • 数组在默认情况下是**引用传递,赋的值是地址**,赋值方式为地址拷贝

    1
    2
    3
    4
    5
    6
    7
    // b的变化会影响到a
    int[] arr1 = {1, 2, 3};
    int[] arr2 = arr1; //把数组a赋值给b,赋的是地址
    arr2[0] = 10;
    for(int i = 0; i < arr1.length; i++) {
    System.out.println(arr1[i]);
    } // 10, 2, 3
  • 值传递和引用传递区别

    image-20230913101848477

6.4 数组拷贝

1
2
3
4
5
6
7
8
9
10
11
12
int[] arr1 = {1, 2, 3, 4};
int[] arr2 = new int[arr1.length];

for(int i = 0; i < arr1.length; i++) {
arr2[i] = arr1[i];
}

arr2[0] = 0;
for(int i = 0; i < arr2.length; i++) {
System.out.print(arr1[i] + " ");
System.out.println(arr2[i]);
}

6.5 数组反转

1
2
3
4
5
6
7
8
9
10
11
12
int[] arr = {11, 22, 33, 44, 55, 66, 77};
int temp = 0;
int len = arr.length;
for(int i = 0; i < len / 2; i++) {
temp = arr[i];
arr[i] = arr[len - 1 - i];
arr[len - 1 - i] = temp;
}
System.out.print("反转后的数组");
for(int i = 0; i < len; i++) {
System.out.print(arr[i] + " ");
}

6.6 数组扩容

  • 要求:实现动态的给数组添加元素效果,实现对数组扩容。

  • ```Java
    // 1. 定义初始数组 int[] arr = {1,2,3}//下标 0-2
    // 2. 定义一个新的数组 int[] arrNew = new int[arr.length+1];
    // 3. 遍历 arr 数组,依次将 arr 的元素拷贝到 arrNew 数组
    // 4. 将 4 赋给 arrNew[arrNew.length - 1] = 4;把 4 赋给 arrNew 最后一个元素
    // 5. 让 arr 指向 arrNew ; arr = arrNew; 那么 原来 arr 数组就被销毁
    int[] arr = {1, 2, 3};
    int[] arrNew = new int[arr.length + 1];
    for(int i = 0; i < arr.length; i++) {

    arrNew[i] = arr[i];   
    

    }
    arrNew[arrNew.length - 1] = 4;
    arr = arrNew;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60



    ## 6.7 冒泡排序

    - 基本思想:每一轮,依次比较相邻元素的值,若发现逆序则交换,使得较大的元素逐渐从前向后移动,就像水底的气泡一样逐渐向上冒。

    - 例子

    ![image-20230914092459709](Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/image-20230914092459709.png)

    - 化繁为简,先死后活

    ```java
    int[] arr = [24, 69, 80, 57, 13]
    int temp;

    // 第一轮,4次比较
    for(int j = 0; j < 4; j++) {
    if(arr[j] > arr[j + 1]) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    // 第二轮,3次比较
    for(int j = 0; j < 3; j++) {
    if(arr[j] > arr[j + 1]) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    // 第三轮,2次比较
    for(int j = 0; j < 2; j++) {
    if(arr[j] > arr[j + 1]) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    // 第四轮,1次比较
    for(int j = 0; j < 1; j++) {
    if(arr[j] > arr[j + 1]) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }

    // 一共有四轮比较,可用i代表(轮数-1),每轮比较4-i次
    for(int i = 0; i < 4; i++) {
    for(int j = 0; j < 4 - i; j++) {
    if(arr[j] > arr[j + 1]) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 数组长度length,一共需要比较length-1轮(冒泡length-1次),第i轮需要比较length-1-i次
    for(int i = 0; i < arr.length - 1; i++) {
    for(int j = 0; j < arr.length - 1 - i; j++) {
    if(arr[j] > arr[j + 1]) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    }

6.8 二维数组

image-20230914102416900

6.8.1 初始化

  • 静态初始化:

    • 数据类型[][] 数组名 = <!–swig360–>
    • 数据类型 数组名[][] = <!–swig361–>
    • 数据类型[] 数组名[] = <!–swig362–>
  • 动态初始化1:

    • 数据类型[][] 数组名 = new 数据类型[大小][大小]
    • 数据类型 数组名[][] = new 数据类型[大小][大小]
    • 数据类型[] 数组名[] = new 数据类型[大小][大小]
  • 动态初始化2(列数不确定):

    • 数据类型[][] 数组名 = new 数据类型[大小][]
    • 数据类型 数组名[][] = new 数据类型[大小][]
1
2
3
4
5
6
int[][] arr = new int[3][3];
for(int i = 0; i < arr.length; i++) {
for(int j = 0; j < arr[i].length; j++) {
arr[i][j] = (i + 1) * (j + 1);
}
}
1
2
3
4
5
6
7
8
9
// 声明时列数不确定
int[][] arr = new int[3][];
for(int i = 0; i < arr.length; i++) {
//给每一个一维数组arr[i]开辟空间
arr[i] = new int[i + 1];
for(int j = 0; j < arr[i].length; j++) {
arr[i][j] = i + 1;
}
}

6.8.2 应用-杨辉三角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1

规律:
1.第n行有n个元素
2. 每一行第一个和最后一个元素都为1
3. 从第三行开始,对于中间元素
[i][j] = arr[i - 1][j] + arr[i - 1][j - 1]
*/
int[][] yanghui = new int[10][];
for(int i = 0; i < yanghui.length; i++) {
// 给每一个一维数组开辟空间
yanghui[i] = new int[i + 1];
// 赋值
for(int j = 0; j < yanghui[i].length; j++) {
if(j == 0 || j == yanghui[i].length - 1) {
yanghui[i][j] = 1;
} else {
yanghui[i][j] = yanghui[i - 1][j] + yanghui[i - 1][j - 1];
}
}
}

第七章 面向对象编程(基础)

7.1 类与对象

  • 案例

    1
    2
    3
    4
    5
    6
    class Cat {
    String name;
    int age;
    String color;
    double weight;
    }

7.1.1 类与对象的区别和联系

  • 类是抽象的,概念的,代表一类事物,比如人类,猫类.., 即它是数据类型.
  • 对象是具体的,实际的,代表一个具体事物, 即 是实例.
  • 类是对象的模板,对象是类的一个个体,对应一个实例

7.1.2 对象在内存中存在形式(重要!)

中存放的是对象的引用(对象名),真正的对象在中。

​ 年龄12是一个基本数据类型(整型),直接存放在堆中;名字和颜色是一个引用类型(字符串),在堆中存放的是地址,真正的数据存放在方法区的常量池中。

image-20230915192816359

7.1.3 成员变量|属性|字段

  • 定义:访问修饰符 属性类型 属性名

  • 赋值:属性如果不赋值,有默认值,规则和数组一致。具体说: int 0,short 0, byte 0, long 0, float 0.0,double 0.0,

    char \u0000,boolean false,String null

  • 访问属性:对象名.属性名

7.1.4 创建对象

  • 先声明在创建

    1
    2
    3
    // 声明时会在栈中创建一个值为空的变量,创建时会在堆中创建一个对象并把它的地址赋给这个变量
    Cat cat;
    cat = new Cat();
  • 直接创建

    1
    Cat cat = new Cat();

7.1.5 类和对象的内存分配机制(重要!)

1
2
3
4
5
6
7
8
Person p1 = new Person();
p1.name = "小明";
p1.age = 18;

Person p2 = p1;
p2.age = 80;
System.out.print(p1.age);//80
System.out.print(p2.name);//小明

image-20230916184807203

  • java的内存结构
  1. 栈: 一般存放基本数据类型(局部变量)
  2. 堆: 存放对象(Cat cat , 数组等)
  3. 方法区:常量池(常量,比如字符串), 类加载信息
  • java创建对象的流程
  1. 先加载 Person 类信息到方法区中(属性和方法信息, 只会加载一次)
  2. 中分配空间, 进行默认初始化(看规则)
  3. 把地址赋给中的变量p , p 就指向对象
  4. 进行指定初始化, 比如 p.name =”jack” p.age = 10

7.2 成员方法

7.2.1 方法的调用机制(重要!)

image-20230916191418109

7.2.2 方法的作用

  • 提高代码的复用性
  • 将实现的细节进行封装,然后供其他用户直接调用而不需要了解其内部细节

7.2.3 方法的定义

1
2
3
4
public 返回数据类型 方法名(形参列表...) {
语句; //方法体
return; //可以没有return,此时返回数据类型为void
}

7.2.4 方法的使用细节

  • 访问修饰符:控制方法的使用范围,有 public,protected,默认,private
  • 放回数据类型:
    • 一个方法最多有一个返回值 [思考,如何返回多个结果 返回数组 ]
    • 返回类型可以为任意类型,包含基本类型或引用类型(数组,对象)
    • 如果方法要求有返回数据类型,则方法体中最后的执行语句必须为 return 值; 而且要求返回数据类型必须和 return 的值类型一致或兼容(可以自动转换)
    • 如果方法是 void,则方法体中可以没有 return 语句,或者 只写 return ;
  • 方法名
    • 遵循小驼峰命名法
    • 最好见名知义,如求和getSum
  • 形参列表
    • 调用带参数的列表时,传入的参数必须和参数列表是相同类型或兼容类型
    • 方法定义时的参数成为形参,方法调用时传入的参数成为实参,实参和新参的类型必须一致或兼容,个数、顺序必须一致
  • 方法体:方法体里面不能再定义方法,即方法不能嵌套定义

7.2.5 方法的调用

  • 同一个类中调用:直接调用即可
  • 跨类中的方法调用:需要创建对象,对象名.方法名( ),能否调用与方法的访问修饰符有关

7.3 成员方法的传参机制(重要!)

7.3.1 值传递

  • 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MethodParameter01 {
//编写一个 main 方法
public static void main(String[] args) {
int a = 10;
int b = 20;
//创建 AA 对象 名字 obj
AA obj = new AA();
obj.swap(a, b); //调用 swap
System.out.println("main 方法 a=" + a + " b=" + b); //a=10 b=20
}
}

class AA {
public void swap(int a,int b){
System.out.println("\na 和 b 交换前的值\na=" + a + "\tb=" + b);//a=10 b=20
//完成了 a 和 b 的交换
int tmp = a;
a = b;
b = tmp;
System.out.println("\na 和 b 交换后的值\na=" + a + "\tb=" + b);//a=20 b=10
}
}
  • 基本数据类型的传参机制是值传递(值拷贝),形参的任何改变不会影响实参。

image-20230918132209849

7.3.2 地址传递

  • 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public class MethodParameter02 {
    //编写一个 main 方法
    public static void main(String[] args) {
    //测试
    B b = new B();
    int[] arr = {1, 2, 3};
    b.test100(arr);//调用方法
    System.out.println(" main 的 arr 数组 ");
    // 遍历数组
    for(int i = 0; i < arr.length; i++) {
    System.out.print(arr[i] + "\t"); // {200, 2, 3}
    }
    System.out.println();

    //测试
    Person p = new Person();
    p.name = "jack";
    p.age = 10;
    b.test200(p);
    //测试题, 如果 test200 执行的是 p = null ,下面的结果是 10
    //测试题, 如果 test200 执行的是 p = new Person();..., 下面输出的是 10
    System.out.println("main 的 p.age=" + p.age);//10000
    }
    }

    class Person {
    String name;
    int age;
    }

    class B {
    //可以接收一个数组,在方法中修改该数组,看看原来的数组是否变化
    public void test100(int[] arr) {
    arr[0] = 200;//修改元素
    }

    public void test200(Person p) {
    p.age = 10000; //修改对象属性
    // 思考
    // p = new Person();
    // p.name = "tom";
    // p.age = 99;
    // 思考
    // p = null;
    }

  • 引用数据类型的传参机制是地址传递(地址拷贝),形参的改变会影响实参。除非形参指向新的地址

image-20230918135155966

image-20230918140357919

7.4 递归调用

7.4.1 注意事项

  • 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
  • 方法的局部变量是独立的,不会相互影响, 比如n变量
  • 如果方法中使用的是引用类型变量(比如数组,对象),就会共享该引用类型的数据
  • 递归必须向退出递归的条件逼近, 否则就是无限递归,出现StackOverflowError,栈溢出错误)
  • 当一个方法执行完毕,或者遇到return时,就会返回,遵守谁调用,就将结果返回给谁

7.4.2 执行机制

image-20230918142149760

  • 案例:阶乘

    1
    2
    3
    4
    5
    6
    7
    public int factorial(int n) {
    if(n == 1) {
    return 1
    } else {
    return factorial(n - 2) * n;
    }
    }

    image-20230918142950520

  • 案例:迷宫

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    public class MiGong{

    public static void main(String[] args) {
    // 先创建迷宫,二维数组表示。规定0表示可以走,1表示障碍物
    int[][] map = new int[8][7];
    for(int i = 0; i < 7; i++) {
    map[0][i] = 1;
    map[7][i] = 1;
    }
    for(int i = 0; i < 8; i++) {
    map[i][0] = 1;
    map[i][6] = 1;
    }
    map[3][1]= 1;
    map[3][2] = 1;
    map[2][2] = 1;

    System.out.println("========当前地图情况========");
    for(int i = 0; i < map.length; i++) {
    for(int j = 0; j < map[i].length; j++) {
    System.out.print(map[i][j] + " ");
    }
    System.out.println();
    }

    // 定义findWay给老鼠找路
    T t = new T();
    t.findWay(map, 1, 1);

    System.out.println("========找路的情况========");
    for(int i = 0; i < map.length; i++) {
    for(int j = 0; j < map[i].length; j++) {
    System.out.print(map[i][j] + " ");
    }
    System.out.println();
    }
    }
    }

    class T {
    //1. findWay 方法就是专门来找出迷宫的路径
    //2. 如果找到,就返回 true ,否则返回 false
    //3. map 就是二维数组,即表示迷宫
    //4. i,j 就是老鼠的位置,初始化的位置为(1,1)
    //5. 因为我们是递归的找路,所以我先规定 map 数组的各个值的含义
    // 0 表示没有走过可以走(无障碍) 1 表示障碍物 2 表示走过且可以走通 3 表示走过,但是走不通是死路
    //6. 当 map[6][5] =2 就说明找到通路,就可以结束,否则就继续找.
    //7. 先确定老鼠找路策略 下->右->上->左

    public boolean findWay(int[][] map, int i, int j) {
    if(map[6][5] == 2) { //找到出口
    return true;
    } else {
    if(map[i][j] == 0) { //当前位置没有障碍物可以走
    // 先假设这个位置可以走通
    map[i][j] = 2;
    // 判断下一个位置(4个方向是否走得通)
    if(findWay(map, i + 1, j)) { //下
    return true;
    } else if(findWay(map, i, j + 1)){ //右
    return true;
    } else if(findWay(map, i - 1, j)){ //上
    return true;
    } else if(findWay(map, i, j - 1)){ //左
    return true;
    } else { //当前位置四个方向都试过了,走不通
    map[i][j] = 3;
    return false;
    }
    } else { //map[i][j]=1,2,3 当前位置是障碍物走不通,或者已经走过(2,3)
    return false;
    }
    }
    }

    }

image-20230919100158512

7.5 方法重载 Overload

  • java 中允许同一个类中,多个同名方法的存在,但要求**形参列表不一致**!

  • 减轻了起名和记名的麻烦

  • 注意事项

    • 方法名必须相同
    • 形参列表必须不同(形参类型、个数、顺序至少有一样不同,形参名无要求
    • 返回类型无要求
  • 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class OverLoad{

    public static void main(String[] args) {
    Methods md = new Methods();
    System.out.println(md.max(10, 20));
    System.out.println(md.max(10.2, 20.5));
    System.out.println(md.max(33.0, 20.1, 44.0));
    System.out.println(md.max(33.0, 20.1, 44));

    }
    }

    class Methods{
    int max(int a, int b) {
    return a > b ? a : b;
    }

    double max(double a, double b) {
    return a > b ? a : b;
    }

    double max(double a, double b, double c) {
    return (a > b ? a : b) > c ? (a > b ? a : b) : c;
    }

    double max(double a, double b, int c) {
    return (a > b ? a : b) > c ? (a > b ? a : b) : c;
    }
    }

7.6 可变参数

  • java 允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。就可以通过可变参数实现

  • 访问修饰符 返回类型 方法名(数据类型… 形参名) { }

  • 注意事项

    • 可变参数的实参个数可以为0或者任意多个
    • 可变参数的实参可以为数组
    • 可变参数的本质就是数组
    • 可变参数可以和普通类型的参数一起放在形参列表,单必须保证可变参数在最后
    • 一个新参列表只能出现一个可变参数
  • 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class VarParameterExercise {

    public static void main(String[] args) {
    HspMethod hm = new HspMethod();
    System.out.println(hm.showScore("milan" , 90.1, 80.0 ));
    System.out.println(hm.showScore("terry" , 90.1, 80.0,10,30.5,70 ));
    }
    }

    class HspMethod {
    /*
    有三个方法,分别实现返回姓名和两门课成绩(总分),
    返回姓名和三门课成绩(总分),返回姓名和五门课成绩(总分)。
    封装成一个可变参数的方法
    */

    //分析 1. 方法名 showScore 2. 形参(String ,double... ) 3. 返回 String
    public String showScore(String name ,double... scores ) {
    double totalScore = 0;
    for(int i = 0; i < scores.length; i++) {
    totalScore += scores[i];
    }
    return name + " 有 " +scores.length + "门课的成绩总分为=" + totalScore;
    }
    }

7.7 作用域

  • 基本作用

    • 全局变量:也就是属性(成员变量),作用域在整个类体中

    • 局部变量:一般指在成员方法中定义的变量

    • 全局变量可以不赋值直接使用,因为有默认值。局部变量必须先赋值再使用,因为没有默认值。

  • 注意事项

    1. 属性和局部变量可以重名,访问时遵循就近原则。

    2. 在同一个作用域中,比如在同一个成员方法中, 两个局部变量,不能重名。

    3. 属性生命周期较长,伴随着对象的创建而创建,伴随着对象的销毁而销毁。局部变量,生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的结束而销毁。即在一次方法调用过程中。

    4. 作用域范围不同:全局变量/属性可以被本类使用,或其他类使用(通过对象调用)。局部变量只能在本类中对应的方法中使用

    5. 修饰符不同:全局变量/属性可以加修饰符局部变量不可以加修饰符

7.8 构造方法

7.8.1 基本介绍

  • 构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化(调用时对象已经创建)。

7.8.2 注意事项

  1. 构造器名与类名相同
  2. 构造器没有返回值,也不能加void
  3. 一个类中可以定义多个构造器,即构造器重载
  4. 构造器是完成对对象的初始化,并不是创建对象
  5. 在new创建对象时,系统自动调用该类的构造方法。
  6. 如果程序员没有定义构造器,系统会自动生成一个默认无参的构造器(默认构造器),如Dog(){},使用javap指令反编译看看。javap dog.class
  7. **一旦定义了自己的构造器,默认的构造器就被覆盖了,就不能再使用默认的无参构造器,除非显示的定义一下,即Dog(){}**(重要!)
  • 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Person {
    String name;
    int age;

    //构造器
    public Person() {
    age = 18;
    }

    public Person(String pName, int pAge) {
    name = pName;
    age = pAge;
    }
    }

7.9 对象创建的流程分析(重要!)

  • 案例

    image-20230919151736302

  • 流程

    1. 先加载Person类信息到方法区
    2. 在堆中开辟空间,进行默认初始化
    3. 然后运行构造方法,给属性赋值(基本数据类型保存在堆中,引用类型保存在常量词)

7.10 this关键字

  • java虚拟机会给每个对象分配一个this,表示当前对象的引用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Person {
    String name;
    int age;

    //问题:构造器中的name和age是局部变量
    public Person(String name, int age) {
    name = Name;
    age = Age;
    }
    //解决:用this关键字指明当前对象的属性
    public Person(String Name, int Age) {
    this.name = Name;
    this.age = Age;
    }
    }

    7.10.1 内存示意图

    image-20230920092051740

  • 使用**this.hashcode()**可以输出对象的唯一标识

7.10.2 this注意事项

  1. this 关键字可以用来访问本类的属性、方法、构造器
  2. this 用于区分当前类的属性和局部变量
  3. 访问成员方法的语法:this.方法名(参数列表);
  4. 访问构造器语法this(参数列表); 注意只能在构造器中使用(即只能在构造器中访问另外一个构造器, 必须放在第一条语句)
  5. this 不能在类定义的外部使用,只能在类定义的方法中使用。

第八章 面向对象编程(中级)

8.1 开发工具IDEA

8.1.1 目录结构

  • src:存放java源码文件
  • out:存放编译后的class文件

8.1.2 常用快捷键(重要!)

  1. 删除当前行, 默认是 ctrl + Y 自己配置 ctrl + d
  2. 复制当前行, 自己配置 ctrl + alt + 向下光标
  3. 补全代码 alt + /
  4. 添加注释和取消注释 ctrl + /
  5. 导入该行需要的类 先配置 auto import , 然后使用 alt+enter 即可
  6. 快速格式化代码 ctrl + alt + L
  7. 快速运行程序,默认是shift + F10,自己定义 alt + R
  8. 生成构造器等 alt + insert [提高开发效率]
  9. 查看一个类的层级关系 ctrl + H [学习继承后,非常有用]
  10. 将光标放在一个方法上,输入 ctrl + B , 可以定位到方法 [学继承后,非常有用]
  11. 自动的分配变量名 , 通过在后面加.var [老师最喜欢的]
  12. 另起一行编写代码,**shift + enter**
  13. 将当前行代码向上/下移动,ctrl + shift + ↑ / ↓

8.1.3 自定义模板(提高开发速度)

  • 设置:file -> settings -> editor-> Live templates ->
  • 如fori , sout等,多用多查孰能生巧

8.2 包

8.4.1 作用

  • 区分相同名字的类
  • 当类很多时,便于管理类
  • 控制访问范围

8.4.2 基本语法

  • package com.durango.xxx

  • package + 包名

8.4.3 包的本质

​ 包的本质实际上就是创建不同的文件夹/目录来保存类文件

image-20230922101528833

8.4.4 包的命名

  • 命名规则
    • 只能包含数字、字母、下划线、小圆点
    • 不能用数字开头 ,如demo.12a
    • 不能是关键字或保留字,如demo.class.exec
  • 命名规范
  • 一般是 小写字母+小圆点
  • 包名一般是 com.公司名.项目名.业务模块名
  • 如,com.sina.crm.user com.sina.crm.order com.sina.vrm.utils

8.4.5 常用的包

  1. java.lang.* :lang 包是基本包,默认引入,不需要再引入.
  2. java.util.* :util 包,系统提供的工具包, 工具类,使用 Scanner
  3. java.net.* :网络包,网络开发
  4. java.awt.* :是做 java 界面的开发,GUI

8.4.6 包的导入

  • 两种方式

    1
    2
    3
    import java.util.Scanner;	//推荐,用到哪个导入哪个

    import java.util.*
  • 使用系统提供Arrays完成数组排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import java.util.Arrays;

    public class Import01 {
    public static void main(String[] args) {
    //使用系统提供 Arrays 完成 数组排序
    int[] arr = {-1, 20, 2, 13, 3};

    Arrays.sort(arr);

    //输出排序结果
    for (int i = 0; i < arr.length ; i++) {
    System.out.print(arr[i] + "\t");
    }
    }

8.4.9 注意事项

  • package的作用是声明当前类所在的包,需要放在类的最上面,一个类中最多只有一句package

  • import指令位置放在package的下面,在类定义前面,可以有多句且没有顺序要求

8.5 访问修饰符 Modifier

8.5.1 基本介绍(必背)

  1. 公开级别:用 public 修饰,对外公开

  2. 受保护级别:用 protected 修饰,对子类和同一个包中的类公开

  3. 默认级别:没有修饰符号,向同一个包的类公开.

  4. 私有级别:用 private 修饰,只有类本身可以访问,不对外公开.

访问级别 访问修饰符 同类 同包 子类 不同包
公开 public
受保护 protected ×
默认 无修饰符 × ×
私有 private × × ×
访问权限 可见性 访问性
public 所有类、接口、方法、变量都可见 所有类、接口、方法、变量都可访问
protected 当前包中的所有类、接口、方法、变量都可见,其他包中的子类也可见 当前包中的所有类、接口、方法、变量都可访问,其他包中的子类也可访问
default 当前包中的所有类、接口、方法、变量都可见 当前包中的所有类、接口、方法、变量都可访问
private 当前类中的所有方法和变量都可见 当前类中的所有方法和变量都可访问

8.5.2 注意事项

  • 修饰符可以用来修饰类中的属性、成员方法以及类
  • 只有默认的和public才能修饰类!,并且遵循上述访问权限的特点。

8.6 封装 Encapsulation

8.6.1 封装介绍

​ 封装(encapsulation)就是把抽象出的数据[属性]和对数据的操作[方法]封装在一起,数据被保护在内部,程序的其它部分(比如不同类,不同包)只有通过被授权的操作[方法],才能对数据进行操作

8.6.2 作用

  • 隐藏实现细节
  • 可以对数据进行验证,保证安全合理

8.6.3 封装实现步骤

  1. 将属性进行私有化private

  2. 提供一个公共的public set方法 ,用于对属性判断和赋值

    1
    2
    3
    4
    public void setXxx(类型 参数名) {
    // 加入数据验证的逻辑
    属性 = 参数名;
    }
  3. 提供一个公共的public get方法 ,用于获取属性的值

    1
    2
    3
    puclic 类型 getXxx() {
    return 属性;
    }

8.6.4 快速入门案例

需求:新建一个Person类,不能随便查看人的年龄、工资等隐私,并对设置的年龄、姓名进行合理验证。年龄合理则设置,否则给默认值。年龄必须在1-120。name长度在2-6个字符之间

alt + insert:编写set和get方法的快捷键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Person {
public String name;
private int age;
private double salary;

public Person(String name, int age, double salary) {
setName(name);
setAge(age);
setSalary(salary);
}

// 手写get,set太慢。使用快捷键 alt + insert
public void setName(String name) {
// 增加对数据的校验
if (name.length() >= 2 && name.length() <= 6) {
this.name = name;
} else {
System.out.println("输入的名字长度不对,必须为2-6个字符,已设置默认名字无名");
this.name = "无名";
}
}

public void setAge(int age) {
// 增加数据的合理性判断
if (age >= 1 && age <= 120) {
this.age = age;
} else {
System.out.println("输入年龄范围不对,必须为1-20,已设置默认年龄18");
this.age = 18;
}
}

public void setSalary(double salary) {
this.salary = salary;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

public double getSalary() {
// 可以增加对当前对象的权限判断
return salary;
}

public void showInfo() {
System.out.println("信息为 name=" + this.name + " age=" + this.age + " salary=" + this.salary);
}
}

8.7 继承 Extends

8.7.1 继承介绍

  • 继承可以解决代码复用, 让我们的编程更加靠近人类思维. 当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类, 在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过 extends 来声明继承父类即可。画出继承的示意图。

    image-20230923164603231

  • 基本语法

    class 子类 extends 父类 { }

    • 子类会自动拥有父类定义的属性和方法
    • 父类又叫超类、基类,子类又叫派生类
  • **ctrl + H**:可以看到类的继承关系

8.7.2 继承细节

  1. 子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问, 但是私有属性和方法不能在子类直接访问要通过父类提供公共的方法去访问
  1. 子类必须调用父类的构造器, 完成父类的初始化

  2. 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器如果父类没有提供无参构造器(无参构造器被覆盖),则必须在子类的构造器中用 super 去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过(怎么理解。) [举例说明]。若父类提供了无参构造器,super写不写都行。

  3. 如果希望指定去调用父类的某个构造器,则显式的调用一下 : super(参数列表)

  4. super 在使用时,必须放在构造器第一行(super 只能在构造器中使用)

  5. super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器。如果写了this(),隐式的super()将不会被该构造器调用而是会在this()中调用super()

  6. java 所有类都是 Object 类的子类, Object 是所有类的基类. ctrl + H可以看到类的继承关系

  7. 父类构造器的调用不限于直接父类!将一直往上追溯直到 Object 类(顶级父类)

  8. 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制。思考:如何让 A 类继承 B 类和 C 类? 【A 继承 B, B 继承 C】

  9. 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系

8.7.3 继承本质分析(重要!)

image-20230924111103504

  1. 创建Son对象
  2. 在方法区中加载Son类及其父类信息
  3. 在堆中给GranPa开辟空间,并初始化其属性,引用数据类型时存放在方法区的常量池中
  4. 在堆中给Father开辟空间,并初始化其属性,基本数据类型直接存放在堆中
  5. 最后给Son开辟空间,并初始化其属性
  6. 这时请大家注意,要按照查找关系来返回信息
    1. 首先看子类是否有该属性,如果子类有这个属性,并且可以访问,则返回信息
    2. 如果子类没有这个属性,就看父类有没有这个属性(如果父类有该属性,并且可以访问,就返回信息..)
    3. 如果父类没有就按照2的规则,继续找上级父类,直到 Object…

8.7.4 本质分析案例

  • Exercise01

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package com.extend;

    public class ExtendsExercise01 {
    public static void main(String[] args) {
    B b = new B();
    }
    }

    class A {
    public A() {
    System.out.println("a");
    }

    public A(String name) {
    System.out.println("a name");
    }
    }

    class B extends A {
    public B() {
    this("abc"); // 不会调用super()
    System.out.println("b");
    }

    public B(String name) { //会调用super()
    System.out.println("b name");
    }
    }
    // 结果
    // a
    // b name
    //
  • Exercise02

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package com.extend;

    public class ExtendsExercise02 {
    public static void main(String[] args) {
    C2 c = new C2();
    }
    }

    class A2 {//A2 类
    public A2() {
    System.out.println("我是 A2 类");
    }
    }
    class B2 extends A2 { //B2 类,继承 A2 类 //main 方法中: C2 c =new C2(); 输出么内容? 3min
    public B2() {
    System.out.println("我是 B2 类的无参构造");
    }
    public B2(String name) {
    System.out.println(name + "我是 B2 类的有参构造");
    }
    }
    class C2 extends B2 { //C2 类,继承 B2 类
    public C2() {
    this("hello");
    System.out.println("我是 c 类的无参构造");
    }
    public C2(String name) {
    super("hahah");
    System.out.println("我是 c 类的有参构造");
    }
    }
    // 我是 A2 类
    // hahah我是 B2 类的有参构造
    // 我是 c 类的有参构造
    // 我是 c 类的无参构造

8.8 super关键字

8.8.1 基本介绍

  • super 代表父类的引用,用于访问父类的属性、方法、构造器
  • 不能访问父类私有的private属性、方法
    • 访问父类属性:super.属性名
    • 访问父类方法:super.方法名(参数列表)
    • 访问父类构造器:super(参数列表) 只能放在构造器第一句,只能出现一句
    • 如果父类中属性设置为private,则不能通过super访问。可以在父类中设置公共的get方法,然后在子类中调用get方法获取父类私有属性。

8.8.2 作用和细节

  • 调用父类的构造器的好处(分工明确, 父类属性由父类初始化,子类的属性由子类初始)
  • 当子类中有和父类中的成员**(属性和方法)重名时**,为了访问父类的成员,必须通过super。如果没有重名,使用super、this、直接访问是一样的效果!
  • super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员; 如果多个基类(上级类)中都有同名的成员,使用super访问遵循就近原则。A->B->C,当然也需要遵守访问权限的相关规则

8.8.3 super和this的比较

区别点 this super
访问属性 访问本类中的属性,如果本类中没有此属性,则从父类中继续查找 从父类开始查找属性
调用方法 访问本类中的方法,如果本类中没有此方法,则从父类中继续查找 从父类开始查找方法
调用构造器 调用本类构造器,必须放在构造器的首行 调用父类的构造器,必须放在子类构造器的首行
特殊 对当前对象的引用 对父类对象的引用

8.9 方法重写/重构/覆盖 Override

8.9.1 基本介绍

​ 方法覆盖(重写)就是子类有一个方法和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的方法

8.9.2 注意事项

  • 子类的方法的方法名称,形参列表要和父类方法的形参列表,方法名称完全一样。
  • 子类方法的返回类型和父类方法返回类型一样或者是父类返回类型的子类。比如父类返回类型是Object,子类方法返回类型是String
  • 子类方法不能缩小父类方法的访问权限。public > protected > 默认 > privat

8.9.3 重写与重载的比较

名称 发生范围 方法名 形参列表 返回类型 修饰符
重载overload 本类 必须一样 类型、个数、顺序至少一个不同 无要求 无要求
重写override 父子类 必须一样 必须一样 子类重写的方法的返回类型和父类放回类型一致,或者是其子类 子类方法不能缩小父类方法的访问权限

8.9.4 案例

  1. 编写一个 Person 类,包括属性/private(name、age),构造器、方法 say(返回自我介绍的字符串)。

  2. 编写一个 Student 类,继承 Person 类,增加 id属性/private,、score 属性/private,以及构造器,定义 say 方法(返回自我介绍的信息)。

  3. 在 main 中,分别创建 Person 和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.override_;

public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public void say() {
System.out.println("我叫" + name + ",今年" + age + "岁。");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.override_;

public class Student extends Person {
private String id;
private double score;

public Student(String name, int age, String id, double score) {
super(name, age);
this.id = id;
this.score = score;
}

@Override
public void say() {
super.say(); // 父类的name和age是私有属性,不能直接访问
System.out.println("我的id是" + id + ",考了" + score + "分。");
}
}

8.10 多态 Polymorphic

8.10.1 一个场景

image-20230925164315604

需要在Master类中重载3*3=9个feed方法,才能完成“主人给什么动物喂什么食物”的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 完成主人给小狗 喂 骨头
// public void feed(Dog dog, Bone bone) {
// System.out.println("主人" + name + "给" + dog.getName() + "吃" + bone.getName());
// }

// 完成主人给小猫 喂 鱼
// public void feed(Cat cat, Fish fish) {
// System.out.println("主人" + name + "给" + cat.getName() + "吃" + fish.getName());
// }

// 如果动物很多,食物很多,需要写很多重载的feed,不利于维护和管理
public void feed(Animal animal, Food food) {
System.out.println("主人" + name + "给" + animal.getName() + "吃" + food.getName());

}

8.10.2 基本介绍

多态:方法对象具有多种形态,建立在封装和继承基础上。

(1)方法的多态:重写和重载就体现多态

(2)对象的多态(核心)

  1. 一个对象的编译类型和运行类型可以不一致
  2. 编译类型在定义对象时,就确定了,不能改变
  3. 运行类型是可以变化的.
  4. 编译类型看定义时=号的左边;运行类型看=号的右边

image-20230925165752251

1
2
3
4
5
6
7
// animal 编译类型时Animal,运行类型是Dog
Animal animal = new Dog("大黄");
animal.cry();

// 编译类型时Animal,运行类型是Cat
animal = new Cat("小花");
animal.cry();

8.10.3 注意事项

  • 多态的前提:两个类存在继承关系

  • 多态的向上转型

    • 本质:父类的引用指向了子类的对象

    • 语法:**父类类型 引用名 = new 子类类型( );**

    • 特点:

      • 编译类型看左边,运行类型看右边

      • 可以调用父类中的所有成员(需要遵循访问权限)

      • 不能调用子类中的特有成员

      • 方法的调用看运行类型,属性的调用看编译类型。

      • 最终运行效果看子类的具体实现,即调用方法时,按照从子类开始查找方法,子类没有则查看父类。

    • Animal类有【public】cry,eat,run,sleep方法和【private】name属性,Cat类继承了Animal,重写了run方法,增加了一个catchMouse方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.poly_.detail;

    public class PolyDetail {
    public static void main(String[] args) {

    // 向上转型
    Cat animal = new Cat("小花");
    // Object obj = new Cat("小花");

    // 可以调用父类中所有成员(遵守访问权限)
    animal.cry();
    // animal.name 不能访问父类private的成员
    // annimal.catchMouse(); 不能访问子类特有的方法
    animal.eat();
    animal.run();
    animal.sleep();

    // 结果
    // 小猫叫
    // 动物吃
    // 动物跑
    // 动物睡
    }
    }
  • 多态的向下转型

    • 本质:子类的引用指向父类的引用
    • 语法:**子类类型 引用名 = (子类类型)父类引用;**
    • 特点:
      • 要求父类的引用必须指向的是当前目标类型的对象
      • 当向下转型后,可以调用子类类型中的所有成员(包括特有的)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.poly_.detail;

    public class PolyDetail {
    public static void main(String[] args) {

    // 向上转型
    Cat animal = new Cat("小花");

    // annimal.catchMouse(); 不能访问子类特有的方法

    // 向下转型
    // 需要animal原本就指向Cat对象
    // 向下转型后可以访问子类特有对象
    Cat cat = (Cat)animal;
    cat.catchMouse();

    }
    }

  • 属性没有重写之说!属性的值看编译类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class PolyDetail02 {
    public static void main(String[] args) {
    //属性没有重写之说!属性的值看编译类型
    Base base = new Sub();//向上转型
    System.out.println(base.count);// ? 看编译类型 10
    Sub sub = new Sub();
    System.out.println(sub.count);//? 20
    }
    }
    class Base { //父类
    int count = 10;//属性
    }
    class Sub extends Base {//子类
    int count = 20;//属性
    }
  • instanceOf 比较操作符,用于判断对象的**运行类型**是否为 XX 类型或 XX 类型的子类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class PolyDetail03 {
    public static void main(String[] args) {
    BB bb = new BB();
    System.out.println(bb instanceof BB);// true
    System.out.println(bb instanceof AA);// true

    //aa 编译类型 AA, 运行类型是 BB
    //BB 是 AA 子类
    AA aa = new BB();
    System.out.println(aa instanceof AA); // true
    System.out.println(aa instanceof BB); // true, 判断的是aa的运行类型
    }
    }
  • 一个题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Base {
int count = 10;
public void display(){
System.out.println(this.count);
}
}

class Sub extends Base {
int count = 20;
public void display(){
System.out.println(this.count);
}
}

class test {
public static void main(String[] args){
Sub s = new Sub();
System.out.println(s.count); // 20
s.display(); // 20

Base b = s; //向上转型
System.out.println(b == s); //True,指向同一个对象
System.out.println(b.count); //10 属性看编译类型
b.display(); // 20 方法看运行类型
}
}

8.10.4 java的动态绑定机制(非常重要!)

  • 当调用对象方法的时候,该方法会和对象的内存地址/运行类型绑定(动态绑定)

  • 当调用对象属性的时候,没有动态绑定机制,哪里声明,哪里使用

  • 第一个案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    package com.poly_;

    public class DynamicBanding {
    public static void main(String[] args) {
    A a = new B(); // 向上转型
    // 分析
    /* 调用方法时看运行类型
    a.sum()调用的是B类中的sum(),返回getI+i=20+20
    a.sum1()同理 i+20=20+20=40
    */
    System.out.println(a.sum()); // 40
    System.out.println(a.sum1()); // 40
    }
    }

    class A { //父类
    public int i = 10;

    public int sum() {
    return getI() + 10;
    }

    public int sum1() {
    return i + 10;
    }

    public int getI() {
    return i;
    }
    }

    class B extends A{ //子类
    public int i = 20;

    public int sum() {
    return getI() + 20;
    }

    public int sum1() {
    return i + 20;
    }

    public int getI() {
    return i;
    }
    }

  • 第二个案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.poly_;

    public class DynamicBanding {
    public static void main(String[] args) {
    A a = new B(); // 向上转型
    // 分析
    /* 调用方法时看运行类型(动态绑定),子类中没有该方法则向上找父类
    a的运行类型是B,B中没有sum()方法,则调用父类A中的sum()方法。在A中的sum()方法中会调用
    getI(),这个getI()也会动态绑定运行类型,即调用B中的getI()。
    最终返回(B)getI+10=20+10=30

    a的运行类型是B,B中没有sum1()方法,则调用父类A中的sum1()方法。属性i不会动态绑定
    返回(A)i+10=20
    */
    System.out.println(a.sum()); // 30
    System.out.println(a.sum1()); // 20
    }
    }

    class A { //父类
    public int i = 10;

    public int sum() {
    return getI() + 10; //运行类型是B时,会调用B类的getI()
    }

    public int sum1() {
    return i + 10;
    }

    public int getI() {
    return i;
    }
    }

    class B extends A{ //子类
    public int i = 20;

    public int getI() {
    return i;
    }
    }

  • 第三个案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package com.poly_;

    public class DynamicBanding {
    public static void main(String[] args) {
    A a = new B(); // 向上转型
    // 分析
    /* 调用方法时看运行类型
    a.sum()调用的是B类中的sum(),sum()会调用getI(),此时B类中没有getI(),
    会向上查找调用父类A的getI()。属性没有动态绑定机制,A的getI()中返回的是A的i
    最终结果 (A)getI+20 = (A)i+20 = 10+20 = 30

    a.sum1()返回(B)i+20 = 40
    */
    System.out.println(a.sum()); // 30
    System.out.println(a.sum1()); // 40
    }
    }

    class A { //父类
    public int i = 10;

    public int sum() {
    return getI() + 10;
    }

    public int sum1() {
    return i + 10;
    }

    public int getI() {
    return i;
    }
    }

    class B extends A{ //子类
    public int i = 20;

    public int sum() {
    return getI() + 20;
    }

    public int sum1() {
    return i + 20;
    }
    }

8.11 多态的应用

8.11.1 多态数组

  • 数组的定义类型为父类类型,里面保存的实际元素类型为子类类型

  • 案例:

    • Person类:name, age, say();
    • Student类:继承Person:score, 重写say(),特有study();
    • Teacher类:继承Person,salary,重写say(),特有teach()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.poly_.polyarr;

public class PolyArr {
public static void main(String[] args) {
// 多态数组
Person[] persons = new Person[5];
persons[0] = new Person("Jack", 20);
persons[1] = new Student("Mary", 18, 100);
persons[2] = new Student("Smith", 19, 98);
persons[3] = new Teacher("Scott", 30, 2000.0);
persons[4] = new Teacher("King", 50, 40000.0);

for (int i = 0; i < persons.length; i++) {
//persons[i]的编译类型时Person,运行类型根据实际情况由jvm判断
System.out.println(persons[i].say()); //动态绑定机制
// persons[i].teach();
// persons[i].study() //向上转型无法调用子类特有方法
if(persons[i] instanceof Student){ // 判断persons[i]的运行类型是不是Student
((Student)persons[i]).study(); // 向下转型才能调用子类特有的方法
} else if(persons[i] instanceof Teacher){ // 判断persons[i]的运行类型是不是Teacher
((Teacher)persons[i]).teach();

}
}
}
}

8.11.2 多态参数

  • 方法定义的形参类型为父类类型,实参类型为子类类型。
  • 案例
    • Employee类:name, salary, getAnnual( )
    • Worker类:特有work()
    • Manager类:bonus, 重写getAnnual( ), 特有manage( )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.poly_.polyparameter;

public class PolyParameter {
public static void main(String[] args) {
Worker tom = new Worker("tom", 2500);
Manager willian = new Manager("willian", 5000, 20000);

PolyParameter polyParameter = new PolyParameter();
polyParameter.showEmpAnnual(tom); //多态参数
polyParameter.showEmpAnnual(willian);
polyParameter.testWork(tom); //多态参数
polyParameter.testWork(willian);
}

public void showEmpAnnual(Employee e) {
System.out.println(e.getAnnual());
}

public void testWork(Employee e) {
if (e instanceof Worker) {
((Worker) e).work();
} else if (e instanceof Manager) {
((Manager) e).manage();
} else {
System.out.println("不做处理");
}
}
}

8.12 Object类详解

8.12.1 equals方法

  • == 比较运算符
    • 如果判断基本数据类型,判断的是是否相等
    • 如果判断引用数据类型,判断的是地址是否相等
  • equals方法

    • equals: 是Object类中的方法,只能判断引用类型,默认判断的是地址是否相等子类中往往重写该方法,用于判断内容(值)是否相等。比如Integer,String【看看String 和 Integer的equals 源代码】

    • Object类中的equals

      1
      2
      3
      public boolean equals(Object obj) {
      return (this == ojb); /默认比较两个引用地址是否相同(指向同一个对象)
      }
    • String中重写的equals

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      public boolean equals(Object anObject) {
      if (this == anObject) { // 同一个对象
      return true;
      }
      if (anObject instanceof String) { // 判断类型
      String anotherString = (String)anObject; // 向下转型
      int n = value.length;
      if (n == anotherString.value.length) { // 判断长度
      char v1[] = value;
      char v2[] = anotherString.value;
      int i = 0;
      while (n-- != 0) { // 比较每一个字符
      if (v1[i] != v2[i])
      return false;
      i++;
      }
      return true;
      }
      }
      return false;
      }
    • Integer中重写的equals

      1
      2
      3
      4
      5
      6
      public boolean equals(Object obj) {
      if (obj instanceof Integer) {
      return value == ((Integer)obj).intValue();
      }
      return false;
      }
  • 重写equals方法

    • 重写Person类中的equals方法,当两个Person类的name, age, gender相同时,就返回true
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    package com.object_;

    public class Equals02 {
    public static void main(String[] args) {
    Person person = new Person("jack", 10, '男');
    Person person1 = new Person("jack", 10, '男');
    System.out.println(person.equals(person1));
    }
    }

    class Person {
    private String name;
    private int age;
    private char gender;

    public Person(String name, int age, char gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
    }

    //重写equals(参考String中的equals)
    @Override
    public boolean equals(Object obj) {
    if (this == obj){ //如果是同一个对象直接返回true
    return true;
    }
    if (obj instanceof Person) { //判断(运行)类型,是Person类才(能)比较
    // 进行向下转型 (为什么?obj的运行类型虽然判断为Person类,但是
    // 编译类型是Object,不能访问子类Person中特有的属性方法,需要向下转型)
    Person p = (Person)obj;
    return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
    }
    return false;
    }
    }

8.12.2 hashcode方法

  • 6个结论
    • 提高具有哈希结构的容器的效率!如java.util.Hashtable中提供的哈希表
    • 两个引用,如果指向的是同一个对象,则哈希值肯定是一样的!
    • 两个引用,如果指向的是不同对象,则哈希值是不一样的
    • 哈希值一般是通过该对象的内部地址转换成一个整数来实现的,不能完全将哈希值等价于地址。
  • 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.object_;

    public class HashCode_ {
    public static void main(String[] args) {
    AA aa = new AA();
    AA aa1 = new AA();
    AA aa2 = aa;

    System.out.println(aa.hashCode());
    System.out.println(aa1.hashCode());
    System.out.println(aa2.hashCode());
    }
    }

    class AA {
    }

    // 1163157884
    // 1956725890
    // 1163157884

8.12.2 toString方法

  • 默认返回:**全类名+@+哈希值的十六进制**,子类往往重写 toString 方法,用于返回对象的属性信息

  • 查看 Object 的 toString 方法

    1
    2
    3
    4
    5
    // (1)getClass().getName() 类的全类名(包名+类名 )
    // (2)Integer.toHexString(hashCode()) 将对象的 hashCode 值转成 16 进制字符串
    public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
  • 重写toString方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Monster{
    private String name;
    private String job;
    private double sal;

    public Monster(String name, String job, double sal) {
    this.name = name;
    this.job = job;
    this.sal = sal;
    }

    // 重写toString,快捷键alt+insert
    @Override
    public String toString() {
    return "Monster{" +
    "name='" + name + '\'' +
    ", job='" + job + '\'' +
    ", sal=" + sal +
    '}';
    }
    }
  • 当直接输出一个对象时,toString 方法会被默认的调用, 比如 System.out.println(monster); 就会默认调用

    monster.toString()

8.12.3 finalize 方法

  • 当对象被回收时,系统会自动调用该对象的finalize方法。子类可以重写该方法,做一些释放资源的操作。
  • 什么时候回收:当某个对象没有任何引用时,则jvm就认为这个对象是一个垃圾对象,就会用垃圾回收机制来摧毁该对象,在摧毁该对象之前,会调用finalize方法。
  • 垃圾回收机制的调用,是由系统来决定的(即有自己的GC算法),也可以通过System.gc()主动触发垃圾回收机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.object_;

public class Finalize_ {
public static void main(String[] args) {
Car bmw = new Car("宝马");
bmw = null;
System.gc(); // 主动调用垃圾回收器
//这时候car对象没有任何引用,会被垃圾回收器回收(摧毁)
//在销毁对象前,会调用该对象的 finalize 方法
//程序员就可以在 finalize 中,写自己的业务逻辑代码(比如释放资源:数据库连接,或者打开文件..)
//如果程序员不重写 finalize,那么就会调用 Object 类的 finalize, 即默认处理
}
}

class Car {
private String name;

public Car(String name) {
this.name = name;
}

//重写finalize
@Override
protected void finalize() throws Throwable {
System.out.println("我们销毁了" + name + "汽车");
System.out.println("释放了一些资源");
}
}

8.13 断点调试 debug

8.13.1 介绍

  • —步一步的看源码执行的过程,从而发现错误所在。
  • 重要提示: 在断点调试过程中,是运行状态,是以对象的运行类型来执行的
  • 断点调试是指在程序的某一行设置一个断点,调试时,程序运行到这一行时就会停住,然后你可以一步一步往下调试,调试过程中可以看各个变量当前的值,出错的话,调试到出错的代码行即显示错误,停下。进行分析从而找到这个Bug
  • 断点调试时程序员必须掌握的技能
  • 断点调试也能帮助我们查看java底层源代码的执行过程,提高程序员java水平

8.13.2 快捷键

  • F7跳入:跳入方法内
  • F8跳过:逐行执行代码
  • shift + F8:跳出方法
  • F9:resume执行到下一个断电

image-20231009104027696

8.13.3 案例

  • 使用断点调试,追踪对象创建的过程
  • 使用断点调试,查看动态绑定机制如何工作

8.14 案例:零钱通

使用 Java 开发 零钱通项目 , 可以完成收益入账,消费,查看明细,退出系统等功能.

1
2
3
4
5
1) 先完成显示菜单,并可以选择
2) 完成零钱通明细.
3) 完成收益入账
4) 消费
5) 退出

第九章 项目:房屋出租系统

9.1 项目需求

  • 实现基于文本界面的《房屋出租软件》
  • 能够实现对房屋信息的添加、修改和删除(用数组实现),并能够打印房屋明细表

9.2 界面设计

  • 主菜单

    image-20231011095214080

  • 新增房屋

    image-20231011095238934

  • 查找房屋image-20231011095306640

  • 删除房屋

    image-20231011095332012

  • 修改房屋

    image-20231011095347017

  • 房屋列表

    image-20231011095358271

  • 退出系统

    image-20231011095413116

9.3 系统设计

​ 项目设计-程序框架图 (分层模式=>当软件比较复杂,需要模式管理)

image-20231011100426182

9.4 系统实现

9.4.1 了解和使用工具库

​ 在实际开发中,公司都会提供相应的工具类和开发库,可以提高开发效率,程序员也需要能够看懂别人写的代码,并能够正确的调用。(造轮子的事情别人已经做好了)

  • 了解 Utility 类的使用

  • 测试 Utility 类

  • 当作API使用,会用就行,有余力的可以读懂源码

9.4.2 完成House类

编号 房主 电话 地址 月租 状态(未出租/已出租)

9.4.3 显示主菜单和完成退出软件功能

实现功能的三部曲 [明确完成功能->思路分析->代码实现]

  • 功能说明: 用户打开软件,可以看到主菜单,可以退出软件.

  • 思路分析: 在 HouseView.java 中,编写一个方法 mainMenu,显示菜单.

9.4.4 显示房屋列表的功能

  • 功能说明: 用户输入对应的key,可以显示房屋信息列表

  • 思路分析:

    • 在 HouseView.java 中,编写一个方法 listHouses显示房屋信息列表.
    • 在HouseService.java中,编写一个方法list可以返回所有的房屋信息

9.4.5 添加房屋信息的功能

  • 功能说明:显示添加房屋信息的界面,用户输入相关信息,可以添加房屋信息到系统,新增的房屋id按照自增长来定

  • 思路分析

    • 在 HouseView.java 中,编写一个方法 addHouse接收用户输入
    • 在HouseService.java中,编写一个方法add可以添加新增的房屋信息到系统。需要判断数组容量是否满了

9.4.6 删除房屋信息的功能

  • 功能说明:
    • 显示删除房屋信息的界面
    • 用户输入房源编号(-1退出)
    • Y确认删除,N 取消
  • 思路分析
    • 在 HouseView.java 中,编写一个方法 delHouse接收用户输入id
    • 在HouseService.java中,编写一个方法del可以根据房源id从系统中删除相关信息。需要判断该id是否存在

9.4.7 退出确认功能

  • 功能说明:显示退出界面,根据用户输入确认是否退出

9.4.8 查找房屋信息功能

  • 功能说明:根据用户输入的id显示对应的房屋信息
  • 思路分析:
    • 在 HouseView.java 中,编写一个方法 findHouse接收用户输入id,并显示房屋信息
    • 在HouseService.java中,编写一个方法find可以根据房源id从系统中查找相关信息。

9.4.9 修改房屋信息功能

  • 功能说明:根据用户输入的id显示原始房屋信息,并可以修改
  • 思路分析:
    • 在 HouseView.java 中,编写一个方法 updateHouse接收用户输入id,并显示原始房屋信息。接收新的房屋信息。
    • 在HouseService.java中,编写一个方法update可以根据用户输入的新信息进行更新

— 第二阶段 —

第十章 面向对象编程(高级)

10.1 类变量和类方法(static)

10.1.1 类变量快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.durango.static_;

public class ChildGame {
public static void main(String[] args) {
Child child1 = new Child("aaa");
child1.join();
child1.count++;
Child child2 = new Child("bbb");
child2.join();
child1.count++;
Child child3 = new Child("ccc");
child3.join();
child1.count++;

// 类变量可以通过类名访问
System.out.println("共有" + Child.count + "个小孩加入了游戏");
System.out.println("child1.count=" + child1.count); //3
System.out.println("child2.count=" + child2.count); //3
System.out.println("child3.count=" + child3.count); //3
}
}

class Child {
private String name;
// 类变量(静态变量),被所有实例所共享
public static int count = 0;

public Child(String name) {
this.name = name;
}

public void join() {
System.out.println("有一个小孩加入");
}
}

10.1.2 类变量内存布局

​ 有不同说法:在堆中(jdk8以后); 在方法区的静态域中。与jdk版本有关

10.2.3 类变量基本介绍

  1. 概念

    类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。

  2. 定义

    1
    2
    访问修饰符 static 数据类型 变量名; 【推荐】
    static 访问修饰符 数据类型 变量名;
  3. 访问

    1
    2
    类名.变量名; 【推荐】
    对象名.变量名;

10.2.4 类变量注意事项

  1. 什么时候需要用类变量

    当我们需要让某个类的**所有对象都共享一个变量时**,就可以考虑使用类变量(静态变量),比如:定义学生类,统计所有学生共交多少钱。Student (name, static fee)
    
  2. 类变量与实例变量(普通属性)区别类变量是该类的所有对象共享的,而实例变量是每个对象独享的。类变量是该类的所有对象共享的,而实例变量是每个对象独享的.

  3. 加上static称为类变量或静态变量,否则称为实例变量/普通变量/非静态变量

  4. 类变量可以通过类名.变量名或者对象名.变量名来访问,但java设计者推荐类名.类变量名我们使用类名.类变量名方式访问。【前提是满足访问修饰符的访问权限和范围】

  5. 实例变量不能通过类名.类变量名方式访问。

  6. 类变量是在类加载时就初始化了,也就是说,即使你没有创建对象,只要类加载了,就可以使用类变量了。

  7. 类变量的生命周期是随类的加载开始,随着类消亡而销毁

10.2.5 类方法基本介绍

  1. 概念

    类方法也叫静态方法,是该类的所有对象共享的方法

  2. 定义

    1
    2
    访问修饰符 static 返回数据类型 方法名; 【推荐】
    static 访问修饰符 返回数据类型 方法名;
  3. 访问

    1
    2
    类名.方法名; 【推荐】
    对象名.方法名;
  4. 快速入门

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.durango.static_;

    public class StaticMethod {
    public static void main(String[] args) {
    Stu t1 = new Stu("tom");
    Stu t2 = new Stu("tom1");
    Stu t3 = new Stu("tom2");
    t1.payFee(100);
    t2.payFee(200);
    t3.payFee(300);

    Stu.showFee();
    }
    }

    class Stu {
    private String name;
    // 静态变量累计学生交的学费
    private static double fee = 0;

    public Stu(String name) {
    this.name = name;
    }

    // 静态方法
    public static void payFee(double fee) {
    Stu.fee += fee; //累计学费
    }

    public static void showFee() {
    System.out.println("总学费=" + Stu.fee);
    }
    }

10.2.5 类方法使用场景

如果我们希望不创建实例,也可以调用某个方法时(当做工具来使用)

image-20231012103815736

10.2.5 类方法注意事项

  1. 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区。类方法中无this的参数,普通方法中隐含着this的参数
  2. 类方法可以通过类名调用,也可以通过对象名调用。
  3. 普通方法和对象有关,需要通过对象名调用,比如对象名.方法名(参数),不能通过类名调用。
  4. 类方法中不允许使用和对象有关的关键字,比如this和super。普通方法(成员方法)可以。
  5. **类方法(静态方法)中只能访问静态成员(变量和方法)**。
  6. 普通方法,既可以访问非静态成员,也可以访问静态成员

10.2 main方法

10.2.1 深入理解main方法

image-20231012105301547

10.2.2 特别提示

  • 在 main()方法中,我们可以直接调用 main 方法所在类的静态方法或静态属性
  • 但是,不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.durango.main_;

import sun.applet.Main;

public class Main01 {

//静态变量
private static String name = "Durango";

// 非静态变量
private String name1 = "Jack";

public static void main(String[] args) {
// 1. 静态方法可以直接访问本类的静态成员
System.out.println(name);
hi();

// 2. 静态方法不可以直接访问本类的非静态成员
// System.out.println(name1);
// hi1();

// 静态方法中要访问本类的静态成员,需要先创建本类对象,再调用
Main01 main01 = new Main01();
System.out.println(main01.name1);
main01.hi1();
}

// 静态方法
public static void hi() {
System.out.println(name);
}
// 非静态方法
public void hi1() {
System.out.println(name1);
}
}

10.2.2 在IDEA中给Main()传参

image-20231012142744360

10.3 代码块

10.3.1 基本介绍

  • 代码化块又称为初始化块,属于类中的成员[即是类的一部分,类似于方法,将逻辑语句封装在方法体中,通过{ }包围起来
  • 但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用。

10.3.2 基本语法

  • 语法:【修饰符】{ 代码块内容 };
  • 【修饰符】可选项,写的话只能写static。分为静态代码块和普通代码块。“;”可以省略

10.3.3 使用场景和好处

  • 相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作
  • 场景: 如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的重用性。代码块调用的顺序优先于构造器

10.3.4 注意事项(重要!!)

  • 类加载时(3种情况)执行静态代码块,创建类对象时执行普通代码块

    image-20231012150553113

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.durango.codeblock_;

public class CodeBlockDetail01 {
public static void main(String[] args) {
// 类加载时会执行静态代码块,之后执行一次
// 类加载的3种情况
// 1. 创建类对象时
AA aa = new AA();

// 2. 创建子类对象时,父类信息会先被加载
BB bb = new BB();

// 3. 使用类的静态成员时,也会加载类(其父类信息也会被加载)
System.out.println(CC.n1);

// 普通代码块(只有)在创建类对象时会被调用;如果只是使用类的静态成员,则不会被调用
DD dd = new DD();
DD dd1 = new DD();
System.out.println(DD.n);
}
}

class AA {
// 静态代码块
static {
System.out.println("AA类的静态代码块");
}
}

class BB extends AA{
static {
System.out.println("BB类的静态代码块");
}
}

class CC {
public static int n1 = 1000;
static {
System.out.println("CC类的静态代码块");
}
}

class DD {
public static int n = 999;
static {
System.out.println("DD类的静态代码块");
}
{
System.out.println("DD类的普通代码块");
}
}


输出:
AA类的静态代码块

BB类的静态代码块 //这里由于父类AA已经加载过,不会再执行AA类的静态代码块

CC类的静态代码块
1000

DD类的静态代码块
DD类的普通代码块
DD类的普通代码块
999
  • 调用顺序(重点,难点)

    image-20231012160942764

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.durango.codeblock_;

public class CodeBlockDetail02 {
public static void main(String[] args) {
// 调用顺序
//1. 调用静态代码块和静态属性初始化
//2. 调用普通代码块和普通属性初始化
//3. 调用构造方法
A a = new A();
}
}

class A {
//静态属性初始化
private static int n1 = getN1();
//普通属性初始化
private int n2 = getN2();

//构造器
public A(){
System.out.println("执行A类的构造方法");
}

//普通代码块
{
System.out.println("执行A类的普通代码块");
}

//静态代码块
static {
System.out.println("执行A类的静态代码块");
}

public static int getN1() {
System.out.println("getN1被调用...静态属性初始化");
return 100;
}

private int getN2() {
System.out.println("getN2被调用...普通属性初始化");
return 99;
}
}

输出:
getN1被调用...静态属性初始化
执行A类的静态代码块
getN2被调用...普通属性初始化
执行A类的普通代码块
执行A类的构造方法
  • 构造器前面其实隐含了super()、调用普通代码块和普通属性初始化!!!

    image-20231012162629060

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.durango.codeblock_;

public class CodeBlockDetail03 {
public static void main(String[] args) {
new BBB();
}
}

class AAA {
{
System.out.println("AAA类的普通代码块");
}

public AAA(){
System.out.println("AAA类构造器被调用");
}
}

class BBB extends AAA{
private int n = getN();

private int getN() {
System.out.println("调用getN...普通属性初始化");
return 100;
}

{
System.out.println("BBB类的普通代码块");
}

public BBB(){
// 构造器前面其实隐含了super()、调用普通代码块和普通属性初始化
// super();
// 调用BBB的普通代码块和普通属性初始化
System.out.println("BBB类构造器被调用");
}
}


输出:
AAA类的普通代码块
AAA类的构造器被调用
调用getN...普通属性初始化
BBB类的普通代码块
BBB类的构造器被调用
  • 存在继承关系时,静态代码块,静态属性初始化,普通代码块,普通属性初始化,构造方法调用顺序</font

    image-20231012163641321

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package com.durango.codeblock_;

public class CodeBlockDetail04 {
public static void main(String[] args) {
new CCCC();
}
}

class AAAA {
private int n1 = getN1();
private static int n2 = getN2();

public AAAA(){
System.out.println("AAAA类的构造器被调用");
}

{
System.out.println("AAAA类的普通代码块被执行");
}
static {
System.out.println("AAAA类的静态代码块被执行");
}

public int getN1() {
System.out.println("AAAA类的普通属性初始化");
return 100;
}

public static int getN2() {
System.out.println("AAAA类的静态属性初始化");
return 99;
}
}

class BBBB extends AAAA{
private int n3 = getN3();
private static int n4 = getN4();

public BBBB(){
System.out.println("BBBB类的构造器被调用");
}

{
System.out.println("BBBB类的普通代码块被执行");
}
static {
System.out.println("BBBB类的静态代码块被执行");
}

public int getN3() {
System.out.println("BBBB类的普通属性初始化");
return 100;
}

public static int getN4() {
System.out.println("BBBB类的静态属性初始化");
return 99;
}
}

class CCCC extends BBBB{
private int n5 = getN5();
private static int n6 = getN6();

public CCCC(){
System.out.println("CCCC类的构造器被调用");
}

{
System.out.println("CCCC类的普通代码块被执行");
}
static {
System.out.println("CCCC类的静态代码块被执行");
}

public int getN5() {
System.out.println("CCCC类的普通属性初始化");
return 100;
}

public static int getN6() {
System.out.println("CCCC类的静态属性初始化");
return 99;
}
}
输出:
AAAA类的静态属性初始化
AAAA类的静态代码块被执行
BBBB类的静态属性初始化
BBBB类的静态代码块被执行
CCCC类的静态属性初始化
CCCC类的静态代码块被执行

AAAA类的普通属性初始化
AAAA类的普通代码块被执行
AAAA类的构造器被调用
BBBB类的普通属性初始化
BBBB类的普通代码块被执行
BBBB类的构造器被调用
CCCC类的普通属性初始化
CCCC类的普通代码块被执行
CCCC类的构造器被调用

10.4 单例设计模式(static实践)

10.4.1 设计模式

是为解决软件设计中通用问题而被提出的一套指导性思想。它是一种被反复验证、经过实践证明并被广泛应用的代码设计经验和思想总结,可以帮助开发者通过一定的模式来快速的开发高质量、可维护性强的软件。

10.4.2 单例模式

  • 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例, 并且该类只提供一个取得其对象实例的方法

10.4.3 设计步骤

  1. 先将构造器私有化,防止外部直接new 对象
  2. 在类的内部直接创建一个静态对象(恶汉式:在属性处创建;懒汉式:在方法中创建)
  3. 向外部暴露一个公共的静态方法,返回这个对象实例

10.4.4 恶汉式和懒汉式

  • 恶汉式(类加载时就创建了实例,存在资源浪费问题)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    package com.durango.single_;

    public class SingleTon01 {
    public static void main(String[] args) {
    System.out.println(GirlFriend.n1); // 此时已经创建对象实例

    GirlFriend instance = GirlFriend.getInstance();
    System.out.println(instance);
    }

    }

    class GirlFriend {

    private String name;
    public static int n1 = 100;
    private static GirlFriend gf = new GirlFriend("小花");

    // 饿汉式
    // 1. 先将构造器私有化
    // 2. 在类的内部直接创建一个静态对象
    // 3. 用公共的静态方法返回这个对象实例
    private GirlFriend(String name) {
    System.out.println("构造器被调用");
    this.name = name;
    }

    public static GirlFriend getInstance() {
    return gf;
    }

    @Override
    public String toString() {
    return "GirlFriend{" +
    "name='" + name + '\'' +
    '}';
    }
    }
  • 懒汉式(类使用时才创建实例,存在线程安全问题)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package com.durango.single_;

    public class SingleTon02 {
    public static void main(String[] args) {
    System.out.println(Cat.n1); // 此时不会创建对象实例

    Cat cat = Cat.getInstance(); // 会创建单例
    System.out.println(cat.toString());

    Cat cat1 = Cat.getInstance(); // 不会重复创建实例
    System.out.println(cat1.toString());

    System.out.println(cat == cat1);
    }
    }

    class Cat {
    private String name;
    public static int n1 = 999;
    private static Cat cat;

    // 懒汉式
    // 1. 先将构造器私有化
    // 2. 在类的内部(getInstance方法)直接创建一个静态对象
    // 3. 用公共的静态方法返回这个对象实例
    // 用户调用getInstance方法时才创建对象,重复调用会返回上次创建的对象
    private Cat(String name) {
    System.out.println("构造器被调用");
    this.name = name;
    }

    public static Cat getInstance() {
    if (cat == null) {
    cat = new Cat("小花");
    }
    return cat;
    }

    @Override
    public String toString() {
    return "Cat{" +
    "name='" + name + '\'' +
    '}';
    }
    }
  • image-20231013162756514

10.5 final关键字

10.5.1 基本介绍

  • final可以修饰类、属性、方法、局部变量
    • 修饰类时,该类不能被继承
    • 修饰属性(成员变量)时,属性的值不能被修改
      • 属性是基本数据类型,值是数值,不能被修改
      • 属性是引用数据类型,值是地址,不能被修改
    • 修饰方法时,方法不能被重写/覆盖
    • 修饰局部变量时,变量的值不能被修改

10.5.2 注意事项

  1. final修饰的属性又叫常量,一般用XX_XX_XX来命名
  2. final修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在如
    下位置之一【选择一个位置赋初值即可】:
    ①定义时:如public final double TAX_RATE=0.08; ②在构造器中 ③在代码块中。
  3. 如果final修饰的属性是静态的,则初始化的位置只能是 ①定义时 ②在静态代码块。不能在构造器中赋值。
  4. final类不能继承,但是可以实例化对象。
  5. 如果类不是final类,但是含有final方法,则该方法虽然不能重写,但是可以被继承。
  6. 一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法。6) final不能修饰构造方法(即构造器
  7. final和 static往往搭配使用,效率更高,不会导致类加载!!!底层编译器做了优化处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.durango.final_;

public class Final02 {
public static void main(String[] args) {
System.out.println(AAA.num);
}
}

class AA {
// final修饰的属性必须赋初始值,赋值的位置有三个
// 1. 定义时;2. 在构造器中 3. 在代码块中
public final double TAX_RATE = 0.08;
public final double TAX_RATE2;
public final double TAX_RATE3;

public AA() {
TAX_RATE2 = 0.09;
}

{
TAX_RATE3 = 0.07;
}
}

class BB {
// final修饰的属性如果是static的,则初始化位置只能是
// 1. 定义时;2. 在静态代码块中
public static final double TAX_RATE = 0.08;
public static final double TAX_RATE1;

static {
TAX_RATE1 = 0.07;
}
}

class AAA {
// final和static搭配使用效率更高,不会导致类加载
public static final int num = 1000;
static {
System.out.println("BBB类被加载,静态代码块被执行");
}
}

10.6 抽象类

10.6.1 基本介绍

  • 当父类的某些方法,需要声明,但是又不确定如何实现时,可以将其声明为抽象(abstract)方法, 那么这个类就是抽象类
  • 抽象方法必须存在于抽象类中,用abstract关键字修饰。
  • 抽象类的价值更多是在于设计,是设计者设计好后,让子类继承并实现抽象类()

10.6.2 注意事项 (面试常考)

  1. 抽象类不能被实例化
  2. 抽象类不一定要包含抽象方法;但是抽象方法一定要存在于抽象类
  3. abstract只能修饰类和方法,不能修饰属性和其他的
  4. 抽象类可以有任意成员,如非抽象方法、构造器、静态属性等等
  5. 抽象方法不能有方法体,即不能实现
  6. 如果一个类继承了抽象类,则他必须实现抽象类的所有抽象方法,除非它还是声明为抽象类
  7. 抽象方法不能用private、final和static来修饰,这些关键字都是和重写相违背的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.durango.abstract_;

abstract class Animal {
private String name;

public Animal(String name) {
this.name = name;
}

// 抽象方法
public abstract void eat();
}

class Cat extends Animal {

public Cat(String name) {
super(name);
}

@Override
public void eat() {
System.out.println("小猫吃鱼");
}
}

10.7 模板设计模式(abstract实践)

  • 有不同的几个类,需要完成不同的job,但都需要计算完成job的时间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package com.durango.abstract_;

    public class TemplateTest {
    public static void main(String[] args) {
    new AA().calculateTime();
    new BB().calculateTime();
    }
    }

    abstract class Template { // 模板类

    public abstract void job(); // 模板方法

    public void calculateTime() {
    long start = System.currentTimeMillis();
    job(); // 动态绑定机制
    long end = System.currentTimeMillis();
    System.out.println("job执行了" + (end - start) + "毫秒");
    }
    }

    class AA extends Template {

    @Override
    public void job() {
    long num = 0;
    for (int i = 0; i < 100000; i++) {
    num += i;
    }
    }
    }

    class BB extends Template {

    @Override
    public void job() {
    long num = 0;
    for (int i = 0; i < 100000; i++) {
    num -= i;
    }
    }
    }

10.8 接口(interface)

10.8.1 基本介绍

  • 接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,在根据具体情况把这些方法写出来

  • 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    interface 接口名 {
    属性;(默认是public static final修饰的,需要赋初值)
    方法;(1. 抽象方法【abstract可以省略】; 2.默认实现方法【default】;3.静态方法【static】)
    }

    class 类名 implements 接口名 {
    自己的属性;
    自己的方法;
    必须实现的接口的抽象方法;
    }
  • 小结

    • 在JDK7以前,接口里的方法都是抽象方法,没有方法体

    • 在JDK8以后,接口里面也可以有静态方法、默认方法(default),也就是说可以有方法具体实现。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      interface AInterface {
      // 属性
      public int n = 10;

      // 抽象方法
      public void hi();

      //jdk8以后可以有默认方法和静态方法
      default public void ok() {
      System.out.println("ok~~~");
      }

      static public void hello() {
      System.out.println("hello~~~");
      }
      }

10.8.2 应用场景

image-20231014101020720

image-20231014101059395

  • 举例:项目需要实现对三个不同数据库的连接操作,如果直接编写三个连接的类,类中的连接方法名可能不统一;在项目使用数据库时,需要重载三种不同形参类型的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.durango.interface_;

    public class Interface03 {
    public static void main(String[] args) {
    DBManager manager = new DBManager();
    manager.toDatabase(new MysqlDB());
    manager.toDatabase(new OracleDB());
    }

    }

    class DBManager {
    public void toDatabase(DBInterface dbInterface) {
    dbInterface.connect();
    dbInterface.close();
    }
    }

    class MysqlDB implements DBInterface {

    @Override
    public void connect() {
    System.out.println("连接Mysql数据库");
    }

    @Override
    public void close() {
    System.out.println("关闭Mysql数据库");
    }
    }

    class OracleDB implements DBInterface {

    @Override
    public void connect() {
    System.out.println("连接Oracle数据库");
    }

    @Override
    public void close() {
    System.out.println("关闭Oracle数据库");
    }
    }

10.8.3 注意事项

  1. 接口不能被实例化
  2. 接口中所有**方法都是public的(可以省略public),接口中的抽象方法可以不用abstract修饰**
  3. 普通类实现接口,就必须实现接口中的所有抽象方法。快捷键 alt+enter
  4. 抽象类实现接口,可以不用实现接口中的抽象方法
  5. 一个类可以实现多个接口
  6. 接口中的**属性是public static final的,比如 int n1 = 100 实际上是 public static final int n1 = 100;(必须初始化)。实现了接口的类也可以调用接口中的属性,但是不能修改**(接口中的变量默认是public static final修饰的,需要赋初值)。
  7. 接口不能继承其他的类,但是可以继承多个别的接口
  8. 接口的修饰符只能是public和默认,这点和类的修饰符是一样的

10.8.4 实现接口 VS 继承类

  1. 接口和继承解决的问题不同
    • 继承的价值在于:解决代码的复用性和可维护性
    • 接口的价值在于:设计并设计好各种规范(方法),让其他类去实现这些方法,即更加灵活
  2. 接口比继承更加灵活
    • 继承是 is-a 的关系
    • 接口是 like-a 的关系
  3. 接口在一定程度上实现了代码解耦 【接口规范性+动态绑定】

10.8.5 接口的多态特性

  • 多态参数

    • 接口的引用可以指向实现了接口的类的对象,比如在前面的连接数据库案例中,DBInterface dbInterface既可以接收MysqlDB,也可以接收OrcalDB
  • 多态数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    USBInterface[] usbs = new USBInterface[2];
    usbs[0] = new Phone();
    usbs[1] = new Camera();
    for (int i = 0; i < usbs.length; i++) {
    usbs[i].start(); // 动态绑定
    if (usbs[i] instanceof Phone) {
    ((Phone) usbs[i]).call();
    } else if (usbs[i] instanceof Camera) {
    ((Camera) usbs[i]).photo();
    }
    usbs[i].end();
    }
  • 多态传递

    • 接口的父级接口的引用也可以指向实现了该接口的类的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.durango.interface_;

    public class InterfacePolyPass {
    public static void main(String[] args) {
    // Teacher实现了IG,IG继承了IH。
    IG ig = new Teacher();
    IH ih = new Teacher();
    }
    }

    interface IH {
    }

    interface IG extends IH {
    }

    class Teacher implements IG {
    }

10.9 内部类

10.9.1 基本介绍

  • 类的五大成员:属性;方法;构造器;代码块;内部类

  • 一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类(outer class)。内部类的最大特点:可以直接访问私有属性,并且可以体现类与类之间的包含关系

  • 基本语法

    1
    2
    3
    4
    5
    6
    7
    8
    class Outer {	// 外部类
    class Inner { // 内部类
    }
    }

    class Other{ // 外部其他类

    }

10.9.2 分类

  • 定义在外部类局部位置上(比如方法内,代码块中),本质是局部变量
    • 局部内部类(有类名)
    • 匿名内部类(无类名,重点!!!)
  • 定义在外部类成员位置上,本质是类的成员
    • 成员内部类(没有static修饰)
    • 静态内部类(使用static修饰)

10.9.3 局部内部类

  1. 可以直接访问外部类的所有成员,包含私有的
  2. 不能添加访问修饰符, 因为它的地位就是一个局部变量。局部变量是不能使用修饰符的。但是可以使用final修饰, 因为局部变量也可以使用final
  3. 作用域: 仅仅在定义它的方法或代码块中
  4. 局部内部类—访问—->外部类的成员,访问方式:直接访问
  5. 外部类—>访问—->局部内部类的成员,访问方式:创建对象, 再访问(注意:必须在作用域内)
    • (1)局部内部类定义在方法中/代码块
    • (2)作用域在方法体或者代码块中
    • (3)本质仍然是个类
  6. 外部其他类—不能访问—–>局部内部类(因为局部内部类地位是一个局部变量)
  7. 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用**(外部类名.this.成员**)去访问【演示】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.durango.innerclass_;

public class LocalInnerClass {
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.m1();
System.out.println("outer02的hashcode=" + outer02.hashCode());
}
}

class Outer02 {
private int n1 = 100;

private void m2() {
System.out.println("Outer02 m2()");
}

//局部内部类:
// 1. 可以直接访问外部类成员(包括私有)
// 2. 不能添加访问修饰符,相当于局部变量,可以加final修饰
// 3. 作用域,定义它的方法或代码块中
// 4. 外部类访问内部类成员,需要new对象。注意必须在作用域中
public void m1() {
final class Inner02 { //方法中的局部内部类
private int n1 = 200;
public void f1() {
System.out.println("n1=" + n1); //遵循就近原则
// Outer02.this本质是外部类的对象,哪个对象调用了m1,Outer02.this就指向哪个对象
System.out.println("外部类的n1=" + Outer02.this.n1);
System.out.println("Outer02.this的hashcode=" + Outer02.this.hashCode());
m2();
}
}
m2();
new Inner02().f1();
}

{
class Inner03 { //代码块中的局部内部类
public void f2() {

}
}
}
}
输出:
Outer02 m2()
n1=200
外部类的n1=100
Outer02.this的hashcode=1163157884
Outer02 m2()
outer02的hashcode=1163157884

10.9.4 匿名内部类(重要!!!)

  1. (1)本质是类;(2)内部类;(3)该类没有名字;(4)同时还是一个对象
    • new 类或接口(参数列表){ 类体 };
  2. 因为匿名内部类既是一个类的定义,同时它本身也是一个对象,因此从语法上看,它既有定义类的特征,也有创建对象的特征。调用匿名内部类的成员有两种方式:赋值后调用;直接调用。
  3. 可以直接访问外部类成员,包括私有的
  4. 不能添加访问修饰符,本质是一个局部变量,可以加final修饰
  5. 作用域:定义它的方法或代码块中
  6. 匿名内部类 –> 访问 –> 外部类 成员 : 直接访问
  7. 外部类 –> 访问 –> 匿名内部类 成员:new对象赋值后调用; new对象直接调用
  8. 如果外部类和匿名内部类的成员重名时,匿名内部类访问的话,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问
  9. 底层给匿名内部类分配的名字为:外部类名$X,可以用**getClass()**查看,即运行类型
  • 基于接口实现的匿名内部类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.durango.innerclass_;

    public class AnonymousInnerClass {
    public static void main(String[] args) {
    Outer04 outer04 = new Outer04();
    outer04.method();
    }
    }

    class Outer04 {
    private int n1 = 10;

    public void method() {
    // 基于接口的匿名内部类
    // 1. 需求:使用A接口,并创建对象
    // 2. 传统做法:写一个类实现A接口,然后创建这个类的对象
    // 3. 需求是 Tiger/Dog 类只是使用一次,后面不再使用
    // 4. 可以使用匿名内部类来简化开发
    // 5. tiger 的编译类型 ? A
    // 6. tiger 的运行类型 ? 就是匿名内部类 (名字由jvm分配为Outer04$1)
    // 7. jvm底层在创建匿名内部类Outer04$1,并创建了Outer04$1实例把地址返回给tiger
    // 8. 这个匿名内部类只能使用一次,是指不能再创建这个匿名内部类的实例了。
    // tiger是已经创建的唯一匿名内部类对象,可以使用多次
    A tiger = new A() {
    @Override
    public void cry() {
    System.out.println("老虎叫唤");
    }
    };
    System.out.println("tiger的运行类型=" + tiger.getClass());
    tiger.cry();
    tiger.cry();
    }
    }

    interface A {
    void cry();
    }

    输出:
    tiger的运行类型=class com.durango.innerclass_.Outer04$1
    老虎叫唤
    老虎叫唤
  • 基于类或抽象类继承的匿名内部类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package com.durango.innerclass_;

    public class AnonymousInnerClass {
    public static void main(String[] args) {
    Outer04 outer04 = new Outer04();
    outer04.method();
    }
    }

    class Outer04 {
    private int n1 = 10;

    public void method() {
    // 基于类的匿名内部类
    // 1. father 编译类型 Father
    // 2. father 运行类型 Outer04$2
    // 3. 底层会创建匿名内部类Outer04$2继承Father,并创建Outer04$2实例返回给father
    Father father = new Father("jack") {
    @Override
    public void test() {
    System.out.println("这是匿名内部类的Test");
    }
    };
    System.out.println("father对象的运行类型=" + father.getClass());
    father.test();
    }
    }

    class Father {
    private String name;

    public Father(String name) {
    this.name = name;
    System.out.println("接收到name");
    }

    public void test() {
    System.out.println("这是Father的test");
    }
    }
    输出:
    接收到name
    father对象的运行类型=class com.durango.innerclass_.Outer04$2
    这是匿名内部类的Test
  • 调用匿名内部类的成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Outer05 {
    private int n1 = 99;

    public void f1() {
    // 先赋值再调用
    Person p = new Person(){
    @Override
    public void hi() {
    System.out.println("匿名内部类重写了hi方法");
    }
    };
    p.hi(); //动态绑定,运行类型时Outer05$1

    //也可以直接调用,匿名内部类创建类并返回了一个实例
    new Person(){
    @Override
    public void hi() {
    System.out.println("匿名内部类重写了hi方法");
    }
    }.hi();
    }
    }

10.9.5 匿名内部类最佳实践

  • 匿名内部类当作实参直接传递,简洁高效

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.durango.innerclass_;

    public class InnerClassExercise01 {
    public static void main(String[] args) {

    // 匿名内部类当做实参直接传递
    f1(new IL() {
    @Override
    public void show() {
    System.out.println("匿名内部类当做实参直接传递");
    }
    });

    //传统方式
    f1(new Tr());
    }

    // 静态方法,形参是接口类型
    public static void f1(IL il) {
    il.show();
    }
    }

    interface IL {
    void show();
    }

    class Tr implements IL{
    @Override
    public void show() {
    System.out.println("传统方式,实现接口再传参");
    }
    }
  • 形参有多个接口,实参可以是多个匿名内部类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package com.durango.innerclass_;

    public class InnerClassExercise02 {
    public static void main(String[] args) {
    CellPhone cp = new CellPhone();
    cp.alarmClock(new Bell() {
    @Override
    public void ring() {
    System.out.println("懒猪起床了");
    }
    }, new Bell() {
    @Override
    public void ring() {
    System.out.println("上班要迟到了");
    }
    });
    }
    }

    interface Bell {
    void ring();
    }

    class CellPhone {
    // 形参是接口类型,实参可以用匿名内部类
    public void alarmClock(Bell b1, Bell b2){
    b1.ring();
    System.out.println(b1.getClass());
    b2.ring();
    System.out.println(b2.getClass());
    }
    }
    输出:
    懒猪起床了
    class com.durango.innerclass_.InnerClassExercise02$1
    上班要迟到了
    class com.durango.innerclass_.InnerClassExercise02$2

10.9.6 成员内部类

  1. 可以直接访问外部类成员,包括私有的

  2. 可以添加任意访问修饰符,本质是类的成员

  3. 作用域:定义它的整个类体

  4. 成员内部类访问外部类成员:直接访问

  5. 外部类访问内部类成员:创建对象访问

  6. 成员重名时遵循就近原则,可以通过 外部类名.this.成员 访问外部成员

  7. 外部其他类 可以 访问 内部类:有两种方式如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class MemberInnerClass01 {
    public static void main(String[] args) {
    Outer08 outer08 = new Outer08();
    outer08.hi();

    // 外部其他类,使用成员内部类的三种方式(遵循访问权限)
    // 1. new Outer08().new Inner08()相当于把new Inner08()当做是new Outer08()的成员
    Outer08.Inner08 inner08 = new Outer08().new Inner08();

    // 2. 在外部类中编写一个方法,放回Inner08的实例
    Outer08.Inner08 inner08_1 = new Outer08().getInner();

    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Outer08 { //外部类
    private int n1 = 100;
    public String name = "jack";

    // 成员内部类
    public class Inner08 { // 可以添加访问修饰符
    private int n1 = 99;
    public void say() {
    // 可以直接访问外部类成员,包括私有的
    System.out.println("n1 = " + n1 + " name=" + name);
    // 重名时遵守就近原则,通过"外部类.this.成员"访问外部类成员
    System.out.println("Outer n1="+Outer08.this.n1);
    }
    }

    public void hi() {
    // 成员内部类的作用域是定义它的整个类体
    // 外部类访问内部类成员,需要new对象
    Inner08 inner08 = new Inner08();
    inner08.say();
    }

    public Inner08 getInner() {
    return new Inner08();
    }
    }

10.9.6 静态内部类

  1. 可以直接访问外部类静态的成员,包括私有的。不能访问外部类非静态成员

  2. 可以添加任意访问修饰符,本质是类的成员

  3. 作用域:定义它的整个类体

  4. 成员内部类访问外部类静态的成员:直接访问

  5. 外部类访问静态内部类成员:创建对象访问

  6. 成员重名时遵循就近原则,可以通过 外部类名.成员 访问外部成员

  7. 外部其他类 可以 访问 静态内部类:有两种方式如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MemberInnerClass02 {
    public static void main(String[] args) {
    // 外部其他类访问静态内部类
    // 1. 静态成员可以直接通过类名访问Outer09.Inner09(),无需创建外部类对象
    Outer09.Inner09 inner09 = new Outer09.Inner09();

    // 2.编写一个(静态)方法,可以返回静态内部类的对象实例
    Outer09.Inner09 inner09_1 = new Outer09().getInner();
    Outer09.Inner09 inner09_2 = Outer09.getInner_();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class Outer09 { //外部类
    private static int n1 = 100;
    public static String name = "jack";

    // 静态内部类
    public static class Inner09 { // 可以添加访问修饰符
    private int n1 = 99;

    public void say() {
    // 可以直接访问外部类的静态成员(不能访问非静态成员)
    System.out.println("n1 = " + n1 + " name=" + name);
    // 重名时遵守就近原则,通过"外部类.成员"访问外部类成员
    System.out.println("Outer n1=" + Outer09.n1);
    }
    }

    public void hi() {
    // 静态内部类的作用域是定义它的整个类体
    // 外部类访问静态内部类成员,需要new对象
    Inner09 inner09 = new Inner09();
    inner09.say();
    }

    public Inner09 getInner() {
    return new Inner09();
    }

    public static Inner09 getInner_() {
    return new Inner09();
    }
    }

第十一章 枚举和注解

11.1 自定义类实现枚举

  1. 构造器私有化,防止直接new对象

  2. 本类内部创建一组对象 [四个 春夏秋冬]

  3. 对外暴露对象(通过为对象添加 public final static 修饰符)

  4. 可以提供 get 方法,但是不要提供 set

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Season {
    private String name;
    private String desc;

    //定义了四个对象, 固定.
    public static final Season SPRING = new Season("春天", "温暖");
    public static final Season WINTER = new Season("冬天", "寒冷");
    public static final Season AUTUMN = new Season("秋天", "凉爽");
    public static final Season SUMMER = new Season("夏天", "炎热");

    private Season(String name, String desc) {
    this.name = name;
    this.desc = desc;
    }

    public String getName() {
    return name;
    }

    public String getDesc() {
    return desc;
    }
    }

11.2 enum关键字实现枚举

11.2.1 说明

  1. 用enum关键字替代class
  2. 将定义的常量对象写在最前面,常量名(实参列表),多个常量用 “”隔开
  3. 最后加上构造器和get方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Enumeration02 {
public static void main(String[] args) {
System.out.println(Season2.SPRING); //春天
Season2 spring1 = Season2.SPRING;
Season2 spring2 = Season2.SPRING;
System.out.println(spring1 == spring2); //true
}
}
//使用 enum 来实现枚举类
//1. 使用关键字 enum 替代 class
//2. public static final Season SPRING = new Season("春天", "温暖") 直接替换成
// SPRING("春天", "温暖") 解读 常量名(实参列表)
//3. 如果有多个常量(对象), 使用 ,号间隔即可
//4. 如果使用 enum 来实现枚举,要求将定义常量对象,写在前面
//5. 如果我们使用的是无参构造器,创建常量对象,则可以省略 ()
enum Season2 {
SPRING("春天", "温暖"), //调用有参构造器
WINTER("冬天", "寒冷"),
AUTUMN("秋天", "凉爽"),
SUMMER("夏天", "炎热"),
Other; //调用无参构造器

private String name;
private String desc;

private Season2(String name, String desc) {
this.name = name;
this.desc = desc;
}

private Season2() {}

public String getName() {
return name;
}

public String getDesc() {
return desc;
}
}

11.2.2 注意事项

  1. 当我们使用 enum 关键字开发一个枚举类时,默认会继承 Enum 类, 而且是一个 final 类[如何证明],使用 javap 工具来演示
  1. 传统的 public static final Season2 SPRING = new Season2(“春天”, “温暖”); 简化成 SPRING(“春天”, “温暖”), 这里必须知道,它调用的是哪个构造器.

  2. 如果使用无参构造器 创建 枚举对象,则实参列表和小括号都可以省略

  3. 当有多个枚举对象时,使用”,”间隔,最后有一个分号结尾

  4. 枚举对象必须放在枚举类的行首

11.2.3 Enum类常用方法

  • 说明:使用关键字 enum 时,会隐式继承 Enum 类, 这样我们就可以使用 Enum 类相关的方法。

image-20231018101743063

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EnumMethod {
public static void main(String[] args) {
Season2 autumn = Season2.AUTUMN;

System.out.println(autumn.name()); //枚举对象的名称(常量名)
System.out.println(autumn.ordinal()); //枚举对象的次序

Season2[] values = Season2.values(); // 返回所有枚举对象的数组
for (Season2 season : values) {
System.out.println(season.name());
}

// 根据输入字符串查找对应的枚举对象
Season2 winter = Season2.valueOf("WINTER");
System.out.println(winter.name());

// 比较两个枚举常量的编号 self.ordinal - other.ordinal
System.out.println(autumn.compareTo(winter));
}
}

11.2.4 enum实现接口

  • 使用 enum 关键字后,就不能再继承其它类了,因为 enum 会隐式继承 Enum,而 Java 是单继承机制。
  • 枚举类和普通类一样,可以实现接口,如下形式。enum 类名 implements 接口 1,接口 2{}

11.3 注解(Annotation)

  • 注解(Annotation)也被称为元数据(Metadata),用于修饰解释 包、类、方法、属性、构造器、局部变量等数据信息。

  • 和注释一样,注解不影响程序逻辑,但注解可以被编译或运行相当于嵌入在代码中的补充信息

  • 在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaEE 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 java EE 旧版中所遗留的繁冗代码和 XML 配置等。

11.4 三个基本的Annotation

  1. @Override: 限定某个方法,是重写父类方法, 该注解只能用于方法

    image-20231019093232279

  2. @Deprecated: 用于表示某个程序元素(类, 方法等)已过时

    image-20231019093638120

  3. @SuppressWarnings: 抑制编译器警告

    image-20231019094220353

    • (1) 放置的位置就是 TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE
    • (2) 该注解类有数组 String[] values() 设置一个数组比如 {“rawtypes”, “unchecked”, “unused”}

11.5 JDK的元注解(了解)

11.5.1 基本介绍

  • JDK 的元 Annotation 用于修饰其他 Annotation
  • 四种
    • Retention //指定注解的作用时间范围,三种 SOURCE,CLASS,RUNTIME
    • Target // 指定注解可以在哪些地方使用
    • Documented //指定该注解是否会在 javadoc 体现
    • Inherited //子类会继承父类注解

11.5.2 Retention

  • 只能用于修饰一个 Annotation 定义, 用于指定该 Annotation 可以保留多长时间, @Rentention 包含一个 RetentionPolicy类型的成员变量, 使用 @Rentention 时必须为该 value 成员变量指定值:

  • @Retention 的三种值

    • RetentionPolicy.SOURCE: 编译器使用后,直接丢弃这种策略的注释
    • RetentionPolicy.CLASS: 编译器将把注解记录在 class 文件中. 当运行 Java 程序时, JVM 不会保留注解。 这是默认值
    • RetentionPolicy.RUNTIME:编译器将把注解记录在 class 文件中. 当运行 Java 程序时, JVM 会保留注解. 程序可以通过反射获取该注解
  • image-20231019100145122

11.5.3 Target

image-20231019100225152

11.5.4 Documented

image-20231019100259358

11.5.5 Inherited

image-20231019100325048

第十二章 异常

12.1 基本概念

  • Java语言中,将程序执行中发生的不正常情况称为“异常”。(开发过程中的语法错误和逻辑错误不是异常)
  • 执行过程中所发生的异常事件可分为两大类
    • Error(错误): Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError[栈溢出]和OOM(out of memory). Error是严重错误,程序会崩溃。
    • Exception(异常): 其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如空指针访问,试图读取不存在的文件,网络连接中断等等,Exception分为两大类: 运行时异常[程序运行时,发生的异常]和编译时异常[编程时,编译器检查出的异常]。
  • 处理异常快捷键:Ctrl +Alt + T

12.2 异常体系图

image-20231020100303282

image-20231020100407648

12.3 常见运行时异常

  • NullPointerException 空指针异常
    • 当应用程序试图在需要对象的地方使用 null 时,抛出该异常
  • ArithmeticException 数学运算异常
    • 当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时
  • ArrayIndexOutOfBoundsException 数组下标越界异常
    • 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引
  • ClassCastException 类型转换异
    • 当试图将对象强制转换为不是实例的子类时,抛出该异常。
  • NumberFormatException 数字格式不正确异常
    • 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常

12.4 常见编译异常

  • SQLException 操作数据库时,查询表可能发生异常
  • IOException 操作文件时,发生的异常
  • FileNotFoundException 当操作一个不存在的文件时,发生异常
  • ClassNotFoundException 加载类,,而该类不存在时,异常
  • EOFException 操作文件,到文件末尾,发生异常
  • IllegalArguementException 参数异常

12.5 异常处理

  • **try-catch-finally**:程序员在代码中捕获发生的异常,自行处理

    image-20231020102616097

  • **throws**:将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者就是JVM。如果没有try-catch处理,则默认采用throws处理

    image-20231020102734625

12.6 try-catch 异常处理

  1. 如果异常发生了,则异常发生后面的代码不会执行,直接进入到catch块;如果异常没有发生,则顺序执行try的代码块,不会进入到catch.

  2. 如果异常没有发生,则顺序执行 try 的代码块,不会进入到 catch

  3. 如果希望不管是否发生异常,都执行某段代码(比如关闭连接,释放资源等)则使用如下代码- finally

  4. 可以有多个catch语句,捕获不同的异常(进行不同的业务处理),**要求父类异常在后,子类异常在前**,比如(Exception在后,NullPointerException在前),如果发生异常,只会匹配一个catch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class TryCatchDetail02 {
    public static void main(String[] args) {
    //1.如果 try 代码块有可能有多个异常
    //2.可以使用多个 catch 分别捕获不同的异常,相应处理
    //3.要求子类异常写在前面,父类异常写在后面
    try {
    Person person = new Person();
    // person = null;
    System.out.println(person.getName());//NullPointerException
    int n1 = 10;
    int n2 = 0;
    int res = n1 / n2;//ArithmeticException
    } catch (NullPointerException e) {
    System.out.println("空指针异常=" + e.getMessage());
    } catch (ArithmeticException e) {
    System.out.println("算术异常=" + e.getMessage());
    } catch (Exception e) {
    System.out.println(e.getMessage());
    } finally {}
    }
    }
  5. 可以进行try-finally 配合使用,这种用法相当于没有捕获异常,因此程序会直接崩掉/退出。应用场景,就是执行一段代码,不管是否发生异常,都必须执行某个业务逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class TryCatchDetail02 {
    public static void main(String[] args) {
    try {
    int n1 = 100;
    int n2 = 0;
    System.out.println(n1 / n2);
    } finally {
    System.out.println("执行了finally...");
    }

    System.out.println("程序继续执行...");
    }
    }
    输出:
    执行了finally...

12.7 try-catch 异常处理练习

  • Exercise01

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class TryCatchExercise01 {
    public static void main(String[] args) {
    System.out.println(method());
    }

    public static int method() {
    try {
    String[] names = new String[3];
    if (names[1].equals("durango")) {
    System.out.println(names[1]);
    } else {
    names[3] = "zlx";
    }
    return 1;
    } catch (ArrayIndexOutOfBoundsException e) {
    return 2;
    } catch (NullPointerException e) {
    return 3;
    }finally { // finally必须执行
    return 4;
    }
    }
    }
    输出:4
  • Exercise02

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class TryCatchExercise02 {
    public static void main(String[] args) {
    System.out.println(method());
    }

    public static int method() {
    int i = 1;
    try {
    i++; // i=2
    String[] names = new String[3];
    if (names[1].equals("durango")) {
    i++;
    System.out.println(names[1]);
    } else {
    i++;
    names[3] = "zlx";
    }
    return 1;
    } catch (ArrayIndexOutOfBoundsException e) {
    return 2;
    } catch (NullPointerException e) {
    return ++i; // i=3
    }finally { // finally必须执行
    return ++i; // i=4
    }
    }
    }
    输出:4
  • Exercise03

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package com.durango.trycatch_;

    public class TryCatchExercise03 {
    public static void main(String[] args) {
    System.out.println(method());
    }

    public static int method() {
    int i = 1;
    try {
    i++; // i=2
    String[] names = new String[3];
    if (names[1].equals("durango")) {
    System.out.println(names[1]);
    } else {
    names[3] = "zlx";
    }
    return 1;
    } catch (ArrayIndexOutOfBoundsException e) {
    return 2;
    } catch (NullPointerException e) {
    return ++i; // i=3 ==> 保存到临时变量temp=3并执行完finally后再return!!
    } finally { // finally必须执行
    ++i; // i=4
    System.out.println("i=" + i);
    }
    }
    }
    输出:
    i=4
    3

12.8 throws异常处理

  1. 如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理

  2. 在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

  3. 注意事项:

    • 对于编译异常,程序中必须显式地处理,比如try-catch或者throws
    • 对于运行时异常,程序中如果没有处理,默认就是抛出的方式处理(隐式地处理)
    • 子类重写父类的方法时,对抛出异常的规定: 子类重写的方法,所抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出的异常的类型的子类型
    • 在throws 过程中,如果有方法 try-catch,就相当于处理异常,就可以不必throws
  4. throw和throws的区别

    image-20231021155142632

  5. 一个练习

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    package com.durango.throws_;

    public class ThrowExercise01 {
    public static void main(String[] args) {
    try {
    ReturnExceptionDemo.methodA();
    } catch (Exception e) {
    System.out.println(e.getMessage());
    }

    ReturnExceptionDemo.methodB();
    }
    }

    class ReturnExceptionDemo {
    static void methodA() {
    try {
    System.out.println("进入方法A");
    throw new RuntimeException("制造异常");
    } finally {
    System.out.println("A方法的finally");
    }
    }

    static void methodB() {
    try {
    System.out.println("进入方法B");
    return;
    } finally {
    System.out.println("B方法的finally");
    }
    }
    }
    输出:
    进入方法A
    A方法的finally
    制造异常
    进入方法B
    B方法的finally

12.9 自定义异常

12.9.1 基本概念

​ 当程序中出现了某些“错误”,但该错误信息并没有在Throwable子类中描述处理,这个时候可以自己设计异常类,用于描述该错误信息。

12.9.2 步骤

  • 定义类:自定义异常类名(程序员自己写)继承Exception或RuntimeException
  • 如果继承Exception,属于编译异常
  • 如果继承RuntimeException,属于运行异常(一般来说,继承RuntimeException,好处是可以使用默认处理机制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CustomException {
public static void main(String[] args) {
int age = 180;
if (!(age >= 18 && age <= 120)) {
throw new AgeException("年龄需要再18-120之间");
}
System.out.println("输入年龄范围正确");
}
}

// 自定义异常
class AgeException extends RuntimeException {
public AgeException(String message) {
super(message);
}
}

第十三章 常用类

13.1 包装类

13.1.1 包装类的分类

  • 针对八种基本数据类型相应的引用类型—包装类

    image-20231023093841832

    image-20231023093942360

image-20231023093958943

image-20231023094111547

13.1.2 包装类与基本数据类型转换

  • jdk5前的手动装箱和拆箱方式,装箱: 基本类型->包装类型,反之,拆箱

  • jdk5 以后(含jdk5)的自动装箱和拆箱方式

  • 自动装箱底层调用的是valueOf方法,比如Integer.valueOf()

  • 其它包装类的用法类似,不举例

  • 基本方法:Interger.valueOf(n),integer.intValue()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package com.durango.wrapper_;

    public class Integer01 {
    public static void main(String[] args) {
    //int <--> Integer

    //jdk5以前是手动装箱和拆箱
    int n1 = 100;
    Integer integer = new Integer(n1);
    Integer integer1 = Integer.valueOf(n1);
    int i = integer.intValue();

    //jdk5以后,就可以自动装箱和拆箱
    int n2 = 100;
    Integer integer2 = n2; //底层仍然是使用Integer.valueOf(n2)
    int j = integer2; //底层仍然使用的是 intValue()方法
    }
    }
  • 一个经典面试题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Object obj = true ? new Integer(1) : new Double(2.0);
    System.out.println(obj); // 输出为1.0,三元运算符是一个整体,结果会向高精度类型转换

    Object obj2;
    if(true){
    obj2 = new Integer(1);
    } else {
    obj2 = new Double(2.0);
    }
    System.out.println(obj2); // 输出为1

13.1.3 包装类与String相互转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.durango.wrapper_;

public class WrapperVSString {
public static void main(String[] args) {
// 包装类 --> String
Integer i = 100; //自动装箱
// 方式1
String str1 = i + "";
// 方式2
String str2 = i.toString();
// 方式3
String str3 = String.valueOf(i);

// String --> 包装类
String str4 = "1234";
// 方式一
Integer i2 = Integer.parseInt(str4); //parseInt返回的是int类型,使用到自动装箱
// 方式二
Integer i3 = new Integer(str4);

}
}

13.1.4 Integer类 和 Character类的常用方法

  • 即用即查
1
2
3
4
5
6
7
8
9
System.out.println(Integer.MIN_VALUE); //返回最小值
System.out.println(Integer.MAX_VALUE);//返回最大值
System.out.println(Character.isDigit('a'));//判断是不是数字
System.out.println(Character.isLetter('a'));//判断是不是字母
System.out.println(Character.isUpperCase('a'));//判断是不是大写
System.out.println(Character.isLowerCase('a'));//判断是不是小写
System.out.println(Character.isWhitespace('a'));//判断是不是空格
System.out.println(Character.toUpperCase('a'));//转成大写
System.out.println(Character.toLowerCase('A'));//转成小写

13.1.5 经典面试题

  • Exercise01

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Object obj = true ? new Integer(1) : new Double(2.0);
    System.out.println(obj); // 输出为1.0,三元运算符是一个整体,结果会向高精度类型转换

    Object obj2;
    if(true){
    obj2 = new Integer(1);
    } else {
    obj2 = new Double(2.0);
    }
    System.out.println(obj2); // 输出为1
  • Exercise02查看valueOf源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    Integer i = new Integer(1);
    Integer j = new Integer(1);
    System.out.println(i == j); //false


    //1. 如果 i 在 IntegerCache.low(-128)~IntegerCache.high(127),就直接从数组返回
    //2. 如果不在 -128~127,就 new Integer(i)
    /*
    public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
    return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
    }
    */
    Integer m = Integer.valueOf(1); // 查看valueOf源码
    Integer n = Integer.valueOf(1);
    System.out.println(m == n); //true

    Integer x = 128; // 自动包装,底层也是调用Integer.valueOf,看源码
    Integer y = 128;
    System.out.println(x == y); //false
  • Exercise03

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    Integer i1 = new Integer(127);
    Integer i2 = new Integer(127);
    System.out.println(i1 == i2); //false

    Integer i3 = new Integer(128);
    Integer i4 = new Integer(128);
    System.out.println(i3 == i4); //false

    Integer i5 = 127;
    Integer i6 = 127;
    System.out.println(i5 == i6); //true

    Integer i7 = 128;
    Integer i8 = 128;
    System.out.println(i7 == i8); //false

    Integer i9 = 127;
    Integer i10 = new Integer(127);
    System.out.println(i9 == i10); //false

    // 只要有基本数据类型,判断的就是值是否相等
    Integer i11 = 127;
    int i12 = 127;
    System.out.println(i11 == i12); //true

    Integer i13 = 128;
    int i14 = 128;
    System.out.println(i13 == i14); //true

13.2 String类 (重要!)

13.2.1 String基本介绍

image-20231023104323731

image-20231023104356952

  • 构造器
    • String s1 = new String();
    • String s2 = new String(String original);
    • String s3 = new String(char[] a);
    • String s4 = new String(char[] a,int startIndex,int count)
    • String s5 = new String(byte[] b)
  • String 类实现了接口 Serializable【String 可以串行化:可以在网络传输】 接口 Comparable [String 对象可以比较大小]
  • String 是 final 类,因此不能被其他的类继承
  • String 有属性 private final char value[]; 用于存放字符串内容
  • **一定要注意**:value 是一个 final 类型(的引用类型), 不可以修改:即 value 不能指向新的地址,但是单个字符内容是可以变化

13.2.2 创建 String 对象的两种方式

image-20231023110429072

image-20231023110742572

1
2
3
4
5
6
7
8
9
// 创建String对象
// 1.直接赋值 字符串常量
// 2.调用构造器 创建字符串对象
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1 == str2); //false(比较的是引用地址)

// str1指向的是常量池中的"abc"
// str2指向的是堆中的String对象空间,其中维护的一个value属性指向常量池中的"abc"

13.2.3 练习题

  • Exercise01

    1
    2
    3
    4
    String a = "abc";
    String b = "abc";
    System.out.println(a.equals(b)); //true 查看equals源码,equals比较的是value数组的每一个字符
    System.out.println(a == b); //true 都是指向常量池中的"abc",==比较的是地址
  • Exercise02

    1
    2
    3
    4
    String c = new String("abc");
    String d = new String("abc");
    System.out.println(c.equals(d)); //true
    System.out.println(c == b); //false c和d指向堆中不同的对象空间,都维护了value属性指向常量池中的"abc"
  • Exercise03

    • 当调用intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(Object)方法确定),则返回池中的字符串。否则,将此 String 对象加到池中,并返回此String 对象的引用
    1
    2
    3
    4
    5
    6
    7
    String a = "abc";
    String b = new String("abc");
    System.out.println(a.equals(b)); //true
    System.out.println(a == b); //false

    System.out.println(a == b.intern()); // true b.intern()返回的是常量池的地址(value指向的地址)
    System.out.println(b == b.intern()); // false

13.3 字符串特性及面试题

13.3.1 字符串特性

  • String是一个final类,代表不可变的字符序列

  • 字符串是不可变的。一个字符串对象一旦被分配,其内容是不可变的。(区分对象的引用和对象本身)

    • ```java
      String s1 = “hello”;
      s1 = “haha”;
      // 创建了几个对象,画出内存布局图
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27

      ![image-20231024103622642](Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/image-20231024103622642.png)



      ### 13.3.2 面试题

      - 题目1

      ![image-20231024103856817](Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/image-20231024103856817.png)



      - 题目2

      - <font color='red'>两个字符串**常量相加**,直接在**池**中创建新常量。引用直接指向池中的这个常量</font>
      - <font color='red'>两个字符串**变量相加**,在**堆**中创建字符串对象,value[]指向池中的新常量。引用指向的是堆中的字符串对象。</font>
      - 常量相加指向池,变量相加指向堆

      ```java
      String a = "hello";
      String b = "haha";
      String c = "hello" + "haha";
      // 优化等价于String c = "hellohaha";
      // c指向池中的"hellohaha"

      System.out.println(c == "hellohaha"); // true
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    String a = "hello";
    String b = "haha";
    String c = a + b;
    // 1. 先创建一个StringBuilder sb = StringBuilder()
    // 2. 执行 sb.append("hello")
    // 3. 执行 sb.append("haha")
    // 4. 调用 sb.toString(), 返回一个new String("hellohaha")给c
    // 最终效果,c指向堆中的对象(String), value[]指向池中的"hellohaha"

    System.out.println(c == "hellohaha"); // false
  • 题目3

    image-20231024111346679

  • 题目4

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.durango.string_;

    public class String07 {
    public static void main(String[] args) {
    Test1 ex = new Test1();
    ex.change(ex.str, ex.ch);
    System.out.println(ex.str); //hsp
    System.out.println(ex.ch); //hava
    int[] a = new int[4];
    // ex.ch = new char[]{'a', 'b'}; 报错,ex.ch是final类型,不能重新指向新对象
    }
    }

    class Test1 {
    String str = new String("hsp");
    final char[] ch = {'j', 'a', 'v', 'a'};

    public void change(String str, char[] ch) {
    //str是形参,调用时指向实参对象
    str = "java"; //str重新指向新的对象后不会影响实参对象
    ch[0] = 'h';
    ch = new char[]{'a', 'b'};
    System.out.println(str); //java
    System.out.println(ch); // ab
    }
    }

    绘图1

13.4 String类常见方法

image-20231025164812593

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//1. equals 比较内容是否相同,区分大小写
String str1 = "hello";
String str2 = "Hello";
System.out.println(str1.equals(str2));

// 2.equalsIgnoreCase 忽略大小写的判断内容是否相等
String username = "johN";
if ("john".equalsIgnoreCase(username)) {
System.out.println("Success!");
} else {
System.out.println("Failure!");
}

// 3.length 获取字符的个数,字符串的长度
System.out.println("韩顺平".length());

// 4.indexOf 获取字符在字符串对象中第一次出现的索引,索引从 0 开始,如果找不到,返回-1
String s1 = "wer@terwe@g";
int index = s1.indexOf('@');
System.out.println(index);// 3
System.out.println("weIndex=" + s1.indexOf("we"));//0

// 5.lastIndexOf 获取字符在字符串中最后一次出现的索引,索引从 0 开始,如果找不到,返回-1
s1 = "wer@terwe@g@";
index = s1.lastIndexOf('@');
System.out.println(index);//11
System.out.println("ter 的位置=" + s1.lastIndexOf("ter"));//4

// 6.substring 截取指定范围的子串[start, end)
String name = "hello,张三";
//下面 name.substring(6) 从索引 6 开始截取后面所有的内容
System.out.println(name.substring(6));//截取后面的字符
//name.substring(0,5)表示从索引 0 开始截取,截取到索引 5-1=4 位置
System.out.println(name.substring(2,5));//llo

image-20231025165410347

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 1.toUpperCase 转换成大写
String s = "heLLo";
System.out.println(s.toUpperCase());//HELLO

// 2.toLowerCase
System.out.println(s.toLowerCase());//hello

// 3.concat 拼接字符串
String s1 = "宝玉";
s1 = s1.concat("林黛玉").concat("薛宝钗").concat("together");
System.out.println(s1);//宝玉林黛玉薛宝钗 together

// 4.replace 替换字符串中的字符
s1 = "宝玉 and 林黛玉 林黛玉 林黛玉";
//在 s1 中,将 所有的 林黛玉 替换成薛宝钗
// s1.replace() 方法执行后,返回的结果才是替换过的. // 注意对 s1 没有任何影响
String s11 = s1.replace("林黛玉", "薛宝钗");
System.out.println(s1);//宝玉 and 林黛玉 林黛玉 林黛玉
System.out.println(s11);//宝玉 and 薛宝钗 薛宝钗 薛宝钗

// 5.split 分割字符串, 对于某些分割字符,我们需要 转义比如 | \\等
String poem = "锄禾日当午,汗滴禾下土,谁知盘中餐,粒粒皆辛苦";
String[] split = poem.split(",");
poem = "E:\\aaa\\bbb";
split = poem.split("\\\\"); //分隔符为"\\"
System.out.println("==分割后内容===");
for (int i = 0; i < split.length; i++) {
System.out.println(split[i]);
}

// 6.toCharArray 转换成字符数组
s = "happy";
char[] chs = s.toCharArray();
for (int i = 0; i < chs.length; i++) {
System.out.println(chs[i]);
}

// 7.compareTo 比较两个字符串的大小,如果前者大,
// 则返回正数,后者大,则返回负数,如果相等,返回 0
// (1) 如果长度相同,并且每个字符也相同,就返回 0
// (2) 如果长度相同或者不相同,但是在进行比较时,可以区分大小
// 就返回 if (c1 != c2) {
// return c1 - c2;
// }
// (3) 如果前面的部分都相同,就返回 str1.len - str2.len
String a = "jack";
String b = "jack";
System.out.println(a.compareTo(b)); // 返回值是0

a = "jack";
b = "jcck";
System.out.println(a.compareTo(b)); // 返回值是'a'-'c'=-2
a = "jack";
b = "jckcc";
System.out.println(a.compareTo(b)); // 返回值是'a'-'c'=-2

a = "jack";
b = "jackabc";
System.out.println(a.compareTo(b)); // 返回值是str1.len - str2.len = -3

// 8.format 格式字符串
/* 占位符有:
* %s 字符串 %c 字符 %d 整型 %.2f 浮点型
*
*/
String name = "john";
int age = 10;
double score = 56.857;
char gender = '男';
//将所有的信息都拼接在一个字符串.
String info =
"我的姓名是" + name + "年龄是" + age + ",成绩是" + score + "性别是" + gender + "。希望大家喜欢我!";
System.out.println(info);
//1. %s , %d , %.2f %c 称为占位符
//2. 这些占位符由后面变量来替换
//3. %s 表示后面由 字符串来替换
//4. %d 是整数来替换
//5. %.2f 表示使用小数来替换,替换后,只会保留小数点两位, 并且进行四舍五入的处理
//6. %c 使用 char 类型来替换
String formatStr = "我的姓名是%s 年龄是%d,成绩是%.2f 性别是%c.希望大家喜欢我!";
String info2 = String.format(formatStr, name, age, score, gender);
System.out.println("info2=" + info2);

13.5 StringBuffer类

13.5.1 基本介绍

  • String类是保存字符串常量的。每次更新都需要重新开辟空间(new 对象),效率较低, 因此java设计者还提供了String Builder 和 StringBuffer 来增强String的功能并提高效率。
  • java.lang.StringBuffer代表可变的字符序列,可以对字符串内容进行增删
  • 很多方法与String相同,但StringBuffer是可变长度的
  • StringBuffer是一个容器
  • 继承关系图

    1. StringBuffer 的直接父类 是 AbstractStringBuilder
    2. StringBuffer 实现了 Serializable, 即 StringBuffer 的对象可以串行化
    3. 在父类中 AbstractStringBuilder 有属性 char[] value, 不是 final, 该value数组存放字符串内容,存放在堆中(而不是存放在常量池)
    4. StringBuffer 是一个 final 类,不能被继承
    5. 因为 StringBuffer字符内容是存放在 char[] value, 所以变化(增加/删除),不用每次都更换地址(即不是每次创建新对象), 所以效率高于 String

    image-20231026102141270

13.5.2 StingBuffer VS String (重要!)

  • Java中的String类真的不可变吗?java面试常见问题

  • String通过 private final char value[]属性 保存的是字符串常量,保存在常量池

    • value是private的,且String内部没有提供setValue方法修改value数组里面的内容,即从String类外部无法修改value数组里面的内容(不代表不能修改)【value内容的不可变性】;
    • value是final的,所以value不能重新指向新的字符串常量【value地址的不可变性】
    • 因为有这两个不可变性,我们对String的更新既不能通过改变value的内容实现,也不能通过改变value的地址实现。每次String类的更新,实际上是在堆中new了一个新的String对象,这个对象的value属性指向了常量池中新的字符串常量,因此效率较低 。
  • StringBuffer通过 char[] value属性 保存的是字符串变量,保存在中。

    • value数组里面的内容可以修改,value的地址也可以修改。
    • StringBuffer的更新实际上可以修改内容,不用每次修改地址(除非数组要扩容),因此效率较高。
1
2
3
4
5
final String a = "avd";
//a = "ac"; 报错 final修饰的引用类型不能修改其地址(引用类型赋值是赋地址)
final char[] b = new char[]{'a','v','d'};
b[1] = 'c'; // final修饰的char数组内容可以修改
// b = new char[]{'a','c'}; 报错 // final修饰的char数组地址不可以修改

13.5.3 StringBuffer与String相互转化

  • StringBuffer构造器

    1
    2
    3
    4
    5
    6
    //StringBuffer构造器
    //1. 创建一个大小为 16 的char[] value,用于存放字符内容
    StringBuffer stringBuffer = new StringBuffer();
    //2. 通过构造器指定char[] value的大小
    stringBuffer = new StringBuffer(100);
    //3. 通过给一个String创建StringBuffer,char[] value大小是str.length()+16
  • String–>StringBuffer

    1
    2
    3
    4
    5
    6
    7
    // String --> StringBuffer
    // 1. 通过构造器
    String str = "hello";
    StringBuffer stringBuffer1 = new StringBuffer(str);
    // 2.通过append
    StringBuffer stringBuffer2 = new StringBuffer();
    stringBuffer2.append(str);
  • StringBuffer–>String

    1
    2
    3
    4
    5
    6
    //StringBuffer ->String
    StringBuffer stringBuffer3 = new StringBuffer("hello,world");
    //1.使用构造器来搞定
    String s = new String(stringBuffer3);
    //2. 使用 StringBuffer 提供的 toString 方法
    String s1 = stringBuffer3.toString();

13.5.4 StringBuffer常用方法

    • s.append(string)
    • s.delete(start, end),删除索引在区间 [start,end) 处的字符
    • s.replace(start, end, string),替换区间[start,end) 处的字符
    • s.indexOf(string),查找指定的子串在字符串第一次出现的索引,如果找不到返回-1
    • s.insert(index, string),在指定索引index前插入
  • 长度
    • s.length()
  • 注意!!String类型的修改会返回一个新的String对象,不会影响原String对象,因此需要一个新的引用或者将原引用指向这个新的String对象。StringBuffer的修改会直接影响到原StringBuffer对象,原引用即指向修改后的StringBuffer对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
StringBuffer s = new StringBuffer("hello");
//增
s.append(',');// "hello,"
s.append("张三丰");//"hello,张三丰"
s.append("赵敏").append(100).append(true).append(10.5);//"hello,张三丰赵敏 100true10.5"
System.out.println(s);//"hello,张三丰赵敏 100true10.5"

//删
/*
* 删除索引为>=start && <end 处的字符
* 解读: 删除 11~14 的字符 [11, 14)
*/
s.delete(11, 14);
System.out.println(s);//"hello,张三丰赵敏 true10.5"

//改
//老韩解读,使用 周芷若 替换 索引 9-11 的字符 [9,11)
s.replace(9, 11, "周芷若");
System.out.println(s);//"hello,张三丰周芷若 true10.5"

//查
//查找指定的子串在字符串第一次出现的索引,如果找不到返回-1
int indexOf = s.indexOf("张三丰");
System.out.println(indexOf);//6

//插
//老韩解读,在索引为 9 的位置插入 "赵敏",原来索引为 9 的内容自动后移
s.insert(9, "赵敏");
System.out.println(s);//"hello,张三丰赵敏周芷若 true10.5"

//长度
System.out.println(s.length());//22
System.out.println(s);

13.5.5 测试题

  • Exercise01 区分空指针 和 空字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    String str = null;  // str是个空指针,没有赋值(地址)
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(str); ////需要看源码,底层调用的是AbstractStringBuilder的appendNull
    System.out.println(stringBuffer.length()); // 4
    System.out.println(stringBuffer); // null

    //下面的构造器,会抛出NullpointerException
    try {
    StringBuffer stringBuffer1 = new StringBuffer(str); // 看底层源码super(str.length()+16);
    } catch (Exception e) {
    throw new RuntimeException(e);
    }

    String str1 = ""; //str1不是空指针,指向看内存中一个空字符串
    StringBuffer stringBuffer2 = new StringBuffer();
    stringBuffer.append(str1); ////需要看源码,底层调用的是AbstractStringBuilder的appendNull
    System.out.println(stringBuffer2.length()); // 0
    System.out.println(stringBuffer2); // 无输出

    //下面的构造器,不会报错
    StringBuffer stringBuffer3 = new StringBuffer(str1); // 看底层源码super(str.length()+16);
    System.out.println(stringBuffer2.length()); // 0
    System.out.println(stringBuffer3); //无输出
  • Exercise02 将价格格式化

    1
    2
    3
    4
    5
    6
    7
    8
    String price = "123443545123.57";
    StringBuffer sb = new StringBuffer(price);

    for (int i = sb.lastIndexOf(".") - 3; i > 0; i -= 3) {
    sb.insert(i, ",");
    }
    System.out.println(sb);//123,443,545,123.57

13.6 StringBuilder

13.6.1 基本介绍

  • 一个可变的字符序列。此类提供一个与 StringBuffer兼容的API, 但不保证同步(StringBuilder 不是线程安全)。该类被设计用作 StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候。如果可能,建议优先采用该类因为在大多数实现中,它比 StringBuffer 要快[后面测]。
  • 在StringBuilder上的主要操作是 append和insert方法,可重载这些方法,以接受任意类型的数据。
  • 继承关系图

    • StringBuilder 继承 AbstractStringBuilder 类
    • 实现了 Serializable ,说明 StringBuilder 对象是可以串行化(对象可以网络传输,可以保存到文件)
    • StringBuilder 是 final 类, 不能被继承
    • StringBuilder 对象字符序列仍然是存放在其父类AbstractStringBuilder 的 char[] value; 因此,字符序列是堆中
    • StringBuilder 的方法,没有做互斥的处理,即没有 synchronized 关键字,因此在单线程的情况下使用

    image-20231028145818607

    image-20231028150627951

13.6.2 String StringBuffer StringBuilder比较(重要!)

  • 字符串生成器和StringBuffer非常类似,均代表可变的字符序列,而且方法也一样

  • 字符串:不可变字符序列(final char value[]),效率低,但是复用率高.

  • StringBuffer:可变字符序列(char[] value)、效率较高(增删)、线程安全,看源码

  • StringBuilder:可变字符序列、效率最高、线程不安全

  • 如何选择

    image-20231028151831369

13.6.3 效率测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
long startTime = 0L;
long endTime = 0L;
StringBuffer buffer = new StringBuffer("");
startTime = System.currentTimeMillis();
for (int i = 0; i < 80000; i++) {//StringBuffer 拼接 20000 次
buffer.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer 的执行时间:" + (endTime - startTime));

StringBuilder builder = new StringBuilder("");
startTime = System.currentTimeMillis();
for (int i = 0; i < 80000; i++) {//StringBuilder 拼接 20000 次
builder.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder 的执行时间:" + (endTime - startTime));

String text = "";
startTime = System.currentTimeMillis();
for (int i = 0; i < 80000; i++) {//String 拼接 20000
text = text + i;
}
endTime = System.currentTimeMillis();
System.out.println("String 的执行时间:" + (endTime - startTime));

输出:
StringBuffer 的执行时间:6
StringBuilder 的执行时间:4
String 的执行时间:5644

13.7 Math类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//看看 Math 常用的方法(静态方法)
//1.abs 绝对值
int abs = Math.abs(-9);
System.out.println(abs);//9

//2.pow 求幂
double pow = Math.pow(2, 4);//2 的 4 次方
System.out.println(pow);//16

//3.ceil 向上取整,返回>=该参数的最小整数(转成 double);
double ceil = Math.ceil(3.9);
System.out.println(ceil);//4.0

//4.floor 向下取整,返回<=该参数的最大整数(转成 double)
double floor = Math.floor(4.001);
System.out.println(floor);//4.0

//5.round 四舍五入 Math.floor(该参数+0.5)
long round = Math.round(5.51);
System.out.println(round);//6

// 6.sqrt 求开方
double sqrt = Math.sqrt(9.0);
System.out.println(sqrt);//3.0

//7.random 求随机数
// random 返回的是 0 <= x < 1 之间的一个随机小数
// 思考:请写出获取 a-b 之间的一个随机整数,a,b 均为整数 ,比如 a = 2, b=7
// 即返回一个数 x 2 <= x <= 7
// Math.random() * (b-a) 返回的就是 0 <= 数 <= b-a
// (1) (int)(a) <= x <= (int)(a + Math.random() * (b-a +1) )
// (2) 使用具体的数给小伙伴介绍 a = 2 b = 7
// (int)(a + Math.random() * (b-a +1) ) = (int)( 2 + Math.random()*6)
// Math.random()*6 返回的是 0 <= x < 6 小数
// 2 + Math.random()*6 返回的就是 2 <= x < 8 小数
// (int)(2 + Math.random()*6) = 2 <= x <= 7 整数
// (3) 公式就是 (int)(a + Math.random() * (b-a +1) )
for(int i = 0; i < 100; i++) {
System.out.println((int)(2 + Math.random() * (7 - 2 + 1)));
}

//8. max , min 返回最大值和最小值
int min = Math.min(1, 9);
int max = Math.max(45, 90);
System.out.println("min=" + min);
System.out.println("max=" + max);

13.8 Arrays类

13.8.1 常用方法

  • toString:返回数组的字符串形式

    1
    2
    Integer[] integers = {1, 23, 44};
    System.out.println(Arrays.toString(integers)); // [1, 23, 44]
  • sort:默认排序或定制排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    //2. sort:默认排序和定制排序
    // 数组是引用类型,排序后会直接印象实参
    // 重载的sort,也可以通过传入一个接口 Comparator 实现定制排序
    // 调用 定制排序 时,传入两个参数
    // (1) 排序的数组 arr
    // (2) 实现了 Comparator 接口的匿名内部类 , 要求实现 compare 方法
    Integer[] arr = {1, -1, 3, 7, 88, 20};
    Arrays.sort(arr); //默认排序(从小到大)
    System.out.println(Arrays.toString(arr));

    // 定制排序
    Arrays.sort(arr, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
    return o2 - o1;
    }
    });
    System.out.println(Arrays.toString(arr));
    // 源码分析
    //(1) Arrays.sort(arr, new Comparator(){});
    //(2) 最终到 TimSort 类的 private static <T> void binarySort(T[] a, int lo, int hi, int start, Comparator<? super T> c)()
    //(3) 执行到 binarySort 方法的代码, 会根据动态绑定机制 c.compare()执行我们传入的匿名内部类的 compare ()
    // while (left < right) {
    // int mid = (left + right) >>> 1;
    // if (c.compare(pivot, a[mid]) < 0)
    // right = mid;
    // else
    // left = mid + 1;
    // }
    //(4) new Comparator() {
    // @Override
    // public int compare(Object o1, Object o2) {
    // Integer i1 = (Integer) o1;
    // Integer i2 = (Integer) o2;
    // return i2 - i1;
    // }
    //
    //(5) public int compare(Object o1, Object o2) 返回的值>0 还是 <0
    // 会影响整个排序结果, 这就充分体现了 接口编程+动态绑定+匿名内部类的综合使用
    // 将来的底层框架和源码的使用方式,会非常常见
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    //定制冒泡排序
    public class ArraySortCustom {
    public static void main(String[] args) {
    int[] arr = {1, -1, 8, 0, 20};

    bubble(arr);
    System.out.println(Arrays.toString(arr));

    bubble(arr, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
    int i1 = (Integer)o1;
    int i2 = (Integer)o2;
    return i2 - i1;
    }
    });
    System.out.println(Arrays.toString(arr));
    }

    public static void bubble(int[] arr) {
    int temp = 0;
    for (int i = 0; i < arr.length - 1; i++) {
    for (int j = 0; j < arr.length - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    }
    }

    public static void bubble(int[] arr, Comparator c) {
    int temp = 0;
    for (int i = 0; i < arr.length - 1; i++) {
    for (int j = 0; j < arr.length - 1 - i; j++) {
    // 排序规则由c.compare(arr[j], arr[j + 1])返回值决定
    if (c.compare(arr[j], arr[j + 1]) > 0) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    }
    }
    }
  • binarySearch:二分查找

    1
    2
    3
    4
    5
    6
    7
    8
    Integer[] arr = {1, 2, 90, 123, 567};
    // binarySearch 通过二分搜索法进行查找,要求必须排好
    //1. 使用 binarySearch 二叉查找
    //2. 要求该数组是有序的. 如果该数组是无序的,不能使用 binarySearch
    //3. 如果数组中不存在该元素,就返回 return -(low + 1); // key not found.
    // low是指元素应该插入的位置
    int index = Arrays.binarySearch(arr, 100);
    System.out.println("index=" + index); // -4
  • copyOf:数组拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    // copyOf 数组拷贝
    //1. 从 arr 数组中,拷贝 length 个元素到 newArr 数组中
    //2. 如果拷贝的长度length > oldArr.length 就在新数组的后面 增加 null
    //3. 如果拷贝长度length < 0 就抛出异常 NegativeArraySizeException
    //4. 该方法的底层使用的是 System.arraycopy()
    Integer[] newArr = Arrays.copyOf(oldArr, length);
    System.out.println("==拷贝执行完毕后==");
    System.out.println(Arrays.toString(newArr));
  • fill:数组填充

    1
    2
    3
    4
    5
    6
    //fill 数组元素的填充
    Integer[] num = new Integer[]{9,3,2};
    //1. 使用 99 去填充 num 数组,可以理解成是替换原理的元素
    Arrays.fill(num, 99);
    System.out.println("==num 数组填充后==");
    System.out.println(Arrays.toString(num));
  • equals:数组比对

    1
    2
    3
    4
    5
    6
    //equals 比较两个数组元素内容是否完全一致
    Integer[] arr2 = {1, 2, 90, 123};
    //1. 如果 arr 和 arr2 数组的元素一样,则方法 true;
    //2. 如果不是完全一样,就返回 false
    boolean equals = Arrays.equals(arr, arr2);
    System.out.println("equals=" + equals);
  • asList:将一组数据转换成 list

    1
    2
    3
    4
    5
    6
    7
    8
    //1. asList 方法,会将 (2,3,4,5,6,1)数据转成一个 List 集合
    //2. 返回的 asList编译类型:List(是接口)
    //3. 返回的 asList运行类型:java.util.Arrays#ArrayList,
    // ArrayList是 Arrays 类的静态内部类 private static class ArrayList<E> extends AbstractList<E>
    // implements RandomAccess, java.io.Serializable
    List asList = Arrays.asList(2,3,4,5,6,1);
    System.out.println("asList=" + asList);
    System.out.println("asList 的运行类型" + asList.getClass());

13.8.2 练习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 定制排序
// 1.实现自定义book类数组按price从小到大排序
// 2.实现自定义book类数组按price从大到小排序
// 3.实现自定义book类数组按name的长度从小到大排序

public class ArrayExercise {
public static void main(String[] args) {
Book[] books = new Book[4];
books[0] = new Book("红楼梦", 100);
books[1] = new Book("金瓶梅新", 90);
books[2] = new Book("青年文摘 20 年", 5);
books[3] = new Book("java 从入门到放弃~", 300);

// 按price从小到大排序
Arrays.sort(books, new Comparator<Book>() {
@Override
public int compare(Book o1, Book o2) {
double priceVal = o1.getPrice() - o2.getPrice();
if (priceVal > 0) {
return 1;
} else if (priceVal < 0) {
return -1;
} else {
return 0;
}
}
});
System.out.println(Arrays.toString(books));

// 按price从大到小排序
Arrays.sort(books, new Comparator<Book>() {
@Override
public int compare(Book o1, Book o2) {
double priceVal = o1.getPrice() - o2.getPrice();
if (priceVal > 0) {
return 1;
} else if (priceVal < 0) {
return -1;
} else {
return 0;
}
}
});
System.out.println(Arrays.toString(books));

// 按照name长度从小到大排序
Arrays.sort(books, new Comparator<Book>() {
@Override
public int compare(Book o1, Book o2) {
double nameVal = o1.getName().length() - o2.getName().length();
if (nameVal > 0) {
return -1;
} else if (nameVal < 0) {
return 1;
} else {
return 0;
}
}
});
System.out.println(Arrays.toString(books));
}
}

class Book {
private String name;
private double price;

public Book(String name, double price) {
this.name = name;
this.price = price;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}

13.9 System类

image-20231030182047674

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//exit 退出当前程序
System.out.println("ok1");
//1. exit(0) 表示程序退出
//2. 0 表示一个状态 , 正常的状态
System.exit(0);

//arraycopy :复制数组元素,比较适合底层调用,
// 一般使用 Arrays.copyOf 完成复制数组
int[] src={1,2,3};
int[] dest = new int[3];// dest 当前是 {0,0,0}
//1. 主要是搞清楚这五个参数的含义
// * @param src 源数组
// * @param srcPos 从源数组的哪个索引位置开始拷贝
// * @param dest 目标数组,即把源数组的数据拷贝到哪个数组
// * @param destPos 把源数组的数据拷贝到 目标数组的哪个索引
// * @param length 从源数组拷贝多少个数据到目标数组
System.arraycopy(src, 0, dest, 1, 2);
// int[] src={1,2,3};
System.out.println("dest=" + Arrays.toString(dest));//[0, 1, 2]

//currentTimeMillens:返回当前时间距离 1970-1-1 的毫秒数
System.out.println(System.currentTimeMillis());

13.10 BigInteger 和 BigDecimal 类

  • 应用场景:

    • Biglnteger适合保存比较大的整型
    • BigDecimal适合保存精度更高的浮点型(小数)
    1
    2
    3
    4
    5
    6
    7
    // 当我们编程中,需要处理很大的整数,long 不够用
    // 可以使用 BigInteger 的类来搞定
    //long l = 23788888899999999999999999999l;
    //System.out.println("l=" + l);

    BigInteger bigInteger = new BigInteger("23788888899999999999999999999");
    System.out.println(bigInteger);
    1
    2
    3
    4
    5
    6
    7
    // 当我们需要保存一个精度很高的数时,double 不够用
    // 可以用 BigDecimal
    //double d = 1999.11111111111999999999999977788d;
    //System.out.println(d); //会缩减精度
    BigDecimal bigDecimal = new BigDecimal("1999.11");
    BigDecimal bigDecimal2 = new BigDecimal("3");
    System.out.println(bigDecimal);
  • 加、减、乘、除操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //1. 在对 BigInteger 进行加减乘除的时候,需要使用对应的方法,不能直接进行 + - * /
    //2. 可以创建一个 要操作的 BigInteger 然后进行相应操作
    BigInteger add = bigInteger.add(bigInteger2);
    System.out.println(add);//加
    BigInteger subtract = bigInteger.subtract(bigInteger2);
    System.out.println(subtract);//减
    BigInteger multiply = bigInteger.multiply(bigInteger2);
    System.out.println(multiply);//乘
    BigInteger divide = bigInteger.divide(bigInteger2);
    System.out.println(divide);//除
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //1. 如果对 BigDecimal 进行运算,比如加减乘除,需要使用对应的方法
    //2. 创建一个需要操作的 BigDecimal 然后调用相应的方法即可
    System.out.println(bigDecimal.add(bigDecimal2));
    System.out.println(bigDecimal.subtract(bigDecimal2));
    System.out.println(bigDecimal.multiply(bigDecimal2));
    //System.out.println(bigDecimal.divide(bigDecimal2));//可能抛出异常ArithmeticException,无限循环小数
    //在调用 divide 方法时,指定精度即可. BigDecimal.ROUND_CEILING
    //如果有无限循环小数,就会保留 和“被除数”一致的 精度
    System.out.println(bigDecimal.divide(bigDecimal2, BigDecimal.ROUND_CEILING));

13.11 日期类

13.11.1 第一代日期类 Date

image-20231031093802564

  • 获取日期

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //1. 获取当前系统时间
    //2. 这里的 Date 类是在 java.util 包的
    //3. 默认输出的日期格式是国外的方式, 因此通常需要对格式进行转换
    Date d1 = new Date(); //获取当前系统时间
    System.out.println("当前日期=" + d1);

    Date d2 = new Date(9234567); //通过指定毫秒数得到时间
    System.out.println("d2=" + d2);

    System.out.println(d1.getTime()); //获取某个时间对应的毫秒数
  • Date转格式化String

    1
    2
    3
    4
    5
    //1. 创建 SimpleDateFormat 对象,将Date转换成格式化字符串
    //2. 这里的格式使用的字母是规定好,不能乱写
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E");
    String format = sdf.format(d1); // format:将日期转换成指定格式的字符串
    System.out.println("当前日期=" + format);
  • 格式化String转Date

    1
    2
    3
    4
    5
    6
    //1. 可以把一个格式化的 String 转成对应的 Date
    //2. 得到 Date 在输出时,仍然还是按照国外的形式,如果希望指定格式输出,还是需要转换
    //3. 在把 String -> Date , String 的格式 需要和 定义的sdf格式 一致,否则会抛出转换异常
    String s = "1996 年 01 月 01 日 10:20:30 星期一";
    Date parse = sdf.parse(s); //可能会报异常ParseException
    System.out.println("parse=" + sdf.format(parse));

13.11.2 第二代日期类 Calendar

image-20231031095901376

  • 获取Calendar对象

    1
    2
    3
    4
    5
    6
    7
    //1. Calendar 是一个抽象类, 并且构造器是 private
    //2. 可以通过 getInstance() 来获取实例
    //3. 提供大量的方法和字段提供给程序员
    //4. Calendar 没有提供对应的格式化的类,因此需要程序员自己组合来输出(灵活)
    //5. 如果我们需要按照 24 小时进制来获取时间, Calendar.HOUR ==改成=> Calendar.HOUR_OF_DAY
    Calendar c = Calendar.getInstance(); //创建日历类对象,比较简单,自由
    System.out.println("c=" + c); //会输出很多字段信息
  • 获取日历字段

    1
    2
    3
    4
    5
    6
    7
    8
    //获取日历对象的某个日历字段
    System.out.println("年:" + c.get(Calendar.YEAR));
    // 这里为什么要 + 1, 因为 Calendar 返回月时候,是按照 0 开始编号
    System.out.println("月:" + (c.get(Calendar.MONTH) + 1));
    System.out.println("日:" + c.get(Calendar.DAY_OF_MONTH));
    System.out.println("小时:" + c.get(Calendar.HOUR));
    System.out.println("分钟:" + c.get(Calendar.MINUTE));
    System.out.println("秒:" + c.get(Calendar.SECOND));
  • 格式化输出

    1
    2
    3
    4
    //Calender 没有专门的格式化方法,所以需要程序员自己来组合显示
    System.out.println(c.get(Calendar.YEAR) + "-" + (c.get(Calendar.MONTH) + 1) + "-" +
    c.get(Calendar.DAY_OF_MONTH) +
    " " + c.get(Calendar.HOUR_OF_DAY) + ":" + c.get(Calendar.MINUTE) + ":" + c.get(Calendar.SECOND) );

13.11.3 第三代日期类 LocalDateTime

image-20231031100935458

image-20231031101058708

  • 获取日期时间

    1
    2
    3
    4
    5
    //1. 使用 now() 返回表示当前日期时间的 对象
    LocalDateTime ldt = LocalDateTime.now();
    System.out.println(ldt);
    LocalDate now = LocalDate.now(); //可以获取年月日
    LocalTime now2 = LocalTime.now();//获取到时分秒
  • 格式化输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //2. 使用 DateTimeFormatter 对象来进行格式化
    // 创建 DateTimeFormatter 对象
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    String format = dateTimeFormatter.format(ldt);
    System.out.println("格式化的日期=" + format);

    // 也可以向Calendar一样输出各个字段,组合显示
    System.out.println("年=" + ldt.getYear());
    System.out.println("月=" + ldt.getMonth());
    System.out.println("月=" + ldt.getMonthValue());
    System.out.println("日=" + ldt.getDayOfMonth());
    System.out.println("时=" + ldt.getHour());
    System.out.println("分=" + ldt.getMinute());
    System.out.println("秒=" + ldt.getSecond());
  • 对当前时间进行加减

    1
    2
    3
    4
    5
    6
    7
    //提供 plus 和 minus 方法可以对当前时间进行加或者减
    //看看 890 天后,是什么时候 把 年月日-时分秒
    LocalDateTime ldt2 = ldt.plusDays(890);
    System.out.println("890 天后=" + dateTimeFormatter.format(ldt2));
    //看看在 3456 分钟前是什么时候,把 年月日-时分秒输出
    LocalDateTime ldt3 = ldt.minusMinutes(3456);
    System.out.println("3456 分钟前 日期=" + dateTimeFormatter.format(ldt3));

第十四章 集合

14.1 集合介绍

  • 数组的不足
    • 长度开始时必须指定,而且一旦指定,不能更改
    • 保存的必须为同一类型的元素
    • 使用数组进行增加/删除元素比较麻烦(需要创建一个新数组)
  • 集合的好处
    • 可以动态保存任意多个对象,使用比较方便!
    • 提供了一系列方便的操作对象的方法:add、remove、set、get等
    • 使用集合添加,删除新元素的代码-简洁明了

14.2 框架体系图(必背)

  1. 集合主要是两组(单列集合 , 双列集合)

  2. Collection 接口有两个重要的子接口 List和Set , 他们的实现子类都是单列集合

  3. Map 接口的实现子类 是双列集合,存放的是 K-V

  • Collection
    • List
      • Vector
      • ArrayList
      • LinkedList
    • Set
      • TreeSet
      • HashSet
  • Map
    • TreeMap
    • HashMap
      • LinkedHashMap
    • Hashtable
      • Properties

image-20231101093437236

image-20231101093444815

14.3 Collection接口

14.3.1 实现类的特点和常用方法

  1. 收集实现子类可以存放多个元素,每个元素可以是Object

  2. 有些Collection的实现类,可以存放重复的元素,有些不可以

  3. 有些的实现类,有些是有序的(List),有些是无序的(Set)

  4. 集合接口没有直接的实现子类,是通过它的子接口SetList来实现的

  • 常用方法:以ArrayList为例

    image-20231101095156361

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
List list = new ArrayList();
// 1.add:添加单个元素
list.add("jack");
list.add(10); //会自动装箱list.add(Integer.valueOf(10))
list.add(true);
System.out.println("list=" + list);

// 2.contains:查找元素是否存在
System.out.println(list.contains("jack"));

// 3.remove:删除指定元素
list.remove(0); //按索引删除
list.remove(Integer.valueOf(10)); //按元素删除
System.out.println("list=" + list);

// 4.size:获取元素个数
System.out.println(list.size());

// 5.isEmpty:判断集合是否为空
System.out.println(list.isEmpty());

// 6.clear:清空集合 (list.removeAll(list)与之等价)
list.clear();

// 7.addAll:添加多个元素
List list2 = new ArrayList();
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
System.out.println(list);

// 8.containAll:判断多个元素是否同时存在
System.out.println(list.containsAll(list2));

// 9.removeAll:删除多个元素
list.add("西游记");
list.removeAll(list2);
System.out.println("list=" + list);

14.3.2 遍历1 - Iterator迭代器

image-20231101101622869

image-20231101101905703

image-20231101102225179

  • 快捷键 itit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Collection col = new ArrayList();
col.add(new Book("三国演义", "罗贯中", 10.1));
col.add(new Book("小李飞刀", "古龙", 5.1));
col.add(new Book("红楼梦", "曹雪芹", 34.6));

System.out.println("col=" + col);
// 1.先得到col的Iterator对象
Iterator iterator = col.iterator();
// 2. 使用while循环遍历 快捷键 itit (ctrl+j可以查看所有快捷键)
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}

//3. 当退出 while 循环后 , 这时 iterator 迭代器,指向最后的元素
// iterator.next();//NoSuchElementException
//4. 如果希望再次遍历,需要重置我们的迭代器
iterator = col.iterator();
System.out.println("===第二次遍历===");
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}

14.3.2 遍历3 - for循环增强

image-20231101103127471

  • 快捷键:I
1
2
3
4
5
6
7
8
9
10
11
12
13
Collection col = new ArrayList();
col.add(new Book("三国演义", "罗贯中", 10.1));
col.add(new Book("小李飞刀", "古龙", 5.1));
col.add(new Book("红楼梦", "曹雪芹", 34.6));

// 使用增强for循环遍历集合
// 底层仍然使用的是Iterator,debug看源码
// 快捷键 I
for(Object obj : col) {
System.out.println("obj=" + obj);
}

// 增强for循环也可以遍历数组

14.4 List接口

14.4.1 基本介绍

image-20231101104403257

1
2
3
4
5
6
7
8
9
10
11
//1. List 集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
List list = new ArrayList();
list.add("jack");
list.add("tom");
list.add("mary");
list.add("hsp");
list.add("tom");
System.out.println("list=" + list);
//2. List 集合中的每个元素都有其对应的顺序索引,即支持索引
// 使用get()方法获取元素,索引是从 0 开始的
System.out.println(list.get(3));//hsp

14.4.2 常用方法

  1. add
  2. addAll
  3. get
  4. indexOf
  5. lastIndexOf
  6. remove
  7. set:替换或设置
  8. subList:子集合或切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
List list = new ArrayList();
list.add("张三丰");
list.add("贾宝玉");

// 1.void add(int index, Object ele):在 index 位置插入 ele 元素
//在 index = 1 的位置插入一个对象
list.add(1, "韩顺平");
System.out.println("list=" + list);

// 2.boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
List list2 = new ArrayList();
list2.add("jack");
list2.add("tom");
list.addAll(1, list2);
System.out.println("list=" + list);

// 3.Object get(int index):获取指定 index 位置的元素
System.out.println(list.get(0));

// 4.int indexOf(Object obj):返回 obj 在集合中首次出现的位置
System.out.println(list.indexOf("tom"));//2

// 5.int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
list.add("韩顺平");
System.out.println("list=" + list);
System.out.println(list.lastIndexOf("韩顺平"));

// 6.Object remove(int index):移除指定 index 位置的元素,并返回此元素
list.remove(0);
System.out.println("list=" + list);

// 7.Object set(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换. list.set(1, "玛丽");
System.out.println("list=" + list);

// 8.List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合.
// 注意返回的子集合 fromIndex <= subList < toIndex
List returnlist = list.subList(0, 2);
System.out.println("returnlist=" + returnlist);

14.3.3 遍历方式

  • Iterator迭代器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ArrayList list = new ArrayList();
    list.add(new Book("三国演义", "罗贯中", 10.1));
    list.add(new Book("小李飞刀", "古龙", 5.1));
    list.add(new Book("红楼梦", "曹雪芹", 34.6));

    System.out.println("list=" + list);

    // 1.先得到list的Iterator对象
    Iterator iterator = list.iterator();
    // 2. 使用while循环遍历 快捷键 itit (ctrl+j可以查看所有快捷键)
    while (iterator.hasNext()) {
    Object obj = iterator.next();
    System.out.println("obj=" + obj);
    }
  • 增强for循环

    1
    2
    3
    4
    5
    6
    // 使用增强for循环遍历集合
    // 底层仍然使用的是Iterator,debug看源码
    // 快捷键 I
    for(Object obj : list) {
    System.out.println("obj=" + obj);
    }
  • 普通for循环

    1
    2
    3
    for(int i = 0; i < list.size(); i++) {
    System.out.println("obj=" + list.get(i))
    }

14.5 List - ArrayList

14.5.1 注意事项

  • ArrayList允许所有元素,包括null;ArrayList可以加入null、并且多个
  • ArrayList是由数组来实现数据存储的
  • ArrayList基本等同于Vector,除了ArrayList是线程不安全(执行效率高)看源码。在多线程情况下,不建议使用数组列表

14.5.2 底层操作机制(重要!)

  1. ArrayList中维护了一个**Object类型的数组elementData. [debug 看源码]。transient Object[] elementData; //transient表示瞬间,短暂的,表示该属性不会被序列号**
  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为**0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5**倍(即第一次扩容为15,第二次22……)。
  3. 如果使用的是指定大小的构造器,则初始elementData容量为**指定大小,如果需要扩容,则直接扩容elementData为1.5**倍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用无参构造器创建 ArrayList 对象
//ArrayList list = new ArrayList();
// 使用有参构造器创建 ArrayList 对象
ArrayList list = new ArrayList(8);

// 使用 for 给 list 集合添加 1-10个 数据
for (int i = 1; i <= 10; i++) {
list.add(i);
}
// 使用 for 给 list 集合添加 11-15 数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100);
list.add(200);
list.add(null);

image-20231102155326382

image-20231102155512644

image-20231102155550922

14.6 List - Vector

14.6.1 注意事项

image-20231103100547672

14.6.2 底层操作机制(重要!)

  1. Vector中维护了一个**Object类型的数组elementData. 。protected Object[] elementData**;
  2. 当创建Vector对象时,如果使用的是无参构造器,则初始elementData容量为**10(调用有参构造器super(10)),之后每次扩容为原容量的**倍,即20, 40, 80…
  3. 如果使用的是指定大小的构造器,则初始elementData容量为**指定大小,如果需要扩容,则扩容elementData为原容量的**倍。

image-20231103102804447

image-20231103103955275image-20231103103955355

14.6.3 Vector 和 ArrayList 的比较

底层结构 版本 线程安全(同步)效率 扩容倍数
ArrayList 可变数组 transient Object[] elementData jdk1.2 不安全,效率高 (1)无参构造器:默认容量0,第一次扩容为10,后面每次扩容为1.5倍;(2)有参构造器:1.5倍
Vector 可变数组 protected Object[] elementData jdk1.0 安全,效率低 (1)无参构造器:默认容量为10,后面每次扩容为2倍;(2)有参构造器:2倍

14.7 List - LinkedList

14.7.1 LinkedList 的全面说明

image-20231103105431067

14.7.2 底层操作机制

  1. LinkedList底层维护了一个**双向链表**
  2. LinedList中维护了两个属性 first last,分别只想首节点和为节点
  3. 每个节点(Node)里面又维护了 pre、next、item三个属性。pre指向前一个、next指向后一个节点,item存储数据
  4. 所以LinkedList元素的添加和删除不是通过数组完成,相对来说效率较高

image-20231103105629283

  • 模拟一个简单的双向链表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    public class LinkedList01 {
    public static void main(String[] args) {
    //模拟一个简单的双向链表
    Node jack = new Node("jack");
    Node tom = new Node("tom");
    Node hsp = new Node("老韩");

    //连接三个结点,形成双向链表
    //jack -> tom -> hsp
    jack.next = tom;
    tom.next = hsp;
    //hsp -> tom -> jack
    hsp.pre = tom;
    tom.pre = jack;
    Node first = jack;//让 first 引用指向 jack,就是双向链表的头结点
    Node last = hsp; //让 last 引用指向 hsp,就是双向链表的尾结点

    //演示,从头到尾进行遍历
    System.out.println("===从头到尾进行遍历===");
    Node cur = first;
    while (true) {
    if(cur == null) {
    break;
    }
    //输出 first 信息
    System.out.println(cur);
    cur = cur.next;
    }

    //演示,从尾到头的遍历
    System.out.println("====从尾到头的遍历====");
    cur = last;
    while (true) {
    if(cur == null) {
    break;
    }
    //输出 last 信息
    System.out.println(cur);
    cur = cur.pre;
    }

    //演示 链表的添加对象/数据
    //要求,是在 tom 和 hsp 之间,插入一个对象 smith
    //1. 先创建一个 Node 结点,name 就是 smith
    Node smith = new Node("smith");
    //下面就把 smith 加入到双向链表了
    smith.next = hsp;
    smith.pre = tom;
    hsp.pre = smith;
    tom.next = smith;


    }
    }

    //定义一个 Node 类,Node对象 表示双向链表的一个结点
    class Node {
    public Object item; //真正存放数据
    public Node next; //指向后一个结点
    public Node pre; //指向前一个结点
    public Node(Object name) {
    this.item = name;
    }
    public String toString() {
    return "Node name=" + item;
    }
    }
  • LinkedList增删改查及其源码分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class LinkedListMethod {
    @SuppressWarnings("all")
    public static void main(String[] args) {
    // 增删改查及其源码分析
    LinkedList linkedList = new LinkedList();
    linkedList.add(1);
    linkedList.add(2);
    linkedList.add(3);
    linkedList.add(4);
    System.out.println("linkedlist=" + linkedList);


    linkedList.remove(); // 默认删除第一个节点
    linkedList.remove(1); //删除第二个节点
    System.out.println("linkedlist=" + linkedList);

    linkedList.set(1, 999);
    System.out.println("linkedlist=" + linkedList);

    linkedList.get(1);

    // 3种遍历
    Iterator iterator = linkedList.iterator();
    while (iterator.hasNext()) { // 快捷键itit
    Object next = iterator.next();
    System.out.println(next);
    }

    for (Object o : linkedList) { // 快捷键I
    System.out.println(o);
    }

    for (int i = 0; i < linkedList.size(); i++) {
    System.out.println(linkedList.get(i));
    }


    }
    }
  • image-20231105143229015

  • image-20231105143254333image-20231105143340454

  • image-20231105143408422

  • image-20231105143421789

14.7.3 ArrayList和Linked比较

底层结构 增删效率 改查效率
ArrayList 可变数组 较低(数组扩容,创建新数组,数组拷贝) 较高(顺序存储,通过索引直接定位目标)
LinkedList 双向链表 较高(通过链表追加和删除节点) 较低(非顺序存储,通过索引不能直接定位目标节点,需要顺着链表依次定位到目标节点)
  • 怎么选择ArrayList和LinkedList
    • 如果我们改查的操作多,选择ArrayList
    • 如果我们增删的操作多,选择LinkedList
    • 一般来说,在程序中,80%-90%的都是查询,因此大部分情况下会选择ArrayList
    • 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList。

14.8 Set接口

14.8.1 基本介绍

image-20231105150701433

14.8.2 常用方法

  • 和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 以HashSet为例,说明Set的方法
    //1. 以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法
    //2. set 接口的实现类的对象(Set 接口对象), 不能存放重复的元素, 可以添加一个 null
    //3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
    //4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是他的固定.
    Set set = new HashSet();
    set.add("john");
    set.add("lucy");
    set.add("john");//重复
    set.add("jack");
    set.add("hsp");
    set.add("mary");
    set.add(null);//
    set.add(null);//再次添加 null
    System.out.println("set=" + set);

    // 其他方法参考Collection
    输出:
    set=[null, hsp, mary, john, lucy, jack]

14.8.3 遍历方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//遍历
//方式 1: 使用迭代器
System.out.println("=====使用迭代器====");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}

//方式 2: 增强 for
System.out.println("=====增强 for====");
for (Object o : set) {
System.out.println("o=" + o);
}

//set 接口对象,不能通过索引来获取
//所以不能通过普通for循环遍历

14.9 Set - HashSet

14.9.1 基本说明

image-20231105152549978

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HashSet set = new HashSet();
/*
public HashSet() {
map = new HashMap<>();
}
*/
set.add(null);
set.add(null); // 会返回false表示添加失败
System.out.println("set=" + set);

//1. 在执行 add 方法后,会返回一个 boolean 值
//2. 如果添加成功,返回 true, 否则返回 false
//3. 可以通过 remove 指定删除哪个对象
System.out.println(set.add("john"));//T
System.out.println(set.add("lucy"));//T
System.out.println(set.add("john"));//F
System.out.println(set.add("jack"));//T
System.out.println(set.add("Rose"));//T
System.out.println("set=" + set);
// 输出顺序:set=[null, Rose, john, lucy, jack]
  • 怎么理解**不能添加重复的元素或对象**

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    HashSet set = new HashSet();
    set.add("lucy");//添加成功T
    set.add("lucy");//加入不了F
    set.add(new Dog("tom"));//T
    set.add(new Dog("tom"));//T(是两个不同的对象)
    System.out.println("set=" + set);

    set.add(new String("hsp"));//T
    set.add(new String("hsp"));//F 添加失败,为什么?看add源码(调用equals进行比较)
    System.out.println("set=" + set);

14.9.2 HashSet底层机制

  • HashSet底层是**HashMap**,底层原理是 数组 + 链表 + 红黑树

  • HashSet只使用到了HashMap中的Key,value是一个常量PRESENT。

  • 模拟一个简单的 数组+链表结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class HashSetStructure {
    @SuppressWarnings("all")
    public static void main(String[] args) {
    // 模拟HashSet底层的 数组+链表结构
    // 1.创建数组(也叫作表)
    Node[] table = new Node[16];
    // 2.创建节点
    Node john = new Node("john", null);
    table[2] = john;
    Node jack = new Node("jack", null);
    john.next = jack; //挂载到john节点之后
    Node rose = new Node("rose", null);
    jack.next = rose;

    Node lucy = new Node("lucy", null);
    table[3] = lucy;

    }
    }
    class Node {
    Object item; //存放数据
    Node next; //指向下一个节点

    public Node(String john, Object o) {
    item = john;
    next = (Node) o;
    }
    }
  1. HashSet底层是HashMap
  2. 添加一个元素时,先得到**hash值-会转成->索引值**
  3. 找到存储数据表table,看这个索引位置是否已经存放的有元素
    • 如果没有,直接加入
    • 如果有,调用 equals 比较。如果相同,就放弃添加;如果不相同,则添加到最后
  4. 在Java 8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=
    MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
1
2
3
4
5
HashSet hashSet = new HashSet();
hashSet.add("java");//到此位置,第 1 次 add 分析完毕.
hashSet.add("php");//到此位置,第 2 次 add 分析完毕
hashSet.add("java");
System.out.println("set=" + hashSet);
image-20231108123806622

image-20231108123834886

image-20231108123910809

image-20231108123936587

image-20231108124038555

image-20231108124055848

image-20231108124106708

  • 扩容和转化成红黑树机制
  1. HashSet底层是HashMap,第一次添加时,table数组扩容到**16,临界值(threshold)是12** 【16 * 加载因子
    (loadFactor)0.75 = 12】
  2. 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 =24,依次类推
  3. 在Java8中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8).并且table的大小>=MIN TREEIFY CAPACITY(默认64),就会进行**树化**(红黑树),否则仍然采用数组扩容机制

【小技巧】怎么让不同对象挂载到table的统一条列表。重写类的hashCode方法使其返回同一个固定值。

1
2
3
4
HashSet hashSet = new HashSet();
for (int i = 1; i <= 100; i++) {
hashSet.add(i);//1,2,3,4,5...100
}

image-20231108125936570

1
2
3
for (int i = 1; i <= 12; i++) {
hashSet.add(new A(i));
}

image-20231108130926446

image-20231108131551385

image-20231108131821816

14.9.3 HashSet练习题

  • 定义一个Employee类,该类包含:私有成员属性名称,年龄要求;
    1.创建3个雇员对象放入HashSet中
    2.当名称和Aage的值相同时,认为是相同员工,不能添加到HashSet集合中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Employee {
    private String name;
    private int age;

    public Employee(String name, int age) {
    this.name = name;
    this.age = age;
    }

    @Override
    public String toString() {
    return "Employee{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }

    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Employee employee = (Employee) o;
    return age == employee.age && Objects.equals(name, employee.name); //当name和age相同时会返回true
    }

    @Override
    public int hashCode() {
    return Objects.hash(name, age); //name和age(哈希值)相同会返回相同哈希值
    }
    }
  • 定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中 birthday 为MyDate类型(属性包括:year, month, day),要求:
    1.创建3个Employee放入HashSet中
    2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    class Employee2 {
    private String name;
    private double sal;
    private MyDate birthday;

    public Employee2(String name, double sal, MyDate birthday) {
    this.name = name;
    this.sal = sal;
    this.birthday = birthday;
    }

    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Employee2 employee2 = (Employee2) o;
    return Objects.equals(name, employee2.name) && Objects.equals(birthday, employee2.birthday);
    }

    @Override
    public int hashCode() {
    return Objects.hash(name, birthday);
    }
    }

    class MyDate {
    private int year;
    private int month;
    private int day;

    public MyDate(int year, int month, int day) {
    this.year = year;
    this.month = month;
    this.day = day;
    }

    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    MyDate myDate = (MyDate) o;
    return year == myDate.year && month == myDate.month && day == myDate.day;
    }

    @Override
    public int hashCode() {
    return Objects.hash(year, month, day); // year,month,day的哈希值相同,会返回相同的哈希值
    }
    }
  • 看看Objects.hash()源码

    image-20231109093116457

image-20231109093157377

  • image-20231115103348306

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    说明:修改之后的p1对象和原p1对象的hash值不一致,所以p1.name="CC"以后,现在的p1对象用的是原p1对象的hash值索引位置,而不是它现在的hash值索引位置。
    当set.add(new Person(1001, "CC"))后,在该对象的hash值索引位置不会比较到相同元素(原因如上),所以可以添加成功
    当set.add(new Person(1001, "AA"))后,该对象会加在现p1对象链表的后面,因为hash值相同且和p1不equals
    */
    public class HashSetExercise3 {
    public static void main(String[] args) {
    HashSet set = new HashSet();
    Person p1 = new Person(1001, "AA");
    Person p2 = new Person(1002, "BB");
    set.add(p1);
    set.add(p2);
    System.out.println(set);
    //[Person{name='BB', age=1002}, Person{name='AA', age=1001}]

    p1.name = "CC";
    System.out.println(set);
    //[Person{name='BB', age=1002}, Person{name='CC', age=1001}]
    System.out.println(set.remove(p1)); //false

    set.add(new Person(1001, "CC"));
    System.out.println(set);
    //[Person{name='BB', age=1002}, Person{name='CC', age=1001}, Person{name='CC', age=1001}]

    set.add(new Person(1001, "AA"));
    System.out.println(set);
    //[Person{name='BB', age=1002}, Person{name='CC', age=1001}, Person{name='CC', age=1001}, Person{name='AA', age=1001}]

    }
    }

14.10 Set - LinkedHashSet

14.10.1 基本说明

image-20231109103359524

14.10.2 LinkedHashSet底层机制

image-20231109111130186

  • 节点是LinkedHashMap$Entry类型(继承了HashMap$Node),有自身的before和tail属性,同时也有父类的next属性
  • p.next = newNode(hash, key, value, null); 其实做了两件事:一是创建新节点(Entry类型),且将新节点 **linkNodeLast()**接到双向列表tail尾节点处;二是将p.next指向新节点
  • 疑问?afterNodeAccess(e);和afterNodeInsertion(evict);的作用是什么
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class LinkedHashSetSource {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Car("奥拓", 1000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//OK
linkedHashSet.add(new Car("法拉利", 10000000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//加入不了
linkedHashSet.add(new Car("保时捷", 70000000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//加入不了
System.out.println("linkedHashSet=" + linkedHashSet);

// 1. LinkedHashSet 加入顺序和取出元素顺序一致
// 2. LinkedHashSet 底层维护的是一个LinkedHashMap(HashMap的子类)
// 3. LinkedHashSet 底层结构 (数组table+双向链表)
// 4. 第一次添加时,直接将数组table扩容到16,
// 5. 数组table是HashMap$Node[]类型,存放的节点类型是LinkedHashMap$Entry(继承了HashMap$Node)
/* Entry继承关系是在内部类完成的
static class Entry<K,V> extends HashMap.Node<K,V> {
LinkedHashMap.Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Node<K,V> next) {
super(hash, key, value, next);
}
}*/

}
}

class Car {
private String name;
private double price;

public Car(String name, double price) {
this.name = name;
this.price = price;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

@Override
public String toString() {
return "\nCar{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}

//重写 equals 方法 和 hashCode
//当 name 和 price 相同时, 就返回相同的 hashCode 值, equals 返回 t
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Double.compare(car.price, price) == 0 &&
Objects.equals(name, car.name);
}

@Override
public int hashCode() {
return Objects.hash(name, price);
}
}

14.11 Map接口

14.11.1 基本介绍

image-20231113103555012

image-20231113104451042

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void main(String[] args) {
Map map = new HashMap();
map.put("no1", "韩顺平");//k-v
map.put("no2", "张无忌");//k-v

/*
1. k-v存储在 HashMap$Node node = new Node(hash,key,value,null)
2. k-v为了方便遍历,还会创建 entrySet 集合,该集合存放的元素类型是 Entry
而一个Entry对象有k,v 即 transient Set<Map.Entry<K,V>> entrySet;
3. entrySet中,定义的类型是Map.Entry,但实际存放的还是HashMap$Node
因为hashMap$Node 实现了Map.Entry接口,即static class Node<K,V> implements Map.Entry<K,V>{}
4. 当把hashMap$Node 对象存放到 entrySet 就方便我们遍历,因为 Map.Entry提供了两个重要的方法
K getKey(); V getValue();
*/

Set set = map.entrySet();
System.out.println(set.getClass()); //HashMap$EntrySet
for (Object obj : set) {
System.out.println(obj.getClass()); //HashMap$Node
// 为了从HashMap$Node中取出k-v
// 1.先向下转型
Map.Entry entry = (Map.Entry)obj;
System.out.println(entry.getKey() + "-" + entry.getValue());
}

Set set1 = map.keySet();
System.out.println(set1.getClass()); //HashMap$KeySet
Collection values = map.values();
System.out.println(values.getClass()); //HashMap$Values

}

14.11.2 常用方法

  1. put:添加k-v,若k存在则更新v
  2. remove:根据键删除映射关系
  3. get:根据键获取值
  4. size:获取元素个数
  5. isEmpty:判断个数是否为 0
  6. clear:清除 所有k-v
  7. containsKey:查找键是否存在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MapMethod {
public static void main(String[] args) {
//演示 map 接口常用方法
// put:添加k-v,若k存在则更新v
Map map = new HashMap();
map.put("邓超", new Object());//OK
map.put("邓超", "孙俪");//替换-> 一会分析源码
map.put("王宝强", "马蓉");//OK
map.put("宋喆", "马蓉");//OK
map.put("刘令博", null);//OK
map.put(null, "刘亦菲");//OK
map.put("鹿晗", "关晓彤");//OK
map.put("hsp", "hsp 的老婆");
System.out.println("map=" + map);

// remove:根据键删除映射关系
map.remove(null);
System.out.println("map=" + map);

// get:根据键获取值
Object val = map.get("鹿晗");
System.out.println("val=" + val);

// size:获取元素个数
System.out.println("k-v=" + map.size());

// isEmpty:判断个数是否为 0
System.out.println(map.isEmpty());//F

// clear:清除 k-v
//map.clear();
System.out.println("map=" + map);

// containsKey:查找键是否存在
System.out.println("结果=" + map.containsKey("hsp"));//T
}
}

14.11.3 遍历方式

  1. 第一组: 先取出 所有的 Key , 通过 Key 取出对应的 Value

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Set keyset = map.keySet();
    //(1) 增强 for
    System.out.println("-----第一种方式-------");
    for (Object key : keyset) {
    System.out.println(key + "-" + map.get(key));
    }
    //(2) 迭代器
    System.out.println("----第二种方式--------");
    Iterator iterator = keyset.iterator();
    while (iterator.hasNext()) {
    Object key = iterator.next();
    System.out.println(key + "-" + map.get(key));
    }
  2. 第二组: 把所有的 values 取出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Collection values = map.values();
    //这里可以使用所有的 Collections 使用的遍历方法
    //(1) 增强 for
    System.out.println("---取出所有的 value 增强 for----");
    for (Object value : values) {
    System.out.println(value);
    }
    //(2) 迭代器
    System.out.println("---取出所有的 value 迭代器----");
    Iterator iterator2 = values.iterator();
    while (iterator2.hasNext()) {
    Object value = iterator2.next();
    System.out.println(value);
    }
  3. 第三组: 通过 EntrySet 来获取 k-v

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Set entrySet = map.entrySet();// EntrySet<Map.Entry<K,V>>
    //(1) 增强 for
    System.out.println("----使用 EntrySet 的 for 增强(第 3 种)----");
    for (Object entry : entrySet) {
    //将 entry 转成 Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + "-" + m.getValue());
    }
    //(2) 迭代器
    System.out.println("----使用 EntrySet 的 迭代器(第 4 种)----");
    Iterator iterator3 = entrySet.iterator();
    while (iterator3.hasNext()) {
    Object entry = iterator3.next();
    //System.out.println(entry.getClass());//HashMap$Node -实现-> Map.Entry (getKey,getValue)
    //向下转型 Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + "-" + m.getValue());
    }

14.12 Map - HashMap

14.12.1 基本说明

image-20231113160440645

14.12.2 HashMap底层源码

image-20231113163611349

image-20231113163812898

  • put底层源码分析:具体流程与HashSet底层机制一样

  • 需要补充的一点:当Key相同时,会更新对应的Value

    image-20231113164722020

  • 扩容和树化机制

14.13 Map - Hashtable

14.13.1 基本介绍

image-20231114101200404

14.13.2 底层机制

  1. 底层有Entry[]类型的数组table 初始化大小为**11**
  2. 初始临界值 threshold 8 = 11 * 0.75
  3. 扩容机制:
    • 执行 addEntry(hash, key, value, index); 封装k-v到一个Entry
    • 当if (count >= threshold) 满足时就扩容,指向rehash()
    • 按照 int newCapacity = (oldCapacity << 1) + 1; 方式扩容

14.13.2 Hashtable VS HashMap

image-20231114101241707

14.14 Map - Properties

14.14.1 基本介绍

image-20231114103553427

Java读取Properties配置文件

14.15 开发中如何选择集合实现类(记住!)

image-20231114104350142

14.16 TreeSet 和 TreeMap

14.16.1 TreeSet

  • 注意:如果TreeSet使用的是无参构造器,即没有传入Comparator接口的匿名内部类。使用add(key)时,底层会调用key实现的Comparable接口进行比较,Comparable<? super K> k = (Comparable<? super K>) key; 如果Key没有实现Comparable接口,则会报错 ClassCastException
  • image-20231115102142671
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class TreeSet_ {
public static void main(String[] args) {
//1. 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的
//2. 希望添加的元素,按照字符串大小(或字符串长度)来排序
//3. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
// 并指定排序规则
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//下面调用 String 的 compareTo 方法进行字符串大小比较
//如果老韩要求加入的元素,按照长度大小排序
//return ((String) o2).compareTo((String) o1);
return ((String) o1).length() - ((String) o2).length();
}
});

// 添加数据.
treeSet.add("jack");
treeSet.add("tom");//3
treeSet.add("sp");
treeSet.add("a");
treeSet.add("abc");//3 比较规则是长度,已经存在长度为3的元素,"abc"添加不了
System.out.println("treeSet=" + treeSet);

//4. 简单看看源码
/*
1. 构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2. 在 调用 treeSet.add("tom"), 在底层会执行到
if (cpr != null) { //cpr 就是我们的匿名内部类(对象)
do {
parent = t;
//动态绑定到我们的匿名内部类(对象)compare
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果相等,即返回 0,这个 Key 就没有加入
return t.setValue(value);
} while (t != null);
}
*/
}
}

14.16.2 TreeMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class TreeMap_ {
public static void main(String[] args) {
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按照传入的 key(String) 的大小进行排序
//按照 Key(String) 的长度大小排序
//return ((String) o2).compareTo((String) o1);
return ((String) o2).length() - ((String) o1).length();
}
});
treeMap.put("jack", "杰克");
treeMap.put("tom", "汤姆");
treeMap.put("kristina", "克瑞斯提诺");
treeMap.put("smith", "斯密斯");
treeMap.put("hsp", "韩顺平");//加入不了,比较规则是Key的长度,已存在长度为3的Key
System.out.println("treemap=" + treeMap);

/* 源码解读
1. 构造器. 把传入的实现了 Comparator 接口的匿名内部类(对象),传给给 TreeMap 的 comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}

2. 调用 put 方法
2.1 第一次添加, 把 k-v 封装到 Entry 对象,放入 root
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
2.2 以后添加
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do { //遍历所有的 key , 给当前 key 找到适当位置
parent = t;
cmp = cpr.compare(key, t.key);//动态绑定到我们的匿名内部类的 compare
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果遍历过程中,发现准备添加 Key 和当前已有的 Key 相等,就不添加
return t.setValue(value);
} while (t != null);
}
*/
}
}

14.17 Collections工具类

image-20231114143658150

image-20231114143810012

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//创建 ArrayList 集合,用于测试.
List list = new ArrayList();
list.add("tom");
list.add("smith");
list.add("king");
list.add("milan");
list.add("tom");

// 1)reverse(List):反转 List 中元素的顺序
Collections.reverse(list);
System.out.println("list=" + list);

//2) shuffle(List):对 List 集合元素进行随机排序
for (int i = 0; i < 5; i++) {
Collections.shuffle(list);
System.out.println("list=" + list);
}

// 3)sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
Collections.sort(list);
System.out.println("自然排序后");
System.out.println("list=" + list);

// 4)sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
// 比如我们希望按照 字符串的长度大小排序
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o2).length() - ((String) o1).length();
}
});
System.out.println("字符串长度大小排序=" + list);

// 6)swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Collections.swap(list, 0, 1);
System.out.println("交换后的情况");
System.out.println("list=" + list);

image-20231114144446373

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 1)Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
System.out.println("自然顺序最大元素=" + Collections.max(list));

// 2)Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
//比如,我们要返回长度最大的元素
Object maxObject = Collections.max(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).length() - ((String)o2).length();
}
});
System.out.println("长度最大的元素=" + maxObject);

// 3)Object min(Collection)
// 4)Object min(Collection,Comparator)
//上面的两个方法,参考 max 即可

// 5)int frequency(Collection,Object):返回指定集合中指定元素的出现次数
System.out.println("tom 出现的次数=" + Collections.frequency(list, "tom"));

// 6)void copy(List dest,List src):将 src 中的内容复制到 dest 中
ArrayList dest = new ArrayList();
//为了完成一个完整拷贝,我们需要先给 dest 赋值,大小和 list.size()一样
for(int i = 0; i < list.size(); i++) {
dest.add("");
}
//拷贝
Collections.copy(dest, list);
System.out.println("dest=" + dest);

// 7)boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
Collections.replaceAll(list, "tom", "汤姆");
System.out.println("list 替换后=" + list);

第十五章 泛型

15.1 泛型入门

15.1.1 传统方法问题分析

  • 不能对加入到集合ArrayList中的数据类型进行约束(不安全)
  • 遍历的时候,需要进行类型转换,如果集合中的数据量较大,对效率有影响

15.1.2 泛型的好处

image-20231115152106005

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1. 当我们 ArrayList<Dog> 表示存放到 ArrayList 集合中的元素是 Dog 类型 (细节后面说...)
//2. 如果编译器发现添加的类型,不满足要求,就会报错
//3. 在遍历的时候,可以直接取出 Dog 类型而不是 Object
//4. public class ArrayList<E> {} E 称为泛型,那么 Dog->E
ArrayList<Dog> arrayList = new ArrayList<Dog>();
arrayList.add(new Dog("旺财", 10));
arrayList.add(new Dog("发财", 1));
arrayList.add(new Dog("小黄", 5));
//假如我们的程序员,不小心,添加了一只猫
//arrayList.add(new Cat("招财猫", 8)); 编译会报错
System.out.println("===使用泛型====");
for (Dog dog : arrayList) {
System.out.println(dog.getName() + "-" + dog.getAge());
}

15.2 泛型介绍

  1. 泛型又称**参数化类型,是Jdk5.0出现的新特性,解决数据类型的安全性问题**
  2. 在类声明或实例化时只要指定好需要的具体的类型即可。
  3. JAVA泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常.同时,代码更加简洁、健壮
  4. 泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,或者是某个方法返回值的类型,或者是某个方法的参数类型.

15.3 泛型的语法

  • 声明

    image-20231115154817021

  • 实例化

    image-20231115154942951

  • 举例

    • image-20231115160310713

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      public class GenericExercise {
      public static void main(String[] args) {
      //使用泛型方式给 HashSet 放入 3 个学生对象
      HashSet<Student> students = new HashSet<Student>();
      students.add(new Student("jack", 18));
      students.add(new Student("tom", 28));
      students.add(new Student("mary", 19));
      //遍历
      for (Student student : students) {
      System.out.println(student);
      }

      //使用泛型方式给 HashMap 放入 3 个学生对象
      //K -> String V->Student
      HashMap<String, Student> hm = new HashMap<String, Student>();
      /*
      public class HashMap<K,V> {}
      */
      hm.put("milan", new Student("milan", 38));
      hm.put("smith", new Student("smith", 48));

      // 遍历
      /*
      public Set<Map.Entry<K,V>> entrySet() {
      Set<Map.Entry<K,V>> es;
      return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
      }
      */
      Set<Map.Entry<String, Student>> entries = hm.entrySet();
      /*
      public final Iterator<Map.Entry<K,V>> iterator() {
      return new EntryIterator();
      }
      */
      Iterator<Map.Entry<String, Student>> iterator = entries.iterator();
      while (iterator.hasNext()) {
      Map.Entry<String, Student> next = iterator.next();
      System.out.println(next.getKey() + "=" + next.getValue());
      }
      }
      }

      class Student {
      private String name;
      private int age;

      public Student(String name, int age) {
      this.name = name;
      this.age = age;
      }

      public String getName() {
      return name;
      }

      public void setName(String name) {
      this.name = name;
      }

      public int getAge() {
      return age;
      }

      public void setAge(int age) {
      this.age = age;
      }

      @Override
      public String toString() {
      return "Student{" +
      "name='" + name + '\'' +
      ", age=" + age +
      '}';
      }
      }

15.4 注意事项

  1. 泛型的参数化类型只能是引用类型,不能是基本数据类型
  2. 在给泛型指定具体类型后,可以传入该类型或其子类类型
  3. 不指定泛型的具体类型时,默认的泛型是Object
  4. 泛型的使用形式:
    • List<Integer> list1 = new ArrayList<Integer>();
    • List<Integer> list1 = new ArrayList<>(); 【推荐】

15.5 练习题

image-20231115172404368

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
public class GenericExercise01 {
public static void main(String[] args) {
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee("durango", 18000, new MyDate(1999, 4, 2)));
list.add(new Employee("durango", 18000, new MyDate(1999, 1, 2)));
list.add(new Employee("durango", 18000, new MyDate(1999, 4, 20)));
list.add(new Employee("aoiegn", 18000, new MyDate(2000, 7, 2)));
System.out.println(list);

list.sort(new Comparator<Employee>() {
@Override
public int compare(Employee o1, Employee o2) {
return o1.compareTo(o2);
}
});
System.out.println(list);
}
}

class Employee implements Comparable<Employee> {
private String name;
private double sal;
private MyDate birthday;

public Employee(String name, double sal, MyDate birthday) {
this.name = name;
this.sal = sal;
this.birthday = birthday;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getSal() {
return sal;
}

public void setSal(double sal) {
this.sal = sal;
}

public MyDate getBirthday() {
return birthday;
}

public void setBirthday(MyDate birthday) {
this.birthday = birthday;
}

@Override
public String toString() {
return "Employee{" +
"name=" + name +
", sal=" + sal +
", birthday=" + birthday +
'}';
}

@Override
public int compareTo(Employee o) {
int c;
if ((c = this.name.compareTo(o.name)) != 0) {
return c;
}
// 如果名字相同,则比较生日
return this.birthday.compareTo(o.birthday);
}
}

class MyDate implements Comparable<MyDate> {
private int year;
private int month;
private int day;

public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public int getMonth() {
return month;
}

public void setMonth(int month) {
this.month = month;
}

public int getDay() {
return day;
}

public void setDay(int day) {
this.day = day;
}

@Override
public String toString() {
return year + "-" + month + "-" + day;
}


@Override
public int compareTo(MyDate o) {
int y, m;
// if ((y = this.year - o.year) == 0) {
// if ((m = this.month - o.month) == 0) {
// return this.day - o.day;
// } else {
// return m;
// }
// } else {
// return y;
// }
// 简洁写法
if ((y = this.year - o.year) != 0) {
return y;
}
// 年份相同则比较月份
if ((m = this.month - o.month) != 0) {
return m;
}
// 月份相同比较天数
return this.day - o.day;
}
}

15.6 自定义泛型

15.6.1 自定义泛型类

image-20231116104904262

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//1. Tiger 后面泛型,所以我们把 Tiger 就称为自定义泛型类
//2, T, R, M 泛型的标识符, 一般是单个大写字母
//3. 泛型标识符可以有多个. //4. 普通成员可以使用泛型 (属性、方法)
//5. 使用泛型的数组,不能初始化
//6. 静态方法中不能使用类的泛型
class Tiger<T, R, M> {
String name;
R r; //属性使用到泛型
M m;
T t;
// T[] ts = new T[8];
//因为数组在 new 时不能确定 T 的类型,就无法在内存中开辟空间
T[] ts;

public Tiger(String name, R r, M m, T t) {//构造器使用泛型
this.name = name;
this.r = r;
this.m = m;
this.t = t;
}

//因为静态是和类相关的,在类加载时,对象还没有创建
//所以,如果静态方法和静态属性使用了泛型,JVM 就无法完成初始化
// static R r2;
// public static void m1(M m) {
//
// }

//方法使用泛型
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public R getR() {
return r;
}

public void setR(R r) {//方法使用到泛型
this.r = r;
}

public M getM() {//返回类型可以使用泛型. return m;
}

public void setM(M m) {
this.m = m;
}

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}

@Override
public String toString() {
return "Tiger{" +
"name='" + name + '\'' +
", r=" + r +
", m=" + m +
", t=" + t +
", ts=" + Arrays.toString(ts) +
'}';
}
}

15.6.2 自定义泛型接口

image-20231116110621493

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 泛型接口使用的说明
* 1. 接口中,静态成员也不能使用泛型
* 2. 泛型接口的类型, 在继承接口或者实现接口时确定
* 3. 没有指定类型,默认为 Object
*/

//在继承接口 指定泛型接口的类型
interface IA extends IUsb<String, Double> {

}

//当我们去实现 IA 接口时,因为 IA 在继承 IUsu 接口时,指定了 U 为 String R 为 Double
//在实现 IUsu 接口的方法时,使用 String 替换 U, 使用 Double 替换 R
class AA implements IA {
@Override
public Double get(String s) {
return null;
}

@Override
public void hi(Double aDouble) {
}

@Override
public void run(Double r1, Double r2, String u1, String u2) {
}
}

//实现接口时,直接指定泛型接口的类型
//给 U 指定 Integer,给 R 指定了 Float
//所以,当我们实现 IUsb 方法时,会使用 Integer 替换 U, 使用 Float 替换 R
class BB implements IUsb<Integer, Float> {
@Override
public Float get(Integer integer) {
return null;
}

@Override
public void hi(Float aFloat) {
}

@Override
public void run(Float r1, Float r2, Integer u1, Integer u2) {
}
}

//没有指定类型,默认为 Object
//建议直接写成 IUsb<Object,Object>
class CC implements IUsb { //等价 class CC implements IUsb<Object,Object>
@Override
public Object get(Object o) {
return null;
}

@Override
public void hi(Object o) {
}

@Override
public void run(Object r1, Object r2, Object u1, Object u2) {
}
}

interface IUsb<U, R> {
int n = 10;
//U name; 不能这样使用

//普通方法中,可以使用接口泛型
R get(U u);

void hi(R r);

void run(R r1, R r2, U u1, U u2);

//在 jdk8 中,可以在接口中使用默认方法, 也是可以使用泛型
default R method(U u) {
return null;
}
}

15.6.3 自定义泛型方法

image-20231119145951717

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class CustomMethodGeneric {
public static void main(String[] args) {
Car car = new Car();
car.fly("宝马", 100);//当调用方法时,传入参数,编译器,就会确定类型
System.out.println("=======");
car.fly(300, 100.1);//当调用方法时,传入参数,编译器,就会确定类型
//测试
//T->String, R-> ArrayList
Fish<String, ArrayList> fish = new Fish<>();
fish.hello(new ArrayList(), 11.3f);
}
}

//泛型方法,可以定义在普通类中, 也可以定义在泛型类中
class Car {//普通类

public void run() {//普通方法
}

//说明 泛型方法
//1. <T,R> 就是泛型
//2. 是提供给 fly 使用的
public <T, R> void fly(T t, R r) {//泛型方法
System.out.println(t.getClass());//String
System.out.println(r.getClass());//Integer
}
}

class Fish<T, R> {//泛型类

public void run() {//普通方法
}

public <U, M> void eat(U u, M m) {//泛型方法
}

//说明
//1. 下面 hi 方法不是泛型方法
//2. 只是 hi 方法使用了类声明的 泛型
public void hi(T t) {
}

//泛型方法,可以使用类声明的泛型,也可以使用自己声明泛型
public <K> void hello(R r, K k) {
System.out.println(r.getClass());//ArrayList
System.out.println(k.getClass());//Float
}
}

15.7 泛型的继承和通配符

image-20231119151234218

15.8 JUnit单元测试类

image-20231119152211721

image-20231119152603009

第十六章 坦克大战【1】

image-20231120105005091

16.1 绘制坦克

16.1.1 坐标和像素

image-20231119155510069

image-20231119155552024

  • 绘制一个圆形

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    import javax.swing.*;
    import java.awt.*;

    public class DrawCircle extends JFrame { // DrawCircle继承JFrame,画框类(一个窗口)
    private MyPanel mp = null;

    public static void main(String[] args) {
    new DrawCircle();
    System.out.println("退出程序~");
    }

    public DrawCircle() { //构造器
    // 初始化画板
    mp = new MyPanel();
    // 把画板加入到画框(窗口)
    this.add(mp);
    // 设置窗口大小
    this.setSize(400, 300);
    //当点击窗口的小×,程序完全退出.
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    // 设置可以显示
    this.setVisible(true);
    }
    }

    // 1.先定义一个MyPanel,继承JPanel类,画板类
    class MyPanel extends JPanel {
    //说明:
    //1. MyPanel 对象就是一个画板
    //2. Graphics g 把 g 理解成一支画笔
    //3. Graphics 提供了很多绘图的方法
    @Override
    public void paint(Graphics g) { //绘图方法
    super.paint(g);
    System.out.println("paint方法被调用了");
    // 画圆形
    g.drawOval(10, 10, 100, 100);
    }
    }

16.1.2 绘图原理

image-20231119163054954

16.1.3 Graphics类

image-20231119163153997

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 画圆形
g.drawOval(10, 10, 100, 100);

//演示绘制不同的图形..
//画直线 drawLine(int x1,int y1,int x2,int y2)
g.drawLine(10, 10, 100, 100);

//画矩形边框 drawRect(int x, int y, int width, int height)
g.drawRect(10, 10, 100, 100);

//画椭圆边框 drawOval(int x, int y, int width, int height)

//填充矩形 fillRect(int x, int y, int width, int height)
//设置画笔的颜色
g.setColor(Color.blue);
g.fillRect(10, 10, 100, 100);

//填充椭圆 fillOval(int x, int y, int width, int height)
g.setColor(Color.red);
g.fillOval(10, 10, 100, 100);

//画图片 drawImage(Image img, int x, int y, ..)
//1. 获取图片资源, /bg.png 表示在该项目的根目录[chapter16/out/production/chapter16]去获取 bq.png 图片资源
Image image = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bq.png"));
g.drawImage(image, 10, 10, 175, 221, this);

//画字符串 drawString(String str, int x, int y)//写字
//给画笔设置颜色和字体
g.setColor(Color.red);
g.setFont(new Font("隶书", Font.BOLD, 50));
//这里设置的 100, 100, 是 "北京你好"左下角
g.drawString("北京你好", 100, 100);

16.1.4 绘制坦克

image-20231119174509641

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class MyPanel extends JPanel { //自定义画板类
Hero hero = null; // 定义我的坦克

public MyPanel() {
hero = new Hero(100, 100); // 初始化坦克
}

@Override
public void paint(Graphics g) {
super.paint(g);
g.fillRect(0, 0, 1000, 750); //填充背景,默认是黑色

// 画出坦克-封装成方法
drawTank(hero.getX(), hero.getY(), g, 0, 0);
drawTank(hero.getX() - 50, hero.getY(), g, 0, 1);

}

// 画坦克

/**
* @param x 左上角坐标
* @param y 右上角坐标
* @param g 画笔
* @param direct 方向(上下左右)
* @param type 坦克类型
*/
public void drawTank(int x, int y, Graphics g, int direct, int type) {
// 根据不同坦克类型,设置不同颜色
switch (type) {
case 0: //我们的坦克
g.setColor(Color.cyan);
break;
case 1: //敌人的坦克
g.setColor(Color.yellow);
break;
}

// 根据坦克方向,绘制坦克
switch (direct) {
case 0: //向上
g.fill3DRect(x, y, 10, 60, false); //左边轮子
g.fill3DRect(x + 30, y, 10, 60, false); //右边轮子
g.fill3DRect(x + 10, y + 10, 20, 40, false); //中间部分
g.fillOval(x + 10, y + 20, 20, 20); //中间的圆
g.drawLine(x + 20, y + 30, x + 20, y); //炮筒
break;
default:
System.out.println("没有处理");
}
}
}

16.2 java事件处理机制

16.2.1 小球移动

  • MyPanel要实现KeyListener键盘监听器,并实现对应监听方法(逻辑是 哪个键 ==> 哪个变量变化),注意要**repaint()**才能看到画面变化
  • BallMove窗口要添加这个监听器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class BallMove extends JFrame {
private MyPanel mp = null;

public static void main(String[] args) {
new BallMove();
}

public BallMove() {
mp = new MyPanel();
this.add(mp);
// 使窗口 JFrame 对象可以监听键盘事件, 即可以监听到面板发生的键盘事件
this.addKeyListener(mp);
this.setSize(400, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
}

// KeyListener监听器,可以监听键盘事件KeyEvent
class MyPanel extends Panel implements KeyListener {
int x = 10;
int y = 10; //小球初始坐标

@Override
public void paint(Graphics g) {
super.paint(g);
g.fillOval(x, y, 20, 20);
}

// 有字符输出时,该方法触发
@Override
public void keyTyped(KeyEvent e) {

}

// 有按键按下时,该方法触发
@Override
public void keyPressed(KeyEvent e) {
System.out.println((char) e.getKeyCode() + "被按下..");
// 根据按下的键控制小球移动(每个键分配了一个int值)
if (e.getKeyCode() == KeyEvent.VK_DOWN) { //向下
y++;
} else if (e.getKeyCode() == KeyEvent.VK_UP) { //向上
y--;
} else if (e.getKeyCode() == KeyEvent.VK_LEFT) { //向左
x--;
} else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { //向右
x++;
}
// 重新绘制面板
this.repaint();
}

// 有按键松开时,该方法出发
@Override
public void keyReleased(KeyEvent e) {

}
}

16.2.2 事件处理机制

image-20231120094737864

image-20231120094802149

image-20231120095147376

image-20231120095208027

image-20231120095314114

16.3 坦克移动

  • 绘制不同方向的坦克

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public void drawTank(int x, int y, Graphics g, int direct, int type) {
    // 根据不同坦克类型,设置不同颜色
    // ...
    // 根据不同方向,绘制坦克
    switch (direct) {
    case 0: //向上
    g.fill3DRect(x, y, 10, 60, false); //左边轮子
    g.fill3DRect(x + 30, y, 10, 60, false); //右边轮子
    g.fill3DRect(x + 10, y + 10, 20, 40, false); //中间部分
    g.fillOval(x + 10, y + 20, 20, 20); //中间的圆
    g.drawLine(x + 20, y + 30, x + 20, y); //炮筒
    break;
    case 1: //向右
    g.fill3DRect(x, y, 60, 10, false); //左边轮子
    g.fill3DRect(x, y + 30, 60, 10, false); //右边轮子
    g.fill3DRect(x + 10, y + 10, 40, 20, false); //中间部分
    g.fillOval(x + 20, y + 10, 20, 20); //中间的圆
    g.drawLine(x + 30, y + 20, x + 60, y + 20); //炮筒
    break;
    case 2: //向下
    g.fill3DRect(x, y, 10, 60, false); //右边轮子
    g.fill3DRect(x + 30, y, 10, 60, false); //左边轮子
    g.fill3DRect(x + 10, y + 10, 20, 40, false); //中间部分
    g.fillOval(x + 10, y + 20, 20, 20); //中间的圆
    g.drawLine(x + 20, y + 30, x + 20, y + 60); //炮筒
    break;
    case 3: //向左
    g.fill3DRect(x, y, 60, 10, false); //左边轮子
    g.fill3DRect(x, y + 30, 60, 10, false); //右边轮子
    g.fill3DRect(x + 10, y + 10, 40, 20, false); //中间部分
    g.fillOval(x + 20, y + 10, 20, 20); //中间的圆
    g.drawLine(x + 30, y + 20, x, y + 20); //炮筒
    break;
    default:
    System.out.println("没有处理");
    }
    }
  • 监听键盘事件,让坦克移动。注意:repaint() 和 addListener()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Override
    public void keyPressed(KeyEvent e) {
    if(e.getKeyCode() == KeyEvent.VK_W) { //W 上
    // 修改方向
    hero.setDirect(0);
    // 修改坐标
    hero.moveUp();
    } else if (e.getKeyCode() == KeyEvent.VK_D) { // D 右
    hero.setDirect(1);
    hero.moveRight();
    } else if (e.getKeyCode() == KeyEvent.VK_S) { // S 下
    hero.setDirect(2);
    hero.moveDown();
    } else if (e.getKeyCode() == KeyEvent.VK_A) { // A 左
    hero.setDirect(3);
    hero.moveLeft();
    }
    // 重写绘制画板
    this.repaint();
    }

16.4 作业

image-20231120103410941

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MyPanel extends JPanel implements KeyListener { //自定义画板类
Hero hero = null; // 定义我的坦克
// 定义敌人的坦克,放入Vector集合中(线程安全)
Vector<Enemy> enemies = new Vector<>();
int enemySize = 3;

public MyPanel() {
hero = new Hero(100, 100); // 初始化自己的坦克
hero.setSpeed(5); // 初始化坦克速度

//初始化敌人坦克
for (int i = 0; i < enemySize; i++) {
Enemy enemy = new Enemy(100 * (i + 1), 0);
enemy.setDirect(2); // 设置初始炮筒方向
enemies.add(enemy);
}
}

@Override
public void paint(Graphics g) {
super.paint(g);
g.fillRect(0, 0, 1000, 750); //填充背景,默认是黑色

// 画出坦克-封装成方法
drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 0);

// 画出敌人坦克(遍历Vector)
for (int i = 0; i < enemies.size(); i++) {
// 取出坦克并绘制
Enemy enemy = enemies.get(i);
drawTank(enemy.getX(), enemy.getY(), g, enemy.getDirect(), 1);
}
}

...
}

第十七章 多线程基础

17.1 线程相关概念

  1. 程序、
    • 是为完成特定任务、用某种语言编写的一组指令的集合。简单的说:就是我们写的代码
  2. 进程
    • 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间.
    • 进程是程序的一次执行过程,或是正在运行的一个程序.是动态过程:有它自身的产生、存在和消亡过程
  3. 线程
    • 线程由进程创建的,是进程的一个实体
    • 一个进程可以拥有多个线程
  4. 其他概念
    • 单线程: 同一个时刻,只允许执行一个线程
    • 多线程: 同一个时刻,可以执行多个线程,比如:一个QQ进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件
    • 并发: 同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说, 单核cpu实现的多任务就是并发。
    • 并行: 同一个时刻,多个任务同时执行。多核cpu可以实现并行。

17.2 线程基本使用

17.2.1 创建线程的两种方式

  1. 继承Thread 类, 重写run方法

  2. 实现Runnable接口, 重写run方法

    image-20231121104552926

    image-20231121093524733

17.2.2 继承Thread类

image-20231121095419896

image-20231121101159823

image-20231121101613160

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
// 创建 Cat 对象,可以当做线程使用
Cat cat = new Cat();
cat.start(); ////启动线程->start0()-> 最终会执行cat的run方法
/*
(1)
public synchronized void start() {
...
start0();
}
(2)
start0() 是本地方法,是 JVM 调用, 底层是 c/c++实现真正实现多线程的效果, 是 start0(), 而不是 run
private native void start0();
*/

//cat.run();//run 方法就是一个普通的方法, 没有真正的启动一个线程,就会把 run方法执行完毕,然后才向下执行
//说明: 当 main 线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行
//这时 主线程和子线程是交替执行..
System.out.println("主线程继续执行" + Thread.currentThread().getName());//名字 main
for (int i = 0; i < 60; i++) {
System.out.println("主线程" + Thread.currentThread().getName() + " i=" + i);
//让主线程休眠
Thread.sleep(1000);
}


}
}

//1. 当一个类继承了 Thread 类, 该类就可以当做线程使用
//2. 我们会重写 run 方法,写上自己的业务代码
//3. Thread类 实现了 Runnable接口 的 run 方法
/*
@Override
public void run() {
if (target != null) {
target.run();
}
}
*/
class Cat extends Thread {
int times = 0;

@Override
public void run() {
while (true) {
//该线程每隔 1 秒。在控制台输出 “喵喵, 我是小猫咪”
System.out.println("喵喵, 我是小猫咪" + (++times) + " 线程名=" + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 让线程休眠一秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (times == 80) { // times到达80时,子线程执行完毕
break;
}
}
}

}

17.2.3 实现Runnable接口

image-20231121102613498

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();
//dog.start() //错误,Dog类没有start
//创建Thread对象,把Dog对象(实现了Runnable)放入Thread
Thread thread = new Thread(dog);
thread.start();
}
}

class Dog implements Runnable { //实现Runnable接口来开发线程
int count = 0;

@Override
public void run() {
while (true) {
System.out.println("小狗汪汪叫..hi" + (++count) + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 10) {
break;
}
}
}
}
  • 模拟一个简单的Thread代理类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class Thread02 {
    public static void main(String[] args) {
    // Tiger tiger = new Tiger();//实现了 Runnable
    // ThreadProxy threadProxy = new ThreadProxy(tiger);
    // threadProxy.start();
    }
    }

    class Tiger implements Runnable {
    @Override
    public void run() {
    System.out.println("老虎嗷嗷叫....");
    }
    }

    //线程代理类 , 模拟了一个极简的 Thread 类
    class ThreadProxy implements Runnable {//你可以把 Proxy 类当做 ThreadProxy
    private Runnable target = null;//属性,类型是 Runnable

    @Override
    public void run() {
    if (target != null) {
    target.run();//动态绑定(运行类型 Tiger)
    }
    }

    public ThreadProxy(Runnable target) {
    this.target = target;
    }

    public void start() {
    start0();//这个方法时真正实现多线程方法
    }

    public void start0() {
    run();
    }
    }

17.2.4 多线程案例

image-20231121104204115

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);
thread1.start();
thread2.start();
}
}

class T1 implements Runnable {
int count = 0;

@Override
public void run() {
while (true) {
// 每个1s输出“hello, world!”,输出10次
System.out.println("hello, world!" + (++count) + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 10) {
break;
}
}
}
}

class T2 implements Runnable {
int count = 0;

@Override
public void run() {
while (true) {
// 每个1s输出“hi”,输出5次
System.out.println("hi!" + (++count) + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 5) {
break;
}
}
}
}

17.3 模拟售票系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class SellTicket {
public static void main(String[] args) {
// SellTicket01 sellTicket01 = new SellTicket01();
// SellTicket01 sellTicket02 = new SellTicket01();
// SellTicket01 sellTicket03 = new SellTicket01();
// // 这里会有多个进程同时执行,同时访问共享数据时会出现数据竞争,从而导致错误结果
// sellTicket01.start();
// sellTicket02.start();
// sellTicket03.start();

// 也会出现资源竞争问题,从而导致错误结果
SellTicket02 sellTicket02 = new SellTicket02();
new Thread(sellTicket02).start();
new Thread(sellTicket02).start();
new Thread(sellTicket02).start();


}
}
// 继承Thread方式
class SellTicket01 extends Thread {
public static int ticketNum = 100; //设置static,多个线程共享

@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
break;
}
//休眠 50 毫秒, 模拟
try {
Thread.sleep(60);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票" +
",剩余票数=" + (--ticketNum));
}
}
}

//实现接口方式
class SellTicket02 implements Runnable {
private int ticketNum = 100; //让多个线程共享 ticketNum

@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
break;
}
//休眠 50 毫秒, 模拟
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));//1 - 0 - -1 - -2
}
}
}

image-20231123094913189

17.4 线程终止

image-20231122093546074

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ThreadExit {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
// 通知方式:主线程通过修改loop变量,让线程终止(停止run方法)
System.out.println("main线程休眠10s");
Thread.sleep(10 * 1000);
t.setLoop(false);
}
}

class T extends Thread {
private int count = 0;
// 设置控制变量,控制run方法
private boolean loop = true;

@Override
public void run() {
while (loop) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在运行:" + (count++));
}
}

public void setLoop(boolean loop) {
this.loop = loop;
}
}

17.5 线程常用方法

17.5.1 第一组方法

image-20231122093745346

image-20231122093956022

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ThreadMethod01 {
public static void main(String[] args) throws InterruptedException {
// 测试相关方法
T t = new T();
t.setName("durango");
t.setPriority(Thread.MAX_PRIORITY);
t.start();

// 主线程打印5个hi,然后中断子线程的休眠
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println("hi" + i);
}
System.out.println(t.getName() + "优先级是" + t.getPriority());
t.interrupt(); //执行到这里是会中断t线程的休眠
}
}

class T extends Thread {
private int count = 0;

@Override
public void run() {
while (true) {
for (int i = 0; i < 10; i++) {
// 获取当前线程名称
System.out.println(Thread.currentThread().getName() + "" + i);
}
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
// 当线程执行到一个interrupt方法时,就会catch到一个异常,可以加入自己的业务代码
// InterruptedException是捕获到的一个中断异常
System.out.println(Thread.currentThread().getName() + "被中断了");
}
}
}
}

17.5.2 第二组方法

image-20231122100737396

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
t1.start();

for (int i = 1; i <= 20; i++) {
Thread.sleep(1000);
System.out.println("hi" + i);
if (i == 5) {
//Thread.yield(); //主线程礼让,资源充足时不一定成功
t1.join(); //子线程插队,会先执行完毕,再执行主线程
}
}
}
}

class T1 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello" + i);
}
}
}

17.5.3 用户线程和守护线程

image-20231122102151846

  • 在main主线程中开启了一个无限执行的子线程T,即使主线程指向完毕了,T也还在继续执行。

  • 将一个线程设置成守护线程setDaemon(true)

    image-20231122102545351

17.6 线程的生命周期

17.6.1 线程的状态

image-20231122103200151

17.6.2 状态转化图(重要!)

image-20231122105844283

17.6.3 通过程序查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ThreadState {
public static void main(String[] args) throws InterruptedException {
T t = new T();
System.out.println(t.getState());
t.start();
while (t.getState() != Thread.State.TERMINATED) {
System.out.println(t.getName() + "状态" + t.getState());
Thread.sleep(1000);
}
}
}

class T extends Thread {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "状态" + Thread.currentThread().getState());
for (int i = 0; i < 10; i++) {
System.out.println("hi" + i);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

17.7 线程同步

17.7.1 线程同步机制

image-20231123093337461

17.7.2 synchronized关键字

image-20231123093453121

image-20231123095054030

  • 使用synchronized解决售票问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class SellTicket {
    public static void main(String[] args) {
    SellTicket02 sellTicket02 = new SellTicket02();
    new Thread(sellTicket02).start();
    new Thread(sellTicket02).start();
    new Thread(sellTicket02).start();
    }
    }

    //实现接口方式,使用synchronized关键字实现同步
    class SellTicket02 implements Runnable {
    private int ticketNum = 100; //让多个线程共享 ticketNum
    private boolean loop = true;

    public synchronized void sell() { //同步方法,在同一个时刻,只能有一个线程来sell方法
    if (ticketNum <= 0) {
    System.out.println("售票结束...");
    loop = false;
    return;
    }
    System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
    " 剩余票数=" + (--ticketNum));
    }

    @Override
    public void run() {
    while (loop) {
    sell();
    //休眠 50 毫秒, 模拟
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    image-20231123094808434

17.8 互斥锁

17.8.1 基本概念

image-20231123100946318

17.8.2 注意事项

image-20231123101105826

17.8.3 售票系统的同步问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public class SellTicket {
public static void main(String[] args) {
SellTicket01 sellTicket01 = new SellTicket01();
SellTicket01 sellTicket02 = new SellTicket01();
SellTicket01 sellTicket03 = new SellTicket01();
sellTicket01.start();
sellTicket02.start();
sellTicket03.start();

// SellTicket02 sellTicket02 = new SellTicket02();
// new Thread(sellTicket02).start();
// new Thread(sellTicket02).start();
// new Thread(sellTicket02).start();
}
}

// 继承Thread方式
class SellTicket01 extends Thread {
public static int ticketNum = 100; //设置static,多个线程共享
public static boolean loop = true;
public static Object obj = new Object();

// 同步方法,锁在this对象上(无法实现同步,为什么?)
// 注意!!!继承继承Thread方式创建线程时,创建的是多个对象
// 因此this是三个不同的对象!!!
public synchronized void sell() {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));
}

public void sell2() {
// 同步代码块,锁在同一对象obj(obj是所有SellTicket01对象共享的才行)
synchronized (obj) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));
}
}

@Override
public void run() {
while (loop) {
//sell(); //无法同步
sell2(); //可以同步
//休眠 50 毫秒, 模拟
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

//实现接口方式
class SellTicket02 implements Runnable {
private int ticketNum = 100; //让多个线程共享 ticketNum
private boolean loop = true;
Object obj = new Object();

// 同步方法,锁在this对象上
public synchronized void sell() {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));
}

public void sell2() {
// 同步代码块,锁可以在this,也可以在同一对象obj
synchronized (obj) {
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" +
" 剩余票数=" + (--ticketNum));
}
}

// 静态同步方法,锁在当前类本身,即类名.class
public static synchronized void m() {
System.out.println("m");
}

public static void m2() {
// 静态方法中的同步代码块,锁在当前类本身,即类名.class
synchronized (SellTicket.class) {
System.out.println("m");
}
}

@Override
public void run() {
while (loop) {
sell();
//sell2();
//休眠 50 毫秒, 模拟
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

17.9 线程的死锁

image-20231123103318317

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class DeadLock {
public static void main(String[] args) {
//模拟死锁现象
DeadLockDemo A = new DeadLockDemo(true);
A.setName("A 线程");
DeadLockDemo B = new DeadLockDemo(false);
B.setName("B 线程");
A.start();
B.start();
}
}

class DeadLockDemo extends Thread {
static Object o1 = new Object(); // 设为static,所有DeadLockDemo对象共享
static Object o2 = new Object();
boolean flag;

public DeadLockDemo(boolean flag) {
this.flag = flag;
}

@Override
public void run() {
//1. 如果 flag 为 T, 线程 A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
//2. 如果线程 A 得不到 o2 对象锁,就会 Blocked
//3. 如果 flag 为 F, 线程 B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
//4. 如果线程 B 得不到 o1 对象锁,就会 Blocked
if (flag) {
synchronized (o1) { // 同步代码块
System.out.println(Thread.currentThread().getName() + "拿到o1的锁,进入1");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "拿到o2的锁,进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "拿到o2的锁,进入3");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "拿到o1的锁,进入4");
}
}
}
}
}

image-20231123104309556

17.10 锁的释放

17.10.1 下面操作会释放锁

image-20231123104530485

17.10.2 下面操作不会释放锁

image-20231123104706798

第十八章 坦克大战【2】

18.1 坦克大战0.3

18.1.1 发射子弹

image-20231124101154619

  • Shot.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public class Shot implements Runnable {
    int x;
    int y; //子弹坐标
    int direct = 0; //子弹方向
    int speed = 10; //子弹速度
    boolean isLive = true; //子弹是否还存活

    public Shot(int x, int y, int direct) {
    this.x = x;
    this.y = y;
    this.direct = direct;
    }

    @Override
    public void run() {
    while (true) {
    // 休眠50ms
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    // 根据方向改变坐标
    switch (direct) {
    case 0: //上
    y -= speed;
    break;
    case 1: //右
    x += speed;
    break;
    case 2: //下
    y += speed;
    break;
    case 3: //左
    x -= speed;
    break;
    }
    // 测试输出坐标
    System.out.println("子弹线程" + Thread.currentThread().getName() + " x = " + x + ", y = " + y);
    // 当子弹移动到面板的边界时,就应该销毁(把启动的子弹的线程销毁)
    if (!(x >= 0 && x <= 1000 && y >= 0 && y <= 750)) {
    isLive = false;
    break;
    }
    }
    }
    }
  • hero.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class Hero extends Tank { // 自定义坦克类
    // 定义一个Shot对象
    Shot shot = null;
    ...
    // 射击,开启一个线程
    public void shotEnemyTank() {
    // 创建Shot对象,根据当前Hero对象的位置和方向创建Shot
    int direct = getDirect();
    switch (direct) {
    case 0: //向上
    shot = new Shot(getX() + 20, getY(), getDirect());
    break;
    case 1: //向右
    shot = new Shot(getX() + 60, getY() + 20, getDirect());
    break;
    case 2: //向下
    shot = new Shot(getX() + 20, getY() + 60, getDirect());
    break;
    case 3: //向左
    shot = new Shot(getX(), getY() + 20, getDirect());
    break;
    }
    // 启动Shot线程
    new Thread(shot).start();
    }
    }
  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class MyPanel extends JPanel implements KeyListener, Runnable{ //自定义画板类
    ...
    @Override
    public void paint(Graphics g) {
    ...
    // 画出hero射击的子弹
    if(hero.shot != null && hero.shot.isLive) {
    System.out.println("子弹被绘制!");
    //g.fill3DRect(hero.shot.x, hero.shot.y, 1, 1, false);
    g.draw3DRect(hero.shot.x, hero.shot.y, 1, 1, false);
    }
    }
    ...
    @Override
    public void keyPressed(KeyEvent e) {
    ...
    // 如果按下的是J,就发射子弹
    if (e.getKeyCode() == KeyEvent.VK_J) {
    hero.shotEnemyTank();
    }
    }
    ...
    @Override
    public void run() { //每隔100ms,重绘面板,刷新绘图区域,子弹就会移动
    while (true) {
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    this.repaint();
    }
    }
    ...
    }
  • TankGame03.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class TankGame03 extends JFrame {
    ...
    public TankGame03() {
    mp = new MyPanel();
    // 将mp放入Thread并启动
    Thread thread = new Thread(mp);
    thread.start();
    ...
    }
    }

18.2 坦克大战0.4

image-20231124144906258

18.2.1 敌人发射子弹

image-20231124145042566

  • Enemy.java

    1
    2
    3
    4
    5
    6
    7
    public class Enemy extends Tank {
    // 在敌人坦克类,使用Vector 保存多个Shot线程
    Vector<Shot> shots = new Vector<>();
    public Enemy(int x, int y) {
    super(x, y);
    }
    }
  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public class MyPanel extends JPanel implements KeyListener, Runnable{ //自定义画板类
    ...
    public MyPanel() {
    ...
    //初始化敌人坦克
    for (int i = 0; i < enemySize; i++) {
    ...
    // 给该enemy对象初始化Shot对象
    Shot shot = new Shot(enemy.getX() + 20, enemy.getY() + 60, enemy.getDirect());
    enemy.shots.add(shot);
    //启动Shot对象
    new Thread(shot).start();
    enemies.add(enemy);
    }
    }

    @Override
    public void paint(Graphics g) {
    ...
    // 画出enemy坦克(遍历Vector)
    for (int i = 0; i < enemies.size(); i++) {
    // 取出坦克并绘制
    Enemy enemy = enemies.get(i);
    drawTank(enemy.getX(), enemy.getY(), g, enemy.getDirect(), 1);

    // 绘制enemy的所有子弹
    for (int j = 0; j < enemy.shots.size(); j++) {
    // 取出子弹
    Shot shot = enemy.shots.get(j);
    // 绘制
    if (shot.isLive) {
    g.draw3DRect(shot.x, shot.y, 1, 1, false);
    } else {
    // 子弹消亡,从Vector移除
    enemy.shots.remove(shot);
    }
    }
    }
    }
    ...
    }

18.2.2 敌人坦克被击中

  • MyPanel.java:判断子弹是否击中敌人

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    public class MyPanel extends JPanel implements KeyListener, Runnable { //自定义画板类
    ...
    @Override
    public void paint(Graphics g) {
    ...
    // 画出enemy坦克(遍历Vector)
    for (int i = 0; i < enemies.size(); i++) {
    // 取出坦克并绘制
    Enemy enemy = enemies.get(i);
    // 当坦克存活时才绘制
    if (enemy.isLive) {
    drawTank(enemy.getX(), enemy.getY(), g, enemy.getDirect(), 1);
    } else { //坦克死亡时从Vector中删除
    enemies.remove(enemy);
    }
    ...
    }
    }
    ...
    // 判断我方子弹是否击中敌人的子弹
    // 什么时候需要判断是否击中坦克?run方法中循环判断
    public void hitTank(Shot s, Enemy enemy) {
    // 判断s 击中坦克
    switch (enemy.getDirect()) {
    case 0: //坦克向上
    case 2: //坦克向下
    if (s.x > enemy.getX() && s.x < enemy.getX() + 40
    && s.y > enemy.getY() && s.y < enemy.getY() + 60) {
    s.isLive = false; //子弹销毁
    enemy.isLive = false; //敌人坦克销毁
    }
    break;
    case 1: //坦克向右
    case 3: //坦克向左
    if (s.x > enemy.getX() && s.x < enemy.getX() + 60
    && s.y > enemy.getY() && s.y < enemy.getY() + 40) {
    s.isLive = false; //子弹销毁
    enemy.isLive = false; //敌人坦克销毁
    }
    break;
    }
    }
    ...
    @Override
    public void run() { //每隔100ms,重绘面板,刷新绘图区域,子弹就会移动
    while (true) {
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    // 判断是否击中了敌人坦克
    if (hero.shot != null && hero.shot.isLive) {
    // 遍历所有的敌人坦克
    for (int i = 0; i < enemies.size(); i++) {
    Enemy enemy = enemies.get(i);
    hitTank(hero.shot, enemy);
    }
    }
    this.repaint();
    }
    }
    }

18.2.3 爆炸效果

  • Bomb.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Bomb {
    int x, y; //炸弹坐标
    int life = 9; //炸弹生命周期
    boolean isLive = true;

    public Bomb(int x, int y) {
    this.x = x;
    this.y = y;
    }

    // 减少生命值,配合图片出现爆炸效果
    public void lifeDown() {
    if (life > 0) {
    life --;
    } else {
    isLive = false;
    }
    }
    }
  • MyPanel.java:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    public class MyPanel extends JPanel implements KeyListener, Runnable { //自定义画板类
    ...
    // 定义一个Vector,用于存放炸弹(爆炸效果)
    // 当子弹击中坦克时,就加入一个爆炸效果
    Vector<Bomb> bombs = new Vector<>();
    // 定义三张炸弹图片,用于显示爆炸效果
    Image image1 = null;
    Image image2 = null;
    Image image3 = null;

    public MyPanel() {
    ...
    // 初始化Image对象
    image1 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_1.gif"));
    image2 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_2.gif"));
    image3 = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bomb_3.gif"));
    }

    @Override
    public void paint(Graphics g) {
    ...
    // 画出所有炸弹(爆炸效果)
    for (int i = 0; i < bombs.size(); i++) {
    System.out.println("绘制爆炸效果");
    Bomb bomb = bombs.get(i);
    // 根据bomb的life值,画出对应的图片
    if (bomb.life > 6) {
    g.drawImage(image1, bomb.x, bomb.y, 60, 60, this);
    } else if (bomb.life > 3) {
    g.drawImage(image2, bomb.x, bomb.y, 60, 60, this);
    } else {
    g.drawImage(image3, bomb.x, bomb.y, 60, 60, this);
    }
    // 让炸弹生命值减少
    bomb.lifeDown();
    // 如果炸弹的life为0了(isLive为false),就从bombs中删除
    if (!bomb.isLive) {
    bombs.remove(bomb);
    }
    }
    }
    ...
    / 判断我方子弹是否击中敌人的子弹
    // 什么时候需要判断是否击中坦克?run方法中循环判断
    public void hitTank(Shot s, Enemy enemy) {
    // 判断s 击中坦克
    switch (enemy.getDirect()) {
    case 0: //坦克向上
    case 2: //坦克向下
    if (s.x > enemy.getX() && s.x < enemy.getX() + 40
    && s.y > enemy.getY() && s.y < enemy.getY() + 60) {
    s.isLive = false; //子弹销毁
    enemy.isLive = false; //敌人坦克销毁
    // 创建Bomb对象,加入到bombs集合中
    Bomb bomb = new Bomb(enemy.getX(), enemy.getY());
    bombs.add(bomb);
    }
    break;
    case 1: //坦克向右
    case 3: //坦克向左
    if (s.x > enemy.getX() && s.x < enemy.getX() + 60
    && s.y > enemy.getY() && s.y < enemy.getY() + 40) {
    s.isLive = false; //子弹销毁
    enemy.isLive = false; //敌人坦克销毁
    // 创建Bomb对象,加入到bombs集合中
    Bomb bomb = new Bomb(enemy.getX(), enemy.getY());
    bombs.add(bomb);
    }
    break;
    }
    }

18.2.4 敌人坦克自由移动

image-20231124163454322

  • Enemy.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    public class Enemy extends Tank implements Runnable{
    // 在敌人坦克类,使用Vector 保存多个Shot线程
    Vector<Shot> shots = new Vector<>();
    boolean isLive = true;

    public Enemy(int x, int y) {
    super(x, y);
    }

    public void move(int direct, int step) {
    for (int i = 0; i < step; i++) {
    // 休眠50ms
    try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    switch (direct) {
    case 0: //上
    moveUp();
    break;
    case 1: //右
    moveRight();
    break;
    case 2: //下
    moveDown();
    break;
    case 3: //左
    moveLeft();
    break;
    }
    }

    }

    @Override
    public void run() {
    while (isLive) {
    // 根据坦克的方向来继续移动
    move(getDirect(), 30);
    // 然后随机地改变坦克方向 0-3
    setDirect((int)(Math.random() * 4));
    }
    }
    }
  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MyPanel extends JPanel implements KeyListener, Runnable { //自定义画板类
    ...
    public MyPanel() {
    hero = new Hero(100, 100); // 初始化自己的坦克
    hero.setSpeed(5); // 初始化坦克速度

    //初始化敌人坦克
    for (int i = 0; i < enemySize; i++) {
    Enemy enemy = new Enemy(100 * (i + 1), 0);
    enemy.setDirect(2); // 设置初始炮筒方向
    new Thread(enemy).start(); //启动Enemy线程,让坦克自由移动
    ...
    }
    ...
    }
    ....
    }

18.2.4 控制坦克移动范围

  • Enemy.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public void move(int direct, int step) {
    for (int i = 0; i < step; i++) {
    // 休眠50ms
    try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    switch (direct) {
    case 0: //上
    if (getY() > 0){
    moveUp();
    }
    break;
    case 1: //右
    if (getX() + 60 < 1000) {
    moveRight();
    }
    break;
    case 2: //下
    if (getY() + 60 < 750) {
    moveDown();
    }
    break;
    case 3: //左
    if (getX() > 0) {
    moveLeft();
    }
    break;
    }
    }

    }
  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Override
    public void keyPressed(KeyEvent e) {
    //jSystem.out.println((char) e.getKeyCode() + "被按下..");
    if (e.getKeyCode() == KeyEvent.VK_W) { //W 上
    // 修改方向
    hero.setDirect(0);
    // 修改坐标
    if (hero.getY() > 0){
    hero.moveUp();
    }
    } else if (e.getKeyCode() == KeyEvent.VK_D) { // D 右
    hero.setDirect(1);
    if (hero.getX() + 60 < 1000) {
    hero.moveRight();
    }
    } else if (e.getKeyCode() == KeyEvent.VK_S) { // S 下
    hero.setDirect(2);
    if (hero.getY() + 60 < 750) {
    hero.moveDown();
    }
    } else if (e.getKeyCode() == KeyEvent.VK_A) { // A 左
    hero.setDirect(3);
    if (hero.getX() > 0) {
    hero.moveLeft();
    }
    }
    ...
    }

18.3 坦克大战0.5

image-20231125165359114

18.3.1 发射多颗子弹

image-20231125165711044

  • Hero.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class Hero extends Tank { // 自定义坦克类
    // 定义一个Shot对象
    Shot shot = null;
    // 可以发射多颗子弹
    Vector<Shot> shots = new Vector<>();
    ...

    // 射击,开启一个线程
    public void shotEnemyTank() {
    // 控制一次最多发射几颗子弹
    if (shots.size() == 5) {
    return;
    }
    ...
    // 新创建的shot放入shots集合中
    shots.add(shot);
    // 启动Shot线程
    new Thread(shot).start();
    }
    }
  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    public class MyPanel extends JPanel implements KeyListener, Runnable { //自定义画板类
    ...
    @Override
    public void paint(Graphics g) {
    ...
    // 画出hero射击的子弹
    for (int i = 0; i < hero.shots.size(); i++) {
    Shot shot = hero.shots.get(i);
    if (shot != null && shot.isLive) {
    g.draw3DRect(shot.x, shot.y, 1, 1, false);
    } else { // shot对象已经销毁,从shots集合中移除
    hero.shots.remove(shot);
    }
    }
    ...
    }

    ...

    //判断我方所有子弹是否击中任意敌人
    public void hitEnemyTank() {
    for (int i = 0; i < hero.shots.size(); i++) {
    Shot shot = hero.shots.get(i);
    // 判断该颗子弹是否击中任意敌人
    if (hero.shot != null && hero.shot.isLive){
    for (int j = 0; j < enemies.size(); j++) {
    Enemy enemy = enemies.get(j);
    hitTank(shot, enemy);
    }
    }
    }
    }

    // 判断我方子弹是否击中敌人的子弹
    // 什么时候需要判断是否击中坦克?run方法中循环判断
    public void hitTank(Shot s, Enemy enemy) {
    // 判断s 击中坦克
    switch (enemy.getDirect()) {
    case 0: //坦克向上
    case 2: //坦克向下
    if (s.x > enemy.getX() && s.x < enemy.getX() + 40
    && s.y > enemy.getY() && s.y < enemy.getY() + 60) {
    s.isLive = false; //子弹销毁
    enemy.isLive = false; //敌人坦克销毁
    enemies.remove(enemy); //将敌人坦克从Vector中删除
    // 创建Bomb对象,加入到bombs集合中
    Bomb bomb = new Bomb(enemy.getX(), enemy.getY());
    bombs.add(bomb);
    }
    break;
    case 1: //坦克向右
    case 3: //坦克向左
    if (s.x > enemy.getX() && s.x < enemy.getX() + 60
    && s.y > enemy.getY() && s.y < enemy.getY() + 40) {
    s.isLive = false; //子弹销毁
    enemy.isLive = false; //敌人坦克销毁
    enemies.remove(enemy);
    // 创建Bomb对象,加入到bombs集合中
    Bomb bomb = new Bomb(enemy.getX(), enemy.getY());
    bombs.add(bomb);
    }
    break;
    }
    }

    @Override
    public void keyPressed(KeyEvent e) {
    ...
    // 如果按下的是J,就发射子弹
    if (e.getKeyCode() == KeyEvent.VK_J) {
    //只能发射一颗子弹,且子弹消亡了才能发射新的子弹
    /*if (hero.shot == null || !hero.shot.isLive) {
    hero.shotEnemyTank();
    }*/
    // 发射多颗子弹
    hero.shotEnemyTank();
    }
    }

    @Override
    public void run() { //每隔100ms,重绘面板,刷新绘图区域,子弹就会移动
    while (true) {
    try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    // 判断我方所有子弹是否击中了任意敌人坦克
    hitEnemyTank();
    this.repaint();
    }
    }
    }

18.3.2 敌人发射多颗子弹

image-20231126094243019

  • Enemy.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Override
    public void run() {
    while (isLive) {
    // 敌人发射多颗子弹,如果shots.size() < 3,创建新子弹
    if (shots.size() < 3) {
    Shot shot = null;
    switch (getDirect()) {
    case 0:
    shot = new Shot(getX() + 20, getY(), 0);
    break;
    case 1:
    shot = new Shot(getX() + 60, getY() + 20, 1);
    break;
    case 2:
    shot = new Shot(getX() + 20, getY() + 60, 2);
    break;
    case 3:
    shot = new Shot(getX(), getY(), 3);
    break;
    }
    shots.add(shot);
    new Thread(shot).start();
    }

    // 根据坦克的方向来继续移动
    move(getDirect(), 30);
    // 然后随机地改变坦克方向 0-3
    setDirect((int) (Math.random() * 4));
    }
    }

18.3.3 敌人坦克击中我方

  • Tank.java

    1
    boolean isLive = true;
  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    public class MyPanel extends JPanel implements KeyListener, Runnable {
    ...
    @Override
    public void paint(Graphics g) {
    super.paint(g);
    g.fillRect(0, 0, 1000, 750); //填充背景,默认是黑色

    // 画出hero坦克-封装成方法
    if (hero != null && hero.isLive){
    drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 0);
    }
    ...
    }

    / 判断敌人子弹是否击中我方坦克
    public void hitHero() {
    // 遍历所有敌人坦克
    for (int i = 0; i < enemies.size(); i++) {
    Enemy enemy = enemies.get(i);
    // 遍历敌人坦克所有子弹
    for (int j = 0; j < enemy.shots.size(); j++) {
    Shot shot = enemy.shots.get(j);
    if (hero.isLive && shot.isLive) {
    hitTank(shot, hero);
    }
    }
    }
    }

    //判断我方所有子弹是否击中任意敌人
    public void hitEnemyTank() {
    // 取出我方所有子弹
    for (int i = 0; i < hero.shots.size(); i++) {
    Shot shot = hero.shots.get(i);
    // 判断该颗子弹是否击中任意敌人
    if (hero.shot != null && hero.shot.isLive) {
    for (int j = 0; j < enemies.size(); j++) {
    Enemy enemy = enemies.get(j);
    hitTank(shot, enemy);
    }
    }
    }
    }

    // 判断子弹是否击中坦克
    // 什么时候需要判断是否击中坦克?run方法中循环判断
    public void hitTank(Shot s, Tank tank) {
    // 判断s 击中坦克
    switch (tank.getDirect()) {
    case 0: //坦克向上
    case 2: //坦克向下
    if (s.x > tank.getX() && s.x < tank.getX() + 40
    && s.y > tank.getY() && s.y < tank.getY() + 60) {
    s.isLive = false; //子弹销毁
    tank.isLive = false; //敌人坦克销毁
    enemies.remove(tank); //将敌人坦克从Vector中删除
    // 创建Bomb对象,加入到bombs集合中
    Bomb bomb = new Bomb(tank.getX(), tank.getY());
    bombs.add(bomb);
    }
    break;
    case 1: //坦克向右
    case 3: //坦克向左
    if (s.x > tank.getX() && s.x < tank.getX() + 60
    && s.y > tank.getY() && s.y < tank.getY() + 40) {
    s.isLive = false; //子弹销毁
    tank.isLive = false; //敌人坦克销毁
    enemies.remove(tank);
    // 创建Bomb对象,加入到bombs集合中
    Bomb bomb = new Bomb(tank.getX(), tank.getY());
    bombs.add(bomb);
    }
    break;
    }
    }
    ...
    @Override
    public void run() { //每隔100ms,重绘面板,刷新绘图区域,子弹就会移动
    while (true) {
    ...
    // 判断敌人子弹是否击中我方坦克
    hitHero();
    this.repaint();
    }
    }
    }

第十九章 IO流

19.1 文件流

image-20231127091955868

19.2 常用的文件操作

19.2.1 创建文件

image-20231127092406701

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 方式 1 new File(String pathname) 根据路径构建
@Test
public void create01() {
String filePath = "e:\\news1.txt";
File file = new File(filePath);
try {
file.createNewFile();
System.out.println("文件创建成功");
} catch (IOException e) {
e.printStackTrace();
}
}

// 方式 2 new File(File parent,String child) //根据父目录文件+子路径构建
// e:\\news2.txt
@Test
public void create02() {
File parentFile = new File("e:\\");
String fileName = "news2.txt";
File file = new File(parentFile, fileName);
try {
file.createNewFile();
System.out.println("创建成功~");
} catch (IOException e) {
e.printStackTrace();
}
}

// 方式 3 new File(String parent,String child) //根据父目录+子路径构建
@Test
public void create03() {
String parentPath = "e:\\";
String fileName = "news4.txt";
File file = new File(parentPath, fileName);
try {
file.createNewFile();
System.out.println("创建成功~");
} catch (IOException e) {
e.printStackTrace();
}
}

19.2.2 获取文件相关信息

image-20231127094041249

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void info() {
//先创建文件对象
File file = new File("e:\\news1.txt");
//getName、getAbsolutePath、getParent、length、exists、isFile、isDirectory
System.out.println("文件名字=" + file.getName());
System.out.println("文件绝对路径=" + file.getAbsolutePath());
System.out.println("文件父级目录=" + file.getParent());
System.out.println("文件大小(字节)=" + file.length());
System.out.println("文件是否存在=" + file.exists());
System.out.println("是不是一个文件=" + file.isFile());
System.out.println("是不是一个目录=" + file.isDirectory());
}

19.2.3 目录的操作和文件删除

image-20231127094541531

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 判断文件是否存在,存在则删除
@Test
public void m1() {
String filePath = "d:\\news1.txt";
File file = new File(filePath);
if (file.exists()) {
if (file.delete()){
System.out.println("删除成功");
} else {
System.out.println("删除失败");
}
} else {
System.out.println("文件不存在");
}
}

// 判断目录是否存在,存在则删除,不存在则创建
// 目录也是一种文件
@Test
public void m2() {
String filePath = "d:\\demo";
File file = new File(filePath);
if (file.exists()) {
if (file.delete()){
System.out.println("删除成功");
} else {
System.out.println("删除失败");
}
} else {
System.out.println("目录不存在");
if (file.mkdirs()) {
System.out.println("目录创建成功");
}
}
}

19.3 IO流原理和分类

19.3.1 IO流原理

image-20231127095822070

19.3.2 流的分类

image-20231127100030596

19.3.3 IO流体系图

【IO系列】一篇带你读懂java中的IO流!

javaIO流体系图

19.4 IO流常用的类

19.4.1 FileInputStream

image-20231127101619906

image-20231127104542559

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 单个字节的读取,效率比较低
*/
@Test
public void readFile01() {
String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\hello.txt";
int readData = 0;
FileInputStream fileInputStream = null;
try {
//创建 FileInputStream 对象,用于读取文件
fileInputStream = new FileInputStream(filePath);
//从该输入流读取一个字节的数据。 如果没有输入可用,此方法将阻止。
//如果返回-1 , 表示读取完毕
while ((readData = fileInputStream.read()) != -1) {
System.out.print((char) readData); //转成 char 显示
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭文件流,释放资源.
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 使用 read(byte[] b)
* 字节数组读取文件,提高效率
*/
@Test
public void readFile02() {
String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\hello.txt";
//字节数组
byte[] buf = new byte[8]; //一次读取 8 个字节.
int readLen = 0;
FileInputStream fileInputStream = null;
try {
//创建 FileInputStream 对象,用于读取 文件
fileInputStream = new FileInputStream(filePath);
//从该输入流读取最多 b.length 字节的数据到字节数组。 此方法将阻塞,直到某些输入可用。
//如果返回-1 , 表示读取完毕
//如果读取正常, 返回实际读取的字节数 <=8
while ((readLen = fileInputStream.read(buf)) != -1) {
System.out.print(new String(buf, 0, readLen));//显示
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭文件流,释放资源.
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

19.4.2 FileOutputStream

image-20231127104815075

image-20231127105032806

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 演示使用 FileOutputStream 将数据写到文件中, * 如果该文件不存在,则创建该文件
*/
@Test
public void writeFile() {
//创建 FileOutputStream 对象
String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\a.txt";
FileOutputStream fileOutputStream = null;
try {
//得到 FileOutputStream 对象
//1. new FileOutputStream(filePath) 创建方式,当写入内容是,会覆盖原来的内容
//2. new FileOutputStream(filePath, true) 创建方式,当写入内容是,是追加到文件后面
fileOutputStream = new FileOutputStream(filePath, true);
//写入一个字节
fileOutputStream.write('H');
//写入字符串
String str = "hello,world!";
//str.getBytes() 可以把 字符串 -> 字节数组
//fileOutputStream.write(str.getBytes());
fileOutputStream.write(str.getBytes(), 0, 12);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
*文件拷贝
*/
@Test
public void fileCopy() {
//1. 创建文件的输入流 , 将文件读入到程序
//2. 创建文件的输出流, 将读取到的文件数据,写入到指定的文件.
String srcFilePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\img.png";
String destFilePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\img1.png";
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
fileInputStream = new FileInputStream(srcFilePath);
fileOutputStream = new FileOutputStream(destFilePath);
//定义一个字节数组,提高读取效果
byte[] buf = new byte[1024];
int readLen = 0;
while ((readLen = fileInputStream.read(buf)) != -1) {
//读取到后,就写入到文件 通过 fileOutputStream
//即,是一边读,一边写
fileOutputStream.write(buf, 0, readLen);//一定要使用这个方法
}
System.out.println("拷贝完成!");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭输入流和输出流,释放资源
if (fileInputStream != null) {
fileInputStream.close();
}
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

19.4.3 FileReader

image-20231128093504951

image-20231128093627408

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 单个字符读取文件
*/
@Test
public void readFile01() {
String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\story.txt";
FileReader fileReader = null;
int readData = 0;
try {
//1. 创建 FileReader 对象
fileReader = new FileReader(filePath);
//循环读取 使用 read, 单个字符读取
while ((readData = fileReader.read()) != -1) {
System.out.print((char) readData);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileReader != null) {
fileReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 字符数组读取文件
*/
@Test
public void readFile02() {
String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\story.txt";
FileReader fileReader = null;
int readLen = 0;
char[] buf = new char[8];
try {
//1. 创建 FileReader 对象
fileReader = new FileReader(filePath);
//循环读取 使用 read(buf), 返回的是实际读取到的字符数
//如果返回-1, 说明到文件结束
while ((readLen = fileReader.read(buf)) != -1) {
System.out.print(new String(buf, 0, readLen));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileReader != null) {
fileReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

19.4.4 FileWriter

image-20231128093836575

image-20231128093932631

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Test
public void fileWriter01() {
String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\note.txt";
//创建 FileWriter 对象
FileWriter fileWriter = null;
char[] chars = {'a', 'b', 'c'};
try {
fileWriter = new FileWriter(filePath);//默认是覆盖写入
// 3) write(int):写入单个字符
fileWriter.write('H');
// 4) write(char[]):写入指定数组
fileWriter.write(chars);
// 5) write(char[],off,len):写入指定数组的指定部分
fileWriter.write("韩顺平教育".toCharArray(), 0, 3);
// 6) write(string):写入整个字符串
fileWriter.write(" 你好北京~");
fileWriter.write("风雨之后,定见彩虹");
// 7) write(string,off,len):写入字符串的指定部分
fileWriter.write("上海天津", 0, 2);
//在数据量大的情况下,可以使用循环操作.
} catch (IOException e) {
e.printStackTrace();
} finally {
//对应 FileWriter , 一定要关闭流,或者 flush 才能真正的把数据写入到文件
try {
//fileWriter.flush();
//关闭文件流,等价 flush() + 关闭
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("程序结束...");
}

19.5 节点流和处理流

19.5.1 基本介绍

image-20231128100629711

  • 一览图

    字节流和处理流

  • 节点流和处理流的区别和联系

    image-20231128101924537

    image-20231128102740720

  • 处理流的功能主要体现在以下两个方面:

    • 性能的提高: 主要以增加缓冲的方式来提高输入输出的效率.
    • 操作的便捷: 处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便

19.5.2 缓冲流-BufferedReader 和 BufferedWriter

  1. 使用BufferedReader读取文本文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static void main(String[] args) throws IOException {
    String filePath = "e:\\a.java";
    //创建 bufferedReader
    BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
    //读取
    String line; //按行读取, 效率高
    //说明
    //1. bufferedReader.readLine() 是按行读取文件
    //2. 当返回 null 时,表示文件读取完毕
    while ((line = bufferedReader.readLine()) != null) {
    System.out.println(line);
    }
    //关闭流, 这里注意,只需要关闭 BufferedReader ,因为底层会自动地去关闭 节点流
    bufferedReader.close();
    }
  2. 使用BufferedWriter写入文本文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static void main(String[] args) throws IOException {
    String filePath = "e:\\ok.txt";
    //创建 BufferedWriter
    //说明:
    //1. new FileWriter(filePath, true) 表示以追加的方式写入
    //2. new FileWriter(filePath) , 表示以覆盖的方式写入
    BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath));
    bufferedWriter.write("hello, 韩顺平教育!");
    bufferedWriter.newLine();//插入一个和系统相关的换行
    bufferedWriter.write("hello2, 韩顺平教育!");
    bufferedWriter.newLine();
    bufferedWriter.write("hello3, 韩顺平教育!");
    bufferedWriter.newLine();
    //说明:关闭外层流即可 , 传入的 new FileWriter(filePath) ,会在底层关闭
    bufferedWriter.close();
    }
  3. 拷贝文本文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static void main(String[] args) throws IOException {
    // 不要去操作 二进制文件[声音,视频,doc, pdf ], 可能造成文件损坏
    String srcPath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\story.txt";
    String desPath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\story2.txt";

    BufferedReader bufferedReader = null;
    BufferedWriter bufferedWriter = null;

    bufferedReader = new BufferedReader(new FileReader(srcPath));
    bufferedWriter = new BufferedWriter(new FileWriter(desPath));
    String line = null;
    while((line = bufferedReader.readLine()) != null) {
    bufferedWriter.write(line);
    }
    bufferedReader.close();
    bufferedWriter.close();
    }
    }

19.5.3 缓冲流-BufferedInputStream 和 BufferedOutputStream

  • 字节流可以操作二进制文件,也可以操作文本文件

  • 拷贝图片、音频文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public static void main(String[] args) throws IOException {
    String srcPath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\img.png";
    String desPath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\img2.png";

    BufferedInputStream bufferedInputStream = null;
    BufferedOutputStream bufferedOutputStream = null;

    bufferedInputStream = new BufferedInputStream(new FileInputStream(srcPath));
    bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(desPath));

    byte[] buff = new byte[1024];
    int readLen = 0;
    while ((readLen = bufferedInputStream.read(buff)) != -1) {
    bufferedOutputStream.write(buff, 0, readLen);
    }

    bufferedInputStream.close();
    bufferedOutputStream.close();
    }

19.5.4 序列化和反序列化

  • 看一个需求

    image-20231129100806911

    image-20231129101412433

  • 序列化和反序列化

    image-20231129101022414

19.5.5 对象流-ObjectInputStream 和 ObjectOutputStream

  • 提供了对基本类型或对象类型的序列化和反序列化的方法

    • ObjectOutputStream 提供 序列化功能
    • ObjectInputStream 提供 反序列化功能

    image-20231129101606079

  • 演示 ObjectOutputStream 的使用, 完成数据的序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class ObjectOutputStream_ {
    public static void main(String[] args) throws IOException {
    //序列化后,保存的文件格式,不是存文本,而是按照他的格式来保存
    String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\data.dat";

    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
    //序列化数据
    oos.writeInt(100);// int -> Integer (实现了 Serializable)
    oos.writeBoolean(true);// boolean -> Boolean (实现了 Serializable)
    oos.writeChar('a');// char -> Character (实现了 Serializable)
    oos.writeDouble(9.5);// double -> Double (实现了 Serializable)
    oos.writeUTF("韩顺平教育");//String
    //保存一个 dog 对象
    oos.writeObject(new Dog("旺财", 10));
    oos.close();
    }
    }

    class Dog implements Serializable {
    private String name;
    private int age;

    public Dog(String name, int age) {
    this.name = name;
    this.age = age;
    }

    @Override
    public String toString() {
    return "Dog{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }
  • 演示 ObjectInputStream的使用,完成数据的反序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class ObjectInputStream_ {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
    // 1.创建流对象
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src\\data.dat"));
    // 2.读取, 注意顺序和保存数据一致
    System.out.println(ois.readInt());
    System.out.println(ois.readBoolean());
    System.out.println(ois.readChar());
    System.out.println(ois.readDouble());
    System.out.println(ois.readUTF());
    Object o = ois.readObject();
    System.out.println("运行类型=" + o.getClass()); //class com.durango.outputstream.Dog
    System.out.println("Dog信息" + o); //Dog信息Dog{name='旺财', age=10}
    // 3.关闭
    ois.close();
    System.out.println("以反序列化的方式读取(恢复)ok~");
    }
    }
  • 注意事项
    1. 读写顺序要一致
    2. 要求序列化或反序列化对象,需要实现可序列化
    3. 序列化的类中建议添加SerialVersionUID,为了提高版本的兼容性
    4. 序列化对象时,默认将里面所有属性都进行序列化,但除了static或transient修饰的成员
    5. 序列化对象时,要求里面属性的数据类型也需要实现序列化接口
    6. 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化

19.5.6 标准输入输出流

image-20231129104244722

1
2
3
4
5
6
7
8
9
10
11
// System.in 标准输入 - 键盘
// System 类的 public final static InputStream in = null;
// System.in的编译类型 InputStream
// System.in的运行类型 BufferedInputStream
System.out.println(System.in.getClass());

// System.out 标准输出 - 显示器
// System 类的 public final static PrintStream out = null;
// System.in的编译类型 PrintStream
// System.in的运行类型 PrintStream
System.out.println(System.out.getClass());

19.5.7 转换流-InputStreamReader 和 OutputStreamWriter

  • 一个编码问题
    • 默认情况下,字符流读取文件是按照 utf-8 编码,无法指定编码
    • 字节流可以指定编码
  • image-20231129105841034
  • 演示:将字节流 FileInputStream 转成字符流 InputStreamReader, 指定编码 gbk/utf-8

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public static void main(String[] args) throws IOException {
    String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\note.txt";
    //1. 把 FileInputStream 转成 InputStreamReader
    //2. 指定编码 gbk
    //3. 把 InputStreamReader 传入 BufferedReader
    //InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk");
    //BufferedReader br = new BufferedReader(isr);
    //将 2 和 3 合在一起
    BufferedReader br = new BufferedReader(new InputStreamReader(
    new FileInputStream(filePath), "gbk"));

    //4. 读取
    String s;
    while ((s = br.readLine()) != null) {
    System.out.println("读取内容=" + s);
    }
    //5. 关闭外层流
    br.close();
    }
  • 演示:编程将字节流FileOutputStream包装成(转换成)字符流OutputStreamWriter,对文件进行写入(按照GBK格式,可以指定其他,比如utf-8)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void main(String[] args) throws IOException {
    // 1.创建流对象
    String filePath = "D:\\Code\\IdeaJavaProjects\\chapter19\\src\\note2.txt";
    OutputStreamWriter osw =
    new OutputStreamWriter(new FileOutputStream(filePath), "gbk");
    // 2.写入
    osw.write("hello,韩顺平教育~");
    // 3.关闭
    osw.close();
    System.out.println("保存成功~");
    }

19.5.8 打印流-PrintStream 和 PrintWriter

  • 继承关系图

    image-20231130093409020

  • ​ 案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static void main(String[] args) throws IOException {
    PrintStream out = System.out;
    //在默认情况下,PrintStream 输出数据的位置是 标准输出,即显示器
    out.print("john, hello");
    //因为 print 底层使用的是 write , 所以我们可以直接调用 write 进行打印/输出
    out.write("韩顺平,你好".getBytes());
    out.close();

    //我们可以去修改打印流输出的位置/设备
    //1. 输出修改成 "e:\\f1.txt"
    // public static void setOut(PrintStream out) {
    // checkIO();
    // setOut0(out); // native 方法,修改了 out
    // }
    System.setOut(new PrintStream("e:\\f1.txt"));
    System.out.println("hello, 韩顺平教育~");

    }
    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args) throws IOException {
    PrintWriter printWriter = new PrintWriter(System.out);
    PrintWriter printWriter1 = new PrintWriter(new FileWriter("e:\\f1.txt"));
    printWriter.println("hi,你好");
    printWriter1.println("hello,world!");
    printWriter.close();
    printWriter1.close();
    }

19.6 Properties 类

19.6.1 基本介绍

image-20231130100339284

19.6.2 常用方法

image-20231130100503751

19.6.3 案例

image-20231130100601965

1
2
3
4
5
6
7
8
9
10
11
12
//使用 Properties 类来读取 mysql.properties 文件
//1. 创建 Properties 对象
Properties properties = new Properties();
//2. 加载指定配置文件
properties.load(new FileReader("src\\mysql.properties"));
//3. 把 k-v 显示到控制台
properties.list(System.out);
//4. 根据 key 获取对应的值
String user = properties.getProperty("user");
String pwd = properties.getProperty("pwd");
System.out.println("用户名=" + user);
System.out.println("密码是=" + pwd);
1
2
3
4
5
6
7
8
9
10
11
//使用 Properties 类来创建 配置文件, 修改配置文件内容
Properties properties = new Properties();
//创建
//1.如果该文件没有 key 就是创建
//2.如果该文件有 key ,就是修改
properties.setProperty("charset", "utf8");
properties.setProperty("user", "汤姆");//注意保存时,是中文的 unicode 码值
properties.setProperty("pwd", "888888");
//将 k-v 存储文件中即可
properties.store(new FileOutputStream("src\\mysql2.properties"), "this is a comment");
System.out.println("保存配置文件成功~");

第二十章 坦克大战【3】

20.1 坦克大战0.6

image-20231201092439847

20.1.1 防止坦克重叠

image-20231201093358055

  • Enemy.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    public class Enemy extends Tank implements Runnable {
    ...
    // 增加成员enemies,可以得到敌人坦克的Vector(在MyPanel中创建的)
    Vector<Enemy> enemies = new Vector<>();
    // 编写方法,判断当前坦克是否和其他Enemy坦克发生碰撞

    public boolean isTouchEnemy() {
    // 判断当前坦克方向
    switch (this.getDirect()) {
    case 0: //上
    for (int i = 0; i < enemies.size(); i++) {
    //遍历Vector中所有敌人坦克
    Enemy enemy = enemies.get(i);
    if (enemy != this) {
    int direct = enemy.getDirect();
    //1. 敌人坦克上下,x范围[x,x+40],y范围[y,y+60]
    if (direct == 0 || direct == 2) {
    //当前坦克左上角[x,y],右上角[x+40,y]
    if (this.getX() >= enemy.getX()
    && this.getX() <= enemy.getX() + 40
    && this.getY() >= enemy.getY()
    && this.getY() <= enemy.getY() + 60) {
    return true;
    }
    if (this.getX() + 40 >= enemy.getX()
    && this.getX() + 40 <= enemy.getX() + 40
    && this.getY() >= enemy.getY()
    && this.getY() <= enemy.getY() + 60) {
    return true;
    }
    }
    //2. 敌人坦克左右,x范围[x,x+60],y范围[y,y+40]
    if (direct == 1 || direct == 3) {
    //当前坦克左上角[x,y],右上角[x+40,y]
    if (this.getX() >= enemy.getX()
    && this.getX() <= enemy.getX() + 60
    && this.getY() >= enemy.getY()
    && this.getY() <= enemy.getY() + 40) {
    return true;
    }
    if (this.getX() + 40 >= enemy.getX()
    && this.getX() + 40 <= enemy.getX() + 60
    && this.getY() >= enemy.getY()
    && this.getY() <= enemy.getY() + 40) {
    return true;
    }
    }
    }
    }
    break;
    case 1: //右
    ...
    break;
    case 2: //下
    ...
    break;
    case 3: //左
    ...
    break;
    }
    return false;
    }

    public void move(int direct, int step) {
    for (int i = 0; i < step; i++) {
    ...
    switch (direct) {
    case 0: //上
    if (getY() > 0 && !isTouchEnemy()) {
    moveUp();
    }
    ...
    }
    }
    }

  • MyPanel.java

    1
    2
    3
    4
    5
    6
    //初始化敌人坦克
    for (int i = 0; i < enemySize; i++) {
    ...
    // 设置Enemy的成员Vector<Enemy> enemies
    enemy.setEnemies(enemies);
    }

20.1.2 记录总成绩

image-20231201102934955

  • Recorder.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class Recorder {
    // 击毁坦克数
    private static int allEnemyNum = 0;
    // 定义IO对象,写数据到文件
    private static FileWriter fw = null;
    private static BufferedWriter bw = null;
    private static String recordFile = "src\\myRecord.txt";

    // 当我方坦克击毁一辆敌方坦克,allEnemyNum++
    public static void addAllEnemyNum() {
    Recorder.allEnemyNum++;
    }

    // 游戏退出时,保存信息
    public static void keepRecord() {
    try {
    bw = new BufferedWriter(new FileWriter(recordFile));
    bw.write(allEnemyNum + "\r\n");
    } catch (IOException e) {
    throw new RuntimeException(e);
    } finally {
    try {
    if (bw != null) {
    bw.close();
    }
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }
    }

    public static int getAllEnemyNum() {
    return allEnemyNum;
    }

    public static void setAllEnemyNum(int allEnemyNum) {
    Recorder.allEnemyNum = allEnemyNum;
    }

    }
  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public void hitTank(Shot s, Tank tank) {
    // 判断s 击中坦克
    switch (tank.getDirect()) {
    case 0: //坦克向上
    case 2: //坦克向下
    if (s.x > tank.getX() && s.x < tank.getX() + 40
    && s.y > tank.getY() && s.y < tank.getY() + 60) {
    ...
    if (tank instanceof Enemy) {
    enemies.remove(tank); //将敌人坦克从Vector中删除
    Recorder.addAllEnemyNum();
    }
    ...
    }
    break;
    ...
    }
    }
  • TankGame06.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public TankGame06() {
    ...
    // 在JFame中增加窗口监听
    this.addWindowListener(new WindowAdapter() {
    @Override
    public void windowClosing(WindowEvent e) {
    System.out.println("关闭窗口");
    Recorder.keepRecord();
    System.exit(0);
    }
    });
    }

20.1.3 记录敌人信息

  • Recorder.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class Recorder {
    ...
    // 定义Vector指向MyPane对象的 enemies
    private static Vector<Enemy> enemies = null;

    // 游戏退出时,保存信息
    public static void keepRecord() {
    try {
    bw = new BufferedWriter(new FileWriter(recordFile));
    // 保存成绩
    bw.write(allEnemyNum + "\r\n");
    // 遍历敌人坦克Vector,保存敌人信息
    for (int i = 0; i < enemies.size(); i++) {
    Enemy enemy = enemies.get(i);
    if (enemy.isLive) {
    String record = enemy.getX() + " " + enemy.getY() + " " + enemy.getDirect();
    bw.write(record+ "\r\n");
    }
    }
    }
    ...
    }
    ...
    public static void setEnemies(Vector<Enemy> enemies) {
    Recorder.enemies = enemies;
    }
    }
  • MyPanel.java

    1
    2
    3
    4
    5
    public MyPanel() {
    // Recorder获取到enemies,用于保存信息
    Recorder.setEnemies(enemies);
    ...
    }

20.1.4 恢复敌人信息

  • Node.java

    1
    2
    3
    4
    5
    6
    public class Node {
    private int x;
    private int y;
    private int direct;
    ...
    }
  • Recorder.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class Recorder {
    ...
    // 定义Vector保存敌人的信息node
    private static Vector<Node> nodes = new Vector<>();

    // 增加一个方法,用于读取recordFile,恢复相关信息
    // 继续上局游戏时调用
    public static Vector<Node> getNodesAndEnemyRec() {
    try {
    br = new BufferedReader(new FileReader(recordFile));
    allEnemyNum = Integer.parseInt(br.readLine());
    // 循环读取,生成nodes集合
    String line = "";
    while ((line = br.readLine()) != null) {
    String[] xyd = line.split(" ");
    Node node = new Node(Integer.parseInt(xyd[0]), Integer.parseInt(xyd[1]),
    Integer.parseInt(xyd[2]));
    nodes.add(node);
    }
    } catch (IOException e) {
    throw new RuntimeException(e);
    } finally {
    try {
    if (br != null) {
    br.close();
    }
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }
    return nodes;
    }
    ...
    }
  • TankGame06.java

    1
    2
    3
    4
    5
    6
    public TankGame06() {
    System.out.println("请输入选择:1:新游戏;2:继续游戏");
    String key = scanner.next();
    mp = new MyPanel(key);
    ...
    }
  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    public class MyPanel extends JPanel implements KeyListener, Runnable { //自定义画板类
    // 定义一个存放Node对象的Vector,用于恢复坦克信息
    Vector<Node> nodes = new Vector<>();

    ...
    public MyPanel(String key) {
    nodes = Recorder.getNodesAndEnemyRec();
    ...
    switch (key) {
    case "1": // 新游戏
    Recorder.setAllEnemyNum(0);
    //初始化敌人坦克
    for (int i = 0; i < enemySize; i++) {
    Enemy enemy = new Enemy(100 * (i + 1), 0);
    enemy.setDirect(2); // 设置初始炮筒方向
    new Thread(enemy).start(); //启动Enemy线程,让坦克自由移动

    // 给该enemy对象初始化Shot对象
    Shot shot = new Shot(enemy.getX() + 20, enemy.getY() + 60, enemy.getDirect());
    enemy.shots.add(shot);
    //启动Shot线程
    new Thread(shot).start();
    enemies.add(enemy);
    // 设置Enemy的成员Vector<Enemy> enemies
    enemy.setEnemies(enemies);
    }
    break;
    case "2": // 继续游戏
    for (int i = 0; i < nodes.size(); i++) {
    Node node = nodes.get(i);
    Enemy enemy = new Enemy(node.getX(), node.getY());
    enemy.setDirect(node.getDirect()); // 设置初始炮筒方向
    new Thread(enemy).start(); //启动Enemy线程,让坦克自由移动

    // 给该enemy对象初始化Shot对象
    Shot shot = new Shot(enemy.getX() + 20, enemy.getY() + 60, enemy.getDirect());
    enemy.shots.add(shot);
    //启动Shot线程
    new Thread(shot).start();
    enemies.add(enemy);
    // 设置Enemy的成员Vector<Enemy> enemies
    enemy.setEnemies(enemies);
    }
    break;
    default:
    System.out.println("输入有误!");
    }
    ...
    }
    ...
    }

20.2 坦克大战0.7

image-20231202104735994

20.2.1 播放音乐

  • AePlayWave.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class TankGame07 extends JFrame {
    private MyPanel mp = null;
    static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
    new TankGame07();
    }

    public TankGame07() {
    System.out.println("请输入选择:1:新游戏;2:继续游戏");
    String key = scanner.next();
    mp = new MyPanel(key);
    // 将mp放入Thread并启动
    Thread thread = new Thread(mp);
    thread.start();
    this.add(mp);
    this.addKeyListener(mp);
    this.setSize(1300, 750);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setVisible(true);

    // 在JFame中增加窗口监听
    this.addWindowListener(new WindowAdapter() {
    @Override
    public void windowClosing(WindowEvent e) {
    System.out.println("关闭窗口");
    Recorder.keepRecord();
    System.exit(0);
    }
    });
    }
    }
  • MyPanel.java

    1
    2
    3
    4
    5
    public MyPanel(String key) {
    ...
    // 游戏初始化后,播放音乐
    new AePlayWave("src\\111.wav").start();
    }

20.2.2 处理文件相关异常

  • MyPanel.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public MyPanel(String key) {
    // 判断记录文件是否存在
    // 不存在则只能开启"新游戏" key = "1"
    File file = new File(Recorder.getRecordFile());
    if (file.exists()) {
    nodes = Recorder.getNodesAndEnemyRec();
    } else {
    System.out.println("没有存档,只能开启新游戏");
    key = "1";
    }
    ...
    }

— 第三阶段 —

第二十一章 网络编程

21.1 网络的相关概念

21.1.1 网络通讯

image-20231204101445466

21.1.2 网络

image-20231204101507849

21.1.3 IP地址

image-20231204101613377

21.1.4 ipv4 地址分类

image-20231204102429075

image-20231204102445872

21.1.5 域名和端口

image-20231204102903300

21.1.6 TCP/IP协议

  • TCP/IP(Transmission Control Protocol/Internet Protocol)的简写中文译名为 传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是 Internet 最基本的协议、国际互联网络的基础,简单地说,就是由网络层的IP协议和传输层的TCP协议组成的。

image-20231204103420610

21.1.7 网络通信协议

image-20231204104003243

21.1.8 TCP 和 UDP

image-20231204104325582

21.2 InetAddress 类

21.2.1 相关方法

  1. getLocalhost:获取本机InetAddress对象
  2. getByName:根据指定主机名/域名获取InetAddress对象
  3. getHostName:获取InetAddress对象的主机名
  4. getHostAddress:获取InetAddress对象的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获取本机 InetAddress 对象 getLocalHost
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost); //Durango/172.25.153.218

//根据指定主机名/域名获取 InetAddress对象 getByName
InetAddress host2 = InetAddress.getByName("Durango");
System.out.println(host2); //Durango/172.25.153.218
InetAddress host3 = InetAddress.getByName("www.durango.cn");
System.out.println(host3); //www.durango.cn/185.199.108.153

//获取 InetAddress 对象的主机名 getHostName
String host3Name = host3.getHostName();
System.out.println(host3Name); //www.durango.cn

//获取 InetAddress 对象的地址 getHostAddress
String host3Address = host3.getHostAddress();
System.out.println(host3Address); //185.199.108.153

21.3 Socket套接字

21.3.1 基本介绍

image-20231204110318452

image-20231204110548528

21.4 TCP网络通信编程

21.4.1 基本介绍和流程

image-20231204110802157

注意事项:

  • 思路流程图很重要!
  • 数据发送完毕后记得设置结束标记
  • 最后要关闭相关的流

21.4.2 应用案例 1(使用字节流)

image-20231204111017264

image-20231204111528687

注意!设置结束标记 socket.shutdownOutput();

  • SocketTCP01Server.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class SocketTCP01Server {
    public static void main(String[] args) throws IOException {
    //1. 在本机 的 9999 端口监听, 等待连接
    ServerSocket serverSocket = new ServerSocket(9999);
    System.out.println("服务端,在 9999 端口监听,等待连接..");

    //2. 当没有客户端连接 9999 端口时,程序会阻塞, 等待连接
    // 如果有客户端连接,则会返回 Socket 对象,程序继续
    Socket socket = serverSocket.accept();
    System.out.println("客户端 socket =" + socket.getClass() +"已连接!");

    //3. 通过 socket.getInputStream() 读取客户端写入到数据通道的数据, 显示
    InputStream inputStream = socket.getInputStream();
    //4. IO 读取
    byte[] buf = new byte[1024];
    int readLen = 0;
    while ((readLen = inputStream.read(buf)) != -1) {
    System.out.println(new String(buf, 0, readLen));
    }

    //5.关闭流和 socket
    inputStream.close();
    socket.close();
    serverSocket.close();//关闭
    System.out.println("服务端退出.....");
    }
    }
  • SocketTCP01Client.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class SocketTCP01Client {
    public static void main(String[] args) throws IOException {
    //1. 连接服务端 (ip , 端口)
    //解读: 连接本机的 9999 端口, 如果连接成功,返回 Socket 对象
    Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
    System.out.println("客户端 socket 返回=" + socket.getClass());

    //2. 连接上后,生成 Socket, 通过 socket.getOutputStream()
    // 得到 和 socket 对象关联的输出流对象
    OutputStream outputStream = socket.getOutputStream();

    //3. 通过输出流,写入数据到 数据通道
    outputStream.write("hello, server".getBytes());

    //4. 关闭流对象和 socket, 必须关闭
    outputStream.close();
    socket.close();
    System.out.println("客户端退出.....");
    }
    }

21.4.3 应用案例 2(使用字节流)

image-20231204135824646

  • SocketTCP02Server.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class SocketTCP02Server {
    public static void main(String[] args) throws IOException {
    //1. 在本机 的 9999 端口监听, 等待连接
    ServerSocket serverSocket = new ServerSocket(9999);
    System.out.println("服务端,在 9999 端口监听,等待连接..");

    //2. 当没有客户端连接 9999 端口时,程序会阻塞, 等待连接
    // 如果有客户端连接,则会返回 Socket 对象,程序继续
    Socket socket = serverSocket.accept();
    System.out.println("服务端 socket 返回=" + socket.getClass());

    //3. 通过 socket.getInputStream() 读取客户端写入到数据通道的数据, 显示
    InputStream inputStream = socket.getInputStream();
    //4. IO 读取
    byte[] buf = new byte[1024];
    int readLen = 0;
    while ((readLen = inputStream.read(buf)) != -1) {
    System.out.println(new String(buf, 0, readLen));
    }

    //5. 获取 socket 相关联的输出流
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write("hello, client".getBytes());
    // 设置结束标记
    socket.shutdownOutput();

    //6.关闭流和 socket
    inputStream.close();
    outputStream.close();
    socket.close();
    serverSocket.close();//关闭
    System.out.println("服务端退出.....");
    }
    }
  • SocketTCP02Client.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class SocketTCP02Client {
    public static void main(String[] args) throws IOException {
    //1. 连接服务端 (ip , 端口)
    //解读: 连接本机的 9999 端口, 如果连接成功,返回 Socket 对象
    Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
    System.out.println("客户端 socket 返回=" + socket.getClass());

    //2. 连接上后,生成 Socket, 通过 socket.getOutputStream()
    // 得到 和 socket 对象关联的输出流对象
    OutputStream outputStream = socket.getOutputStream();

    //3. 通过输出流,写入数据到 数据通道
    outputStream.write("hello, server".getBytes());
    // 设置结束标记
    socket.shutdownOutput();

    //4. 获取和 socket 关联的输入流. 读取数据(字节),并显示
    InputStream inputStream = socket.getInputStream();
    byte[] buf = new byte[1024];
    int readLen = 0;
    while ((readLen = inputStream.read(buf)) != -1) {
    System.out.println(new String(buf, 0, readLen));
    }

    //5. 关闭流对象和 socket, 必须关闭
    inputStream.close();
    outputStream.close();
    socket.close();
    System.out.println("客户端退出.....");

    }
    }

21.4.4 应用案例 3(使用字符流)

image-20231204140444469

注意!bufferedWriter.newLine();和bufferedWriter.flush();

  • SocketTCP03Server.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //1. 在本机 的 9999 端口监听, 等待连接
    ServerSocket serverSocket = new ServerSocket(9999);
    System.out.println("服务端,在 9999 端口监听,等待连接..");

    //2. 当没有客户端连接 9999 端口时,程序会阻塞, 等待连接
    // 如果有客户端连接,则会返回 Socket 对象,程序继续
    Socket socket = serverSocket.accept();
    System.out.println("服务端 socket 返回=" + socket.getClass());

    //3. 通过 socket.getInputStream() 读取客户端写入到数据通道的数据, 显示
    InputStream inputStream = socket.getInputStream();

    //4. IO 读取, 使用字符流, 使用 InputStreamReader 将 inputStream 转成字符流
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    String s = bufferedReader.readLine();
    System.out.println(s);//输出

    //5. 获取 socket 相关联的输出流
    OutputStream outputStream = socket.getOutputStream();
    // 使用字符输出流的方式回复信息 使用 OutputStreamWriter 将 outputStream 转换成字符流
    BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
    bufferedWriter.write("hello client 字符流");
    bufferedWriter.newLine();// 插入一个换行符,表示回复内容的结束!!! 相当于socket.shutdownOutput()
    bufferedWriter.flush(); //注意需要手动的 flush!!!

    //6.关闭流和 socket
    bufferedReader.close();
    bufferedWriter.close();
    socket.close();
    serverSocket.close();//关闭
    System.out.println("服务端退出.....");
  • SocketTCP03Client.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //1. 连接服务端 (ip , 端口)
    //解读: 连接本机的 9999 端口, 如果连接成功,返回 Socket 对象
    Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
    System.out.println("客户端 socket 返回=" + socket.getClass());

    //2. 连接上后,生成 Socket, 通过 socket.getOutputStream()
    // 得到 和 socket 对象关联的输出流对象
    OutputStream outputStream = socket.getOutputStream();

    //3. 通过输出流,写入数据到 数据通道, 使用字符流
    BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
    bufferedWriter.write("hello, server 字符流");
    bufferedWriter.newLine();//插入一个换行符,表示写入的内容结束, 注意,要求对方使用 readLine()!!!!
    bufferedWriter.flush();// 如果使用的字符流,需要手动刷新,否则数据不会写入数据通道

    //4. 获取和 socket 关联的输入流. 读取数据(字符),并显示
    InputStream inputStream = socket.getInputStream();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    String s = bufferedReader.readLine();
    System.out.println(s);

    //5. 关闭流对象和 socket, 必须关闭
    bufferedReader.close();//关闭外层流
    bufferedWriter.close();
    socket.close();
    System.out.println("客户端退出.....");

21.4.5 应用案例 4(上传图片)

image-20231205092658640

image-20231205102621085

  • StreamUtils.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class StreamUtils {
    //功能:将输入流转换成字节数组 byte[]
    public static byte[] streamToByteArray(InputStream is) throws Exception {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();//创建输出流对象
    byte[] b = new byte[1024]; //字节数组
    int len;
    while ((len = is.read(b)) != -1) {
    bos.write(b, 0, len);
    }
    byte[] array = bos.toByteArray();
    bos.close();
    return array;
    }

    //功能:将 InputStream 转换成 String
    public static String streamToString(InputStream is) throws Exception {
    BufferedReader reader = new BufferedReader(new InputStreamReader(is));
    StringBuilder builder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) { //当读取到 null 时,就表示结束
    builder.append(line + "\r\n");
    }
    return builder.toString();
    }
    }
  • TCPFileUploadServer.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class TCPFileUploadServer {
    public static void main(String[] args) throws Exception {
    // 1. 服务端在本机监听8888端口
    ServerSocket serverSocket = new ServerSocket(8888);
    System.out.println("服务端在8888端口监听,等待连接...");

    // 2. 等待连接
    Socket socket = serverSocket.accept();

    // 3.通过socket获取输入流,读取客户端发送的数据
    BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
    byte[] bytes = StreamUtils.streamToByteArray(bis);

    // 4.将bytes写入到指定路径的文件
    String filePath = "src\\copy.jpg";
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
    bos.write(bytes);

    // 5.通过socket获取输出流,发送“收到图片”(通过字符流)
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
    bw.write("收到图片");
    bw.flush(); //把内容刷新到数据通道
    socket.shutdownOutput();//设置结束标记

    // 6.关闭相关流
    bis.close();
    bos.close();
    bw.close();
    socket.close();
    serverSocket.close();
    }
    }
  • TCPFileUploadClient.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class TCPFileUploadClient {
    public static void main(String[] args) throws Exception {
    // 1. 客户端连接服务端,得到socket对象
    Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
    System.out.println("连接到服务器...");

    // 2. 创建读取磁盘文件的输入流
    String filePath = "D:\\zlx18\\Pictures\\Camera Roll\\绘图1.jpg";
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));

    // 3. 把文件保存到字节数组中
    byte[] bytes = StreamUtils.streamToByteArray(bis);

    // 4.通过socket获取输出流,将bytes发送到服务端
    BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
    bos.write(bytes); //将文件对应的字节内容写入到数据通道
    socket.shutdownOutput(); //设置结束标记

    // 5.通过socket获取输入流,接收服务端回复的消息
    // 使用StreamUtils工具类将输入流转换为String
    String s = StreamUtils.streamToString(socket.getInputStream());
    System.out.println(s);

    // 6.关闭相关流
    bis.close();
    bos.close();
    socket.close();
    }
    }

21.4.6 netstat 指令

image-20231205103402358

image-20231205104119614

21.5 UDP网络通信编程

21.5.1 基本介绍和流程

image-20231205104754694

image-20231205105054222

image-20231205105303498

21.5.2 案例1

image-20231205105332697

  • UDPReceiverA.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class UDPReceiverA {
    public static void main(String[] args) throws IOException {
    //1. 创建一个 DatagramSocket 对象,准备在 9999 接收数据
    DatagramSocket socket = new DatagramSocket(9999);

    //2. 构建一个 (空的)DatagramPacket 对象,准备接收数据
    // 一个数据包最大 64k
    byte[] buf = new byte[1024];
    DatagramPacket packet = new DatagramPacket(buf, buf.length);

    //3. 调用接收方法, 将通过网络传输的 DatagramPacket 对象 填充到 packet 对象
    // 当有数据报发送到 本机的 9999 端口时,就会接收到数据
    // 如果没有数据包发送到 本机的 9999 端口, 就会阻塞等待.
    System.out.println("接收端 A 等待接收数据..");
    socket.receive(packet);

    //4. 把 packet 进行拆包,取出数据,并显示.
    int length = packet.getLength();//实际接收到的数据字节长度
    byte[] data = packet.getData();//接收到数据
    String s = new String(data, 0, length);
    System.out.println(s);

    //===回复信息给 B 端
    //将需要发送的数据,封装到 DatagramPacket 对象
    data = "好的, 明天见".getBytes();
    packet =
    new DatagramPacket(data, data.length, InetAddress.getByName("127.0.0.1"), 9998);
    socket.send(packet);//发送

    //5. 关闭资源
    socket.close();
    System.out.println("A 端退出...");
    }
    }
  • UDPSenderB.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public static void main(String[] args) throws IOException {
    //1.创建 DatagramSocket 对象,准备在 9998 端口 接收数据
    DatagramSocket socket = new DatagramSocket(9998);

    //2. 将需要发送的数据,封装到 DatagramPacket 对象
    byte[] data = "hello 明天吃火锅~".getBytes(); //
    DatagramPacket packet =
    new DatagramPacket(data, data.length, InetAddress.getByName("127.0.0.1"), 9999);
    socket.send(packet);

    //3.=== 接收从 A 端回复的信息
    //(1) 构建一个 DatagramPacket 对象,准备接收数据
    byte[] buf = new byte[1024];
    packet = new DatagramPacket(buf, buf.length);
    //(2) 调用 接收方法, 将通过网络传输的 DatagramPacket 对象填充到 packet 对象
    socket.receive(packet);
    //(3) 可以把 packet 进行拆包,取出数据,并显示.
    int length = packet.getLength();//实际接收到的数据字节长度
    data = packet.getData();//接收到数据
    String s = new String(data, 0, length);
    System.out.println(s);
    //4.关闭资源
    socket.close();
    System.out.println("B 端退出");
    }
    }

21.6 Homeworks

21.6.1 多次对话

image-20231205150951816

  • Homework01Server.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public class Homework01Server {
    public static void main(String[] args) throws IOException {
    ServerSocket serverSocket = new ServerSocket(8080);
    System.out.println("等待客户端连接...");
    Socket socket = serverSocket.accept();
    System.out.println("客户端连接成功!");

    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
    boolean key = true;
    while (key) {
    // 采用字符流
    String s = bufferedReader.readLine();
    System.out.println("客户端:" + s);

    if (s.equals("name")) {
    bufferedWriter.write("我是nova");
    } else if (s.equals("hobby")) {
    bufferedWriter.write("编写java程序");
    } else if (s.equals("bye")) {
    bufferedWriter.write("bye");
    key = false;
    } else {
    bufferedWriter.write("你说什么");
    }
    bufferedWriter.newLine();
    bufferedWriter.flush();
    }

    System.out.println("与客户端对话结束");
    bufferedReader.close();
    bufferedWriter.close();
    socket.close();
    serverSocket.close();
    }
    }
  • Homework01Client.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class Homework01Client {
    public static void main(String[] args) throws IOException {
    Socket socket = new Socket(InetAddress.getLocalHost(), 8080);
    System.out.println("连接到服务端!");
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

    boolean key = true;
    while (key) {
    Scanner scanner = new Scanner(System.in);
    String message = scanner.next();

    bufferedWriter.write(message);
    bufferedWriter.newLine(); // 插入换行符表示结束标志
    bufferedWriter.flush(); // 注意需要手动的 flush!!!

    String s = bufferedReader.readLine();
    System.out.println("服务端:" + s);
    if (s.equals("bye")) {
    key = false;
    }
    }

    System.out.println("与服务端对话结束");
    bufferedReader.close();
    bufferedWriter.close();
    socket.close();
    }
    }

21.6.2 音乐下载

image-20231205153031867

image-20231205195815546

  • Homework03Server.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    public class Homework03Server {
    public static void main(String[] args) throws Exception {
    // 1. 监听端口,等待客户端连接
    ServerSocket serverSocket = new ServerSocket(8888);
    System.out.println("服务端等待连接...");
    Socket socket = serverSocket.accept();
    System.out.println("客户端已连接!");

    // 2. 得到socket关联的输入流,读取客户端要下载的文件名
    InputStream is = socket.getInputStream();
    byte[] b = new byte[1024];
    int len = 0;
    String musicName = "";
    while ((len = is.read(b)) != -1) {
    musicName += new String(b, 0, len);
    }
    System.out.println("客户端想要下载音乐《" + musicName + "》");

    // 3. 判断用户下载的文件是否存在
    String filePath = "src\\music\\" + musicName + ".mp3";
    File file = new File(filePath);
    FileInputStream fileInputStream = null;
    if (file.exists()) {
    fileInputStream = new FileInputStream(file);
    } else {
    fileInputStream = new FileInputStream("src\\music\\无名.mp3");
    }

    // 4.从磁盘读取文件到字节数组
    BufferedInputStream bis2 = new BufferedInputStream(fileInputStream);
    byte[] bytes = StreamUtils.streamToByteArray(bis2);

    // 5. 得到socket关联的输出流,写入数据通道
    BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
    System.out.println("音乐传输中...");
    bos.write(bytes);
    bos.flush();
    System.out.println("音乐传输完毕!");
    socket.shutdownOutput();

    // 6. 关闭相关流
    is.close();
    bis2.close();
    bos.close();
    socket.close();
    serverSocket.close();
    }
    }
  • Homework03Client.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class Homework03Client {
    public static void main(String[] args) throws Exception {
    // 1. 连接服务端
    Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
    System.out.println("连接到服务器");

    // 2. 得到socket关联的输出流,发送要下载的文件名给客户端
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入你想下载的音乐名称:");
    String musicName = scanner.next();
    OutputStream os = socket.getOutputStream();
    os.write(musicName.getBytes());
    socket.shutdownOutput();

    // 3. 得到socket关联的输入流,从数据通道读取数据到字节数组
    String filePath = "src\\music.mp3";
    BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
    byte[] bytes = StreamUtils.streamToByteArray(bis);

    // 4. 将字节数组数据写入文件
    BufferedOutputStream bos2 = new BufferedOutputStream(new FileOutputStream(filePath));
    bos2.write(bytes);
    System.out.println("音乐已经保存");

    // 5. 关闭相关流
    os.close();
    bos2.close();
    bis.close();
    socket.close();

    }
    }

第二十二章 多用户及时通信系统

22.1 相关技术

  • 项目框架设计
  • java面向对象编程
  • 网络编程
  • 多线程
  • IO流
  • mysql/使用集合充当内存数据库

22.2 项目开发流程

  • 需求分析(30%) ==》设计阶段(20%) ==》实现阶段(20%) ==》测试阶段(20%) ==》实施阶段(10%) ==》维护阶段
  • 开发 –> 架构师、项目经理 or 产品经理
  • 项目越大,需求和设计阶段占用时间越多

image-20231206100702907

22.3 需求分析

image-20231206101857544

22.4 界面设计

image-20231206102025331

image-20231206102035230

image-20231206102100889

image-20231206102116587

image-20231206102147145

image-20231206102408675

22.5 功能 - 用户登录

image-20231206103323570

image-20231206103521638

22.6 功能 - 拉取在线用户列表

image-20231211095407465

22.7 功能 - 无异常退出

image-20231211100407993

22.8 功能 - 私聊

image-20231211103253939

image-20231211105712625

22.9 功能 - 群发

image-20231212100345453

22.10 功能 - 发文件

image-20231213095343660

image-20231213095700939

22.11 功能 - 服务器推送新闻

image-20231214094835659

image-20231214094854374

22.12 功能扩展 - 离线发送

image-20231214100731327

image-20231214101119272

22.13 项目框架分析

22.13.1 文件目录

image-20231217113613542

22.13.2 客户端框架

  • 视图类:QQView

  • 服务类:

    • ClientService:完成用户登录验证、用户注册、退出客户端等功能
    • MessageClientService:提供和消息相关的服务功能
    • FileClientService:提供文件传输服务
  • 线程类:ClientConnectServerThread,用于保持和服务器端的socket连接

  • 管理类:ManageClientConnectServerThread,管理客户端连接到服务器端线程

  • 视图类根据用户选择调用各个服务类的功能,ClientService建立起和服务器的socket连接,并创建线程类ClientConnectServerThread,保持和服务器端的socket连接。该线程类通过ManageClientConnectServerThread管理类进行管理,以便于其他服务能获得该线程及socket连接。MessageClientService和FileClientService都是通过管理类获得维持连接的线程类socket后,完成相应功能。

22.13.3 服务端框架

  • 服务类:
    • ServerService:建立来自各个客户端的socket连接,验证登录是否合法
  • 线程类:
    • ServerConnectClientThread:用于保持和用户端的socket连接,实现各种业务逻辑,如获取在线用户列表、退出系统、转发、群发、发文件等。
    • SendNewsToAllService:服务器推送消息的线程
  • 管理类:ManageServerConnectClientThread,管理ServerConnectClientThread线程类
  • 服务类监听端口并建立各个客户端的socket连接,验证登录是否合法后开启ServerConnectClientThread线程类维持socket连接,线程类通过接收到的客户端请求,做出相应业务处理。管理类ManageServerConnectClientThread便于获取线程socket连接以及断开连接等操作。

第二十三章 反射

23.1 一个需求引出反射

image-20240119100103895

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//根据配置文件 re.properties 指定信息, 创建 Cat 对象并调用方法 hi

//传统的方式 new 对象 -》 调用方法
// Cat cat = new Cat();
// cat.hi(); ===> cat.cry() 修改源码.

//1. 使用 Properties 类, 可以读写配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("src\\re.properties"));
String classfullpath = properties.get("classfullpath").toString(); //"com.hspedu.Cat"
String methodName = properties.get("method").toString(); //"hi"
System.out.println("classfullpath=" + classfullpath);
System.out.println("method=" + methodName);

//2. 创建对象 , 传统的方法,行不通 =》 反射机制
//new classfullpath(); classfullpath是字符串

//3. 使用反射机制解决
// 修改配置文件可加载不同的类和不同的方法
// (1) 加载类, 返回 Class 类型的对象 cls
Class cls = Class.forName(classfullpath);
// (2) 通过 cls 得到你加载的类 com.hspedu.Cat 的对象实例
Object o = cls.newInstance();
System.out.println("o 的运行类型=" + o.getClass()); //运行类型
// (3) 通过 cls 得到你加载的类 com.hspedu.Cat 的 methodName 的方法对象
// 即:在反射中,可以把方法视为对象(万物皆对象)
Method method1 = cls.getMethod(methodName);
//(4) 通过 method1 调用方法: 即通过方法对象来实现调用方法
method1.invoke(o); //传统方法 对象.方法() , 反射机制 方法对象.invoke(对象实例)

23.2 反射机制

23.2.1 Java Reflection

  • 反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到
  • 加载完类之后,在堆中就产生了一个Class类型的对象 (一个类只对应一个Class对象) ,这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个Class对象就像一面镜子,透过这个镜子看到类的结构,以,形象的称之为: 反射

23.2.2 原理示意图

image-20240119103941797

23.2.3 反射的作用

  1. 在运行时判断任意一个对象所属的类
  2. 在运行时构造任意一个类的对象
  3. 在运行时得到任意一个类所具有的成员变量和方法
  4. 在运行时调用任意一个对象的成员变量和方法
  5. 生成动态代理(动态加载类,降低依赖性)

23.2.4 反射相关的主要类

  1. java.lang.Class:代表一个类, Class对象表示某个类加载后在堆中的对象
  2. 构造器java.lang.reflect.Method: 代表类的方法,Method对象表示某个类的方法
  3. java.lang.reflect.Field: 代表类的成员变量,Field对象表示某个类的成员变量
  4. java.lang.reflect.Constructor: 代表类的构造方法, Constructor对象表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//1. 使用 Properties 类, 可以读写配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("src\\re.properties"));
String classfullpath = properties.get("classfullpath").toString();//"com.durango.Cat"
String methodName = properties.get("method").toString();//"hi

//2. 使用反射机制解决
//(1) 加载类, 返回 Class 类型的对象 cls
Class cls = Class.forName(classfullpath);
//(2) 通过 cls 得到你加载的类 com.hspedu.Cat 的对象实例
Object o = cls.newInstance();
System.out.println("o 的运行类型=" + o.getClass()); //运行类型
//(3) 通过 cls 得到你加载的类 com.hspedu.Cat 的 methodName"hi" 的方法对象
// 即:在反射中,可以把方法视为对象(万物皆对象)
Method method1 = cls.getMethod(methodName);
//(4) 通过 method1 调用方法: 即通过方法对象来实现调用方法
System.out.println("=============================");
method1.invoke(o); //传统方法 对象.方法() , 反射: 成员方法.invoke(对象)

//(5)java.lang.reflect.Field: 代表类的成员变量, Field 对象表示某个类的成员变量
//得到 name 字段
//getField 不能得到私有的属性
Field nameField = cls.getField("name"); //
System.out.println(nameField.get(o)); // 传统写法 对象.成员变量 , 反射 : 成员变量.get(对象)

//(6)java.lang.reflect.Constructor: 代表类的构造方法, Constructor 对象表示构造器
Constructor constructor = cls.getConstructor(); //返回无参构造器
System.out.println(constructor);//Cat()
Constructor constructor2 = cls.getConstructor(String.class);
System.out.println(constructor2);//Cat(String name)

23.2.5 反射优点和缺点

  • 优点: 可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑。
  • 缺点: 使用反射基本是解释执行,对执行速度有影响

23.2.6 反射调用优化

image-20240119141022509

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Reflection02 {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException {
m1();//传统 耗时=0
m2();//反射 耗时=1075
m3();//反射优化 耗时=766
}

//传统方法来调用 hi
public static void m1() {
Cat cat = new Cat();
long start = System.currentTimeMillis();
for (int i = 0; i < 90; i++) {
cat.hi();
}
long end = System.currentTimeMillis();
System.out.println("m1() 耗时=" + (end - start));
}

//反射机制调用方法 hi
public static void m2() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class cls = Class.forName("com.durango.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
long start = System.currentTimeMillis();
for (int i = 0; i < 900000000; i++) {
hi.invoke(o);//反射调用方法
}
long end = System.currentTimeMillis();
System.out.println("m2() 耗时=" + (end - start));
}

//反射调用优化 + 关闭访问检查
public static void m3() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class cls = Class.forName("com.durango.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
hi.setAccessible(true);//在反射调用方法时,取消访问检查
long start = System.currentTimeMillis();
for (int i = 0; i < 900000000; i++) {
hi.invoke(o);//反射调用方法
}
long end = System.currentTimeMillis();
System.out.println("m3() 耗时=" + (end - start));
}
}

23.3 Class类

23.3.1 基本介绍

image-20240120101117147

  • Class也是类,因此也继承Obiect类[类图]
  • Class类对象不是new出来的,而是系统创建的。ClassLoader类的loadClass方法
  • 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
  • 每个类的实例都会记得自己是由哪个Class实例所生成
  • 通过Class对象可以完整地得到一个类的完整结构,通过一系列API
  • Class对象是存放在
  • 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括方法代码(https://www.zhihu.com/question/38496907)变量名,方法名,访问权限等等)

23.3.2 常用方法

image-20240120103747056

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Class02 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException {
String classAllPath = "com.durango.Car";
//1 . 获取到 Car 类 对应的 Class 对象
//<?> 表示不确定的 Java 类型
Class<?> cls = Class.forName(classAllPath);

//2. 输出 cls
System.out.println(cls); //显示 cls 对象, 是哪个类的 Class 对象 com.durango.Car
System.out.println(cls.getClass());//输出 cls 运行类型 java.lang.Class

//3. 得到包名
System.out.println(cls.getPackage().getName());//包名 com.durango

//4. 得到全类名
System.out.println(cls.getName()); //com.durango.Car

//5. 通过 cls 创建对象实例
Car car = (Car) cls.newInstance();
System.out.println(car);//car.toString()

//6. 通过反射获取属性 brand
Field brand = cls.getField("brand");
System.out.println(brand.get(car));//宝马

//7. 通过反射给属性赋值
brand.set(car, "奔驰");
System.out.println(brand.get(car));//奔驰

//8 我希望大家可以得到所有的属性(字段)
System.out.println("=======所有的字段属性====");
Field[] fields = cls.getFields();
for (Field f : fields) {
System.out.println(f.getName());//名称
}
}
}

23.4 获取Class类对象

image-20240120110300919

image-20240120110310107

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//1. Class.forName
String classAllPath = "com.durango.Car"; //通过读取配置文件获取
Class<?> cls1 = Class.forName(classAllPath);
System.out.println(cls1);

//2. 类名.class , 应用场景: 用于参数传递
Class cls2 = Car.class;
System.out.println(cls2);

//3. 对象.getClass(), 应用场景,有对象实例
Car car = new Car();
Class cls3 = car.getClass();
System.out.println(cls3);

//4. 通过类加载器来获取到类的 Class 对象
// (1)先得到类加载器 car
ClassLoader classLoader = car.getClass().getClassLoader();
// (2)通过类加载器得到 Class 对象
Class cls4 = classLoader.loadClass(classAllPath);
System.out.println(cls4);

//cls1 , cls2 , cls3 , cls4 其实是同一个对象
System.out.println(cls1.hashCode());
System.out.println(cls2.hashCode());
System.out.println(cls3.hashCode());
System.out.println(cls4.hashCode());

//5. 基本数据(int, char,boolean,float,double,byte,long,short) 按如下方式得到 Class 类对象
Class<Integer> integerClass = int.class;
Class<Character> characterClass = char.class;
Class<Boolean> booleanClass = boolean.class;
System.out.println(integerClass);//int

//6. 基本数据类型对应的包装类,可以通过 .TYPE 得到 Class 类对象
Class<Integer> type1 = Integer.TYPE;
Class<Character> type2 = Character.TYPE; //其它包装类 BOOLEAN, DOUBLE, LONG,BYTE 等待
System.out.println(type1);
System.out.println(integerClass.hashCode());//1956725890
System.out.println(type1.hashCode());//1956725890

23.5 哪些类型有 Class 对象

image-20240120110435904

1
2
3
4
5
6
7
8
9
Class<String> cls1 = String.class;//外部类
Class<Serializable> cls2 = Serializable.class;//接口
Class<Integer[]> cls3 = Integer[].class;//数组
Class<float[][]> cls4 = float[][].class;//二维数组
Class<Deprecated> cls5 = Deprecated.class;//注解
Class<Thread.State> cls6 = Thread.State.class;//枚举
Class<Long> cls7 = long.class;//基本数据类型
Class<Void> cls8 = void.class;//void 数据类型
Class<Class> cls9 = Class.class;// Class

23.6 类加载

23.6.1 基本说明

image-20240121125253492

image-20240121125833970

23.6.2 类加载时机

image-20240121125423921

23.6.3 类加载过程图

image-20240121130105461

23.6.4 类加载各阶段完成任务

image-20240121130715158

23.6.5 加载阶段

image-20240121131146246

23.6.6 连接阶段 - 验证

image-20240121131401578

23.6.7 连接阶段 - 准备

image-20240121131457998

23.6.8 连接阶段 - 解析

image-20240121131741830

23.6.9 初始化 Initialization

image-20240121132156139

23.7 通过反射获取类的结构信息

23.7.1 第一组: java.lang.Class 类

image-20240121144201063

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 第一组
@Test
public void api_01() throws ClassNotFoundException {
// 得到 Class 对象
Class<?> personCls = Class.forName("com.durango.reflection.Person");

// getName:获取全类名
System.out.println(personCls.getName());//com.durango.reflection.Person
// getSimpleName:获取简单类名
System.out.println(personCls.getSimpleName());//Person
//getPackage:以 Package 形式返回 包信息
System.out.println(personCls.getPackage());//com.durango.reflection

//getFields:获取所有 public 修饰的属性,包含本类以及父类的
Field[] fields = personCls.getFields();
for (Field field : fields) {//增强 for
System.out.println("本类以及父类的属性=" + field.getName());
}
//getDeclaredFields:获取本类中所有属性
Field[] declaredFields = personCls.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println("本类中所有属性=" + declaredField.getName());
}

//getMethods:获取所有 public 修饰的方法,包含本类以及父类的
Method[] methods = personCls.getMethods();
for (Method method : methods) {
System.out.println("本类以及父类的方法=" + method.getName());
}
//getDeclaredMethods:获取本类中所有方法
Method[] declaredMethods = personCls.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println("本类中所有方法=" + declaredMethod.getName());
}

//getConstructors: 获取所有 public 修饰的构造器,包含本类
Constructor<?>[] constructors = personCls.getConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println("本类的构造器=" + constructor.getName());
}
//getDeclaredConstructors:获取本类中所有构造器
Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
System.out.println("本类中所有构造器=" + declaredConstructor.getName());//这里老师只是输出名
}

//getSuperClass:以 Class 形式返回父类信息
Class<?> superclass = personCls.getSuperclass();
System.out.println("父类的 class 对象=" + superclass);

//getInterfaces:以 Class[]形式返回接口信息
Class<?>[] interfaces = personCls.getInterfaces();
for (Class<?> anInterface : interfaces) {
System.out.println("接口信息=" + anInterface);
}

//getAnnotations:以 Annotation[] 形式返回注解信息
Annotation[] annotations = personCls.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println("注解信息=" + annotation);//注解
}
}

23.7.2 第二组: java.lang.reflect.Field 类

image-20240121144335991

1
2
3
4
5
6
7
8
9
10
11
//得到 Class 对象
Class<?> personCls = Class.forName("com.durango.reflection.Person");

//getDeclaredFields:获取本类中所有属性
//规定 说明: 默认修饰符 是 0 , public 是 1 ,private 是 2 ,protected 是 4 , static 是 8 ,final 是 16
Field[] declaredFields = personCls.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println("本类中所有属性=" + declaredField.getName()
+ " 该属性的修饰符值=" + declaredField.getModifiers()
+ " 该属性的类型=" + declaredField.getType());
}

23.7.3 第三组: java.lang.reflect.Method 类

image-20240121144357121

1
2
3
4
5
6
7
8
9
10
11
12
//getDeclaredMethods:获取本类中所有方法
Method[] declaredMethods = personCls.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println("本类中所有方法=" + declaredMethod.getName()
+ " 该方法的访问修饰符值=" + declaredMethod.getModifiers()
+ " 该方法返回类型" + declaredMethod.getReturnType());
//输出当前这个方法的形参数组情况
Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
for (Class<?> parameterType : parameterTypes) {
System.out.println("该方法的形参类型=" + parameterType);
}
}

23.7.4 第四组: java.lang.reflect.Constructor 类

image-20240121144435308

1
2
3
4
5
6
7
8
9
10
//getDeclaredConstructors:获取本类中所有构造器
Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
System.out.println("====================");
System.out.println("本类中所有构造器=" + declaredConstructor.getName());
Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
for (Class<?> parameterType : parameterTypes) {
System.out.println("该构造器的形参类型=" + parameterType);
}
}

23.8 通过反射创建对象

image-20240122115415198

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
//1. 先获取到 User 类的 Class 对象
Class<?> userClass = Class.forName("com.durango.User");

//2. 通过 public 的无参构造器创建实例
Object o = userClass.newInstance();
System.out.println(o);

//3. 通过 public 的有参构造器创建实例
// 3.1 先得到对应构造器
Constructor<?> constructor = userClass.getConstructor(String.class);
// 3.2 创建实例,并传入实参
Object durango = constructor.newInstance("durango");
System.out.println("durango = " + durango);

//4. 通过非 public 的有参构造器创建实例
// 4.1 得到 private 的构造器对象
Constructor<?> constructor1 = userClass.getDeclaredConstructor(String.class, int.class);
// 4.2 创建实例
constructor1.setAccessible(true);// 暴破, 使用反射可以访问 private 构造器/方法/属性
Object user2 = constructor1.newInstance("张三丰", 100);
System.out.println("user2=" + user2);

}

23.9 通过反射访问类中的成员

23.9.1 访问属性

image-20240122121055144

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//1. 得到 Student 类对应的 Class 对象
Class<?> stuClass = Class.forName("com.durango.reflection.Student");

//2. 创建对象
Object o = stuClass.newInstance();//o 的运行类型就是 Student
System.out.println(o.getClass());//Student

//3. 使用反射得到 age 属性对象
Field age = stuClass.getField("age");
age.set(o, 88);//通过反射来操作属性
System.out.println(o);
System.out.println(age.get(o));//返回 age 属性的值

//4. 使用反射操作 name 属性
Field name = stuClass.getDeclaredField("name");
//对 name 进行暴破, 可以操作 private 属性
name.setAccessible(true);
//name.set(o, "durango");
name.set(null, "durango~");//因为 name 是 static 属性,因此 o 也可以写出 null
System.out.println(o);
System.out.println(name.get(o)); //获取属性值
System.out.println(name.get(null));//获取属性值, 要求 name 是 static
}
}

23.9.2 访问方法

image-20240122121858946

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
//1. 得到 Boss 类对应的 Class 对象
Class<?> bossCls = Class.forName("com.durango.reflection.Boss");
//2. 创建对象
Object o = bossCls.newInstance();

//3. 调用 public 的 hi 方法
//Method hi = bossCls.getMethod("hi", String.class);//OK
//3.1 得到 hi 方法对象
Method hi = bossCls.getDeclaredMethod("hi", String.class);//OK
//3.2 调用
hi.invoke(o, "韩顺平教育~");

//4. 调用 private static 方法
//4.1 得到 say 方法对象
Method say = bossCls.getDeclaredMethod("say", int.class, String.class, char.class);
//4.2 因为 say 方法是 private, 所以需要暴破,原理和前面讲的构造器和属性一样
say.setAccessible(true);
System.out.println(say.invoke(o, 100, "张三", '男'));
//4.3 因为 say 方法是 static 的,还可以这样调用 ,可以传入 null
System.out.println(say.invoke(null, 200, "李四", '女'));

//5. 在反射中,如果方法有返回值,统一返回 Object , 但是他运行类型和方法定义的返回类型一致
Object reVal = say.invoke(null, 300, "王五", '男');
System.out.println("reVal 的运行类型=" + reVal.getClass());//String
//在演示一个返回的案例
Method m1 = bossCls.getDeclaredMethod("m1");
Object reVal2 = m1.invoke(o);
System.out.println("reVal2 的运行类型=" + reVal2.getClass());//Monster
}

第二十四章 MySQL

(略)MySQL基础篇

第二十五章 JDBC和连接池

25.1 JDBC概述

25.1.1 基本介绍

image-20240124104024941

image-20240124104154409

25.1.2 JDBC 带来的好处

image-20240124105311887

25.1.3 JDBC API

image-20240124105548191

25.2 JDBC快速入门

25.2.1 JDBC程序编写步骤

  1. 注册驱动 - 加载Driver类
  2. 获取连接 - 得到Connection
  3. 执行增删改查 - 通过Statement
  4. 释放资源 - 关闭Statement和Connection

25.2.2 JDBC第一个程序image-20240124112817723

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.durango.jdbc;

import com.mysql.jdbc.Connection;
import com.mysql.jdbc.Driver;
import com.mysql.jdbc.Statement;
import java.sql.SQLException;
import java.util.Properties;

public class JDBC01 {
public static void main(String[] args) throws SQLException {
//前置工作: 在项目下创建一个文件夹比如 libs
// 将 mysql.jar 拷贝到该目录下,点击 add to project ..加入到项目中
// 1.注册驱动
Driver driver = new Driver(); //创建 driver 对象

// 2.得到连接
// (1) jdbc:mysql:// 规定好表示协议,通过 jdbc 的方式连接 mysql
// (2) localhost 主机,可以是 ip 地址
// (3) 3306 表示 mysql 监听的端口
// (4) db01 连接到 mysql dbms 的哪个数据库
// (5) ?characterEncoding=utf8 解决中文显示乱码问题
String url = "jdbc:mysql://localhost:3306/db01?characterEncoding=utf8";
// 将用户名和密码放入到 Properties 对象
Properties properties = new Properties();
properties.setProperty("user", "root");
properties.setProperty("password", "123456");
Connection connection = (Connection) driver.connect(url, properties);

// 3.执行sql语句
//String sql = "insert into actor values(null, '刘德华', '男', '1970-11-11', '110')";
//String sql = "update actor set name='周星驰' where id = 1";
String sql = "delete from actor where id = 1";
//statement 用于执行静态 SQL 语句并返回其生成的结果的对象
Statement statement = (Statement) connection.createStatement();
int rows = statement.executeUpdate(sql); // 如果是 DML 语句,返回的就是影响行数
System.out.println(rows > 0 ? "成功" : "失败");

// 4.关闭连接
statement.close();
connection.close();
}

25.3 获取数据库连接 5 种方式

25.3.1 方式一

image-20240125102720128

1
2
3
4
5
6
7
8
9
10
11
12
13
// 方式一
@Test
public void connect01() throws SQLException {
Driver driver = new Driver(); //创建driver对象 静态加载

String url = "jdbc:mysql://localhost:3306/db01";
//将用户名和密码放入到 Properties 对象
Properties properties = new Properties();
properties.setProperty("user", "root");// 用户
properties.setProperty("password", "123456"); //密码
Connection connect = driver.connect(url, properties);
System.out.println(connect);
}

25.3.2 方式二

image-20240125103108015

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式二
@Test
public void connect02() throws SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
//使用反射加载 Driver 类 , 动态加载,更加的灵活,减少依赖性
Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver) aClass.newInstance();

String url = "jdbc:mysql://localhost:3306/db01";
//将用户名和密码放入到 Properties 对象
Properties properties = new Properties();
properties.setProperty("user", "root");// 用户
properties.setProperty("password", "123456"); //密码

Connection connect = driver.connect(url, properties);
System.out.println("方式 2=" + connect);
}

25.3.3 方式三

image-20240125103158622

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//方式 3 使用 DriverManager 替代 driver 进行统一管理
@Test
public void connect03() throws IllegalAccessException, InstantiationException, ClassNotFoundException, SQLException {
//使用反射加载 Driver
Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver) aClass.newInstance();

//创建 url 和 user 和 password
String url = "jdbc:mysql://localhost:3306/db01";
String user = "root";
String password = "123456";

DriverManager.registerDriver(driver);//注册 Driver 驱动
Connection connection = DriverManager.getConnection(url, user, password);
System.out.println("第三种方式=" + connection);
}

25.3.4 方式四

image-20240125103359657

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//方式 4: 使用 Class.forName 自动完成注册驱动,简化代码
//这种方式获取连接是使用的最多,推荐使用
@Test
public void connect04() throws ClassNotFoundException, SQLException {
//使用反射加载了 Driver 类
//在加载 Driver 类时,完成了注册
Class.forName("com.mysql.jdbc.Driver");

//创建 url 和 user 和 password
String url = "jdbc:mysql://localhost:3306/db01";
String user = "root";
String password = "123456";

Connection connection = DriverManager.getConnection(url, user, password);
System.out.println("第 4 种方式~ " + connection);
}

25.3.5 方式 5

image-20240125103505378

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//方式 5 , 在方式 4 的基础上改进,增加配置文件,让连接 mysql 更加灵活
@Test
public void connect05() throws IOException, ClassNotFoundException, SQLException {
//通过 Properties 对象获取配置文件的信息
Properties properties = new Properties();
properties.load(new FileInputStream("src\\mysql.properties"));

//获取相关的值
String driver = properties.getProperty("driver");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String url = properties.getProperty("url");

Class.forName(driver);//建议写上
Connection connection = DriverManager.getConnection(url, user, password);
System.out.println("方式 5 " + connection);
}

25.4 ResultSet(结果集)

image-20240125103940095

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ResultSet_ {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
//通过 Properties 对象获取配置文件的信息
Properties properties = new Properties();
properties.load(new FileInputStream("src\\mysql.properties"));
//获取相关的值
String driver = properties.getProperty("driver");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String url = properties.getProperty("url");

//1. 注册驱动
Class.forName(driver);//建议写上

//2. 得到连接
Connection connection = DriverManager.getConnection(url, user, password);

//3. 得到 Statement
Statement statement = connection.createStatement();

//4. 执行SQL语句
String sql = "select id, name , sex, borndate from actor";
//执行给定的 SQL 语句,该语句返回单个 ResultSet 对象
ResultSet resultSet = statement.executeQuery(sql);

//5. 使用 while 取出数据
while (resultSet.next()) { // 让光标向后移动,如果没有更多行,则返回 false
int id = resultSet.getInt(1); //获取该行的第 1 列
//int id1 = resultSet.getInt("id"); 通过列名来获取值, 推荐
String name = resultSet.getString(2);//获取该行的第 2 列
String sex = resultSet.getString(3);
Date date = resultSet.getDate(4);
System.out.println(id + "\t" + name + "\t" + sex + "\t" + date);
}
}
}

25.5 Statement

25.5.1 基本介绍

  1. Statement对象 用于执行静态SQL语句并返回其生成的结果的对象
  2. 在连接建立后,需要对数据库进行访问,执行命名或是SQL 语句,可以通过
    • Statement 【存在SQL注入】
    • PreparedStatement【预处理】
    • CallableStatement 【存储过程】
  3. 语句对象执行sql语句,存在sql注入风险
  4. SQL 注入是 利用某些系统没有对用户输入的数据进行充分的检查,而在用户输数据中注入非法的 SQL 语句段或命令,恶意攻击数据库。
  5. 要防范 SQL 注入,只要用 PreparedStatement (从Statement扩展而来) 取代 Statement 就可以了

25.5.2 SQL注入案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 演示 sql 注入
-- 创建一张表
CREATE TABLE admin ( -- 管理员表
NAME VARCHAR(32) NOT NULL UNIQUE, pwd VARCHAR(32) NOT NULL DEFAULT '') CHARACTER SET utf8;

-- 添加数据
INSERT INTO admin VALUES('tom', '123');

-- 查找某个管理是否存在
SELECT *
FROM admin
WHERE NAME = 'tom' AND pwd = '123'

-- SQL注入
-- 输入用户名 为 1' or
-- 输入万能密码 为 or '1'= '1
SELECT *
FROM admin
WHERE NAME = '1' OR' AND pwd = 'OR '1'= '1';

25.5.3 应用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Statement_ {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
//让用户输入管理员名(1' or)和密码 (or '1' = '1)
System.out.print("请输入管理员的名字: "); //next(): 当接收到 空格或者 '就是表示结束
String admin_name = scanner.nextLine(); //如果希望看到 SQL 注入,这里需要用 nextLine
System.out.print("请输入管理员的密码: ");
String admin_pwd = scanner.nextLine();

//通过 Properties 对象获取配置文件的信息
Properties properties = new Properties();
properties.load(new FileInputStream("src\\mysql.properties"));
//获取相关的值
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");

//1. 注册驱动
Class.forName(driver);//建议写上
//2. 得到连接
Connection connection = DriverManager.getConnection(url, user, password);
//3. 得到 Statement
Statement statement = connection.createStatement();
//4. 组织 SqL
String sql = "select name , pwd from admin where name ='" + admin_name + "' and pwd = '" + admin_pwd + "'";
ResultSet resultSet = statement.executeQuery(sql);
if (resultSet.next()) { //如果查询到一条记录,则说明该管理存在
System.out.println("恭喜, 登录成功");
} else {
System.out.println("对不起,登录失败");
}

//关闭连接
resultSet.close();
statement.close();
connection.close();
}
}

25.6 PreparedStatement

25.6.1 基本介绍

  1. PreparedStatement 执行的 SQL 语句中的参数用问号(?)来表示,调用
    PreparedStatement 对象的 setXxx()方法来设置这些参数setXxx() 方法有
    两个参数,第一个参数是要设置的 SQL 语句中的参数的索引(从 1 开始),第二个是设置的 SQL 语句中的参数的值
  2. 调用 executeQuery(),返回 ResultSet 对象
  3. 调用 executeUpdate0(), 执行更新,包括增、删、修改

25.6.2 预处理好处

  1. 不再使用+ 拼接sql语句,减少语法错误
  2. 有效的解决了**sql注入问题**!
  3. 大大减少了编译次数,效率较高

25.6.3 应用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class PreparedStatement_ {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
//让用户输入管理员名和密码
System.out.print("请输入管理员的名字: "); //next(): 当接收到 空格或者 '就是表示结束
String admin_name = scanner.nextLine(); //如果希望看到 SQL 注入,这里需要用 nextLine
System.out.print("请输入管理员的密码: ");
String admin_pwd = scanner.nextLine();

//通过 Properties 对象获取配置文件的信息
Properties properties = new Properties();
properties.load(new FileInputStream("src\\mysql.properties"));
//获取相关的值
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");

//1. 注册驱动
Class.forName(driver);//建议写上
//2. 得到连接
Connection connection = DriverManager.getConnection(url, user, password);

//3. 得到 PreparedStatement
// 3.1 组织 SqL , Sql 语句的 ? 就相当于占位符
String sql = "select name , pwd from admin where name =? and pwd = ?";
// 3.2 preparedStatement 对象实现了 PreparedStatement 接口的实现类的对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.3 给 ? 赋值
preparedStatement.setString(1, admin_name);
preparedStatement.setString(2, admin_pwd);

//4. 执行 select 语句使用 executeQuery
// 如果执行的是 dml(update, insert ,delete)语句 使用executeUpdate()
// 这里执行 executeQuery ,不要在写 sql
ResultSet resultSet = preparedStatement.executeQuery();

if (resultSet.next()) { //如果查询到一条记录,则说明该管理存在
System.out.println("恭喜, 登录成功");
} else {
System.out.println("对不起,登录失败");
}
//关闭连接
resultSet.close();
preparedStatement.close();
connection.close();
}
}

25.7 JDBC 的相关 API 小结

image-20240126145750445

25.8 封装 JDBCUtils工具类

  • 在jdbc操作中,获取连接和释放资源是经常使用到可以将其封装jdbc连接的工具类JDBCUtils

25.8.1 JDBCUtils工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class JDBCUtils {
//定义相关的属性(4 个), 因为只需要一份,因此,我们做出 static
private static String user; //用户名
private static String password; //密码
private static String url; //url
private static String driver; //驱动名

//在 static 代码块去初始化
static {
try {
Properties properties = new Properties();
properties.load(new FileInputStream("src\\mysql.properties"));
//读取相关的属性值
user = properties.getProperty("user");
password = properties.getProperty("password");
url = properties.getProperty("url");
driver = properties.getProperty("driver");
} catch (IOException e) {
//在实际开发中,我们可以这样处理
//1. 将 编译异常 转成 运行异常
//2. 调用者,可以选择捕获该异常,也可以选择默认处理该异常,比较方便. throw new RuntimeException(e);
throw new RuntimeException(e);
}
}

//连接数据库, 返回 Connection
public static Connection getConnection() {
try {
return DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
//1. 将编译异常转成 运行异常
//2. 调用者,可以选择捕获该异常,也可以选择默认处理该异常,比较方便.
throw new RuntimeException(e);
}
}

//关闭相关资源
/*
1. ResultSet 结果集
2. Statement 或者 PreparedStatement
3. Connection
4. 如果需要关闭资源,就传入对象,否则传入 null
*/
public static void close(ResultSet set, Statement statement, Connection connection) {
//判断是否为 null
try {
if (set != null) {
set.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
//将编译异常转成运行异常抛出
throw new RuntimeException(e);
}
}

}

25.8.1 JDBCUtils工具类使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class JDBCUtils_Use {
@Test
public void testSelect() {
Connection connection = null;
String sql = "select * from actor where id = ?";
PreparedStatement preparedStatement = null;
ResultSet set = null;

try {
// 1.得到连接
connection = JDBCUtils.getConnection();
System.out.println(connection.getClass());
// 2.创建PreparedStatement对象
preparedStatement = connection.prepareStatement(sql);
// 3. 给?赋值
preparedStatement.setInt(1, 5);
// 4. 执行sql
set = preparedStatement.executeQuery();
// 5. 遍历结果集
while (set.next()) {
int id = set.getInt("id");
String name = set.getString("name");
String sex = set.getString("sex");
Date borndate = set.getDate("borndate");
String phone = set.getString("phone");
System.out.println(id + "\t" + name + "\t" + sex + "\t" + borndate + "\t" + phone);
}

} catch (SQLException e) {
e.printStackTrace();
} finally {
// 6. 关闭资源
JDBCUtils.close(set, preparedStatement, connection);
}
}

@Test
public void testDML() {//insert , update, delete
Connection connection = null;
String sql = "update actor set name = ? where id = ?";
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection();
preparedStatement = connection.prepareStatement(sql);
//给占位符赋值
preparedStatement.setString(1, "周星驰");
preparedStatement.setInt(2, 4);
//执行
preparedStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
//关闭资源
JDBCUtils.close(null, preparedStatement, connection);
}
}

}

25.9 事务

25.9.1 基本介绍

  1. JDBC程序中当一个Connection对象创建时,默认情况下是自动提交事务:每次执行一个 SQL 语句时,如果执行成功,就会向数据库自动提交,而不能回滚
  2. JDBC程序中为了让多个 SQL 语句作为一个整体执行,需要使用事务
  3. 调用 Connection 的 **setAutoCommit(false)**可以取消自动提交事务(开启事务)
  4. 在所有的 SQL 语句都成功执行后,调用 Connection 的 commit() 方法提交事务
  5. 在其中某个操作失败或出现异常时,调用 Connection 的 rollback() 方法回滚事务

25.9.2 应用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Transaction_ {
// 模拟经典的转账业务
@Test
public void noTransaction() {
//操作转账的业务
Connection connection = null;
String sql = "update account set balance = balance - 100 where name = ?";
String sql2 = "update account set balance = balance + 100 where name = ?";
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection(); // 在默认情况下,connection 是默认自动提交
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "马化腾");
preparedStatement.executeUpdate(); // 执行第 1 条 sql
int i = 1 / 0; //抛出异常(下面两行代码不会执行)
preparedStatement = connection.prepareStatement(sql2);
preparedStatement.setString(1, "马云");
preparedStatement.executeUpdate(); // 执行第 2 条 sql
} catch (SQLException e) {
e.printStackTrace();
} finally {
//关闭资源
JDBCUtils.close(null, preparedStatement, connection);
}
}

//事务来解决
@Test
public void useTransaction() {
//操作转账的业务
Connection connection = null;
String sql = "update account set balance = balance - 100 where id = 1";
String sql2 = "update account set balance = balance + 100 where id = 2";
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection(); // 在默认情况下,connection 是默认自动提交
//将 connection 设置为不自动提交
connection.setAutoCommit(false); //开启了事务

preparedStatement = connection.prepareStatement(sql);
preparedStatement.executeUpdate(); // 执行第 1 条 sql
int i = 1 / 0; //抛出异常
preparedStatement = connection.prepareStatement(sql2);
preparedStatement.executeUpdate(); // 执行第 2 条 sql

//这里提交事务
connection.commit();
} catch (Exception e) {
//这里我们可以进行回滚,即撤销执行的 SQL
//默认回滚到事务开始的状态.
System.out.println("执行发生了异常,撤销已经执行的 sql");
try {
connection.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
e.printStackTrace();
} finally {
//关闭资源
JDBCUtils.close(null, preparedStatement, connection);
}
}
}

25.10 批处理

25.10.1 基本介绍

  1. 当需要成批插入或者更新记录时。可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理效率更高
  2. JDBC的批量处理语句包括下面方法:
    • **addBatch()**:添加需要批量处理的SQL语句或参数
    • **executeBatch()**:执行批量处理语句:
    • **clearBatch()**:清空批处理包的语句
  3. JDBC连接MySQL时,如果要使用批处理功能,请再url中加参数?rewriteBatchedStatements=true
  4. 批处理往往和PreparedStatement一起搭配使用,可以既减少编译次数,又减少运行次数,效率大大提高

25.10.2 应用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Batch {
//传统方法,添加 5000 条数据到 admin
@Test
public void noBatch() throws Exception {
Connection connection = JDBCUtils.getConnection();
String sql = "insert into admin2 values(null, ?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
System.out.println("开始执行");
long start = System.currentTimeMillis();//开始时间
for (int i = 0; i < 5000; i++) {//5000 执行
preparedStatement.setString(1, "jack" + i);
preparedStatement.setString(2, "666");
preparedStatement.executeUpdate();
}
long end = System.currentTimeMillis();
System.out.println("传统的方式 耗时=" + (end - start));//传统的方式 耗时=2805
//关闭连接
JDBCUtils.close(null, preparedStatement, connection);
}

//使用批量方式添加数据
//批量处理会减少我们发送 sql 语句的网络开销,而且减少编译次数,因此效率提高
@Test
public void batch() throws Exception {
Connection connection = JDBCUtils.getConnection();
String sql = "insert into admin2 values(null, ?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
System.out.println("开始执行");
long start = System.currentTimeMillis();//开始时间
for (int i = 0; i < 5000; i++) {//5000 执行
preparedStatement.setString(1, "jack" + i);
preparedStatement.setString(2, "666");

preparedStatement.addBatch(); //添加
//当有 1000 条记录时,在批量执行
if ((i + 1) % 1000 == 0) {//满 1000 条 sql
preparedStatement.executeBatch(); //执行批处理
preparedStatement.clearBatch(); //清空
}
}
long end = System.currentTimeMillis();
System.out.println("批量方式 耗时=" + (end - start));//批量方式 耗时=65
//关闭连接
JDBCUtils.close(null, preparedStatement, connection);
}
}

25.11数据库连接池

25.11.1 5k 次连接数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConQuestion {
//代码 连接 mysql 5000 次
@Test
public void testCon() {
//看看连接-关闭 connection 会耗用多久
long start = System.currentTimeMillis();
System.out.println("开始连接.....");
for (int i = 0; i < 5000; i++) {
//使用传统的 jdbc 方式,得到连接
Connection connection = JDBCUtils.getConnection();
//做一些工作,比如得到 PreparedStatement ,发送 sql
//.......... //关闭
JDBCUtils.close(null, null, connection);
}
long end = System.currentTimeMillis();
System.out.println("传统方式 5000 次 耗时=" + (end - start));//传统方式 5000 次 耗时=6684
}
}

25.11.2 传统获取 Connection 问题分析

  1. 传统的JDBC数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中再验证IP地址,用户名和密码(0.05s~1s时间)。需要数据库连接的时候,就向数据库要求一个,频

    繁的进行数据库连接操作将占用很多的系统资源容易造成服务器崩溃

  2. 每一次数据库连接,使用完后都得断开,如果程序出现异常而未能关闭,将导致数据库内存泄漏,最终将导致重启数据库

  3. 传统获取连接的方式,不能控制创建的连接数量,如连接过多,也可能导致内存泄漏,MySQL崩溃。

  4. 解决传统开发中的数据库连接问题,可以采用数据库连接池技术(connection pool)

25.11.3 数据库连接池种类

image-20240128214046525

image-20240128214424546

  1. JDBC 的数据库连接池使用 javax.sgl.DataSource 来表示,DataSource只是一个接口,该接口通常由第三方提供实现[提供 .jar]
  2. C3PO 数据库连接池,速度相对较慢,稳定性不错 (hibernate,spring)
  3. DBCP 数据库连接池,速度相对c3p0较快,但不稳定
  4. Proxool 数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
  5. BoneCP 数据库连接池,速度快
  6. Druid(德鲁伊)是阿里提供的数据库连接池,集DBCP 、C3PO 、Proxoo优点于一身的数据库连接池

25.11.4 C3P0 应用实例

  • 方式一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 方式 1: 相关参数,在程序中指定 user, url , password 等
@Test
public void testC3P0_01() throws Exception {
//1. 创建一个数据源对象
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();

//2. 通过配置文件 mysql.properties 获取相关连接的信息
Properties properties = new Properties();
properties.load(new FileInputStream("src\\mysql.properties"));
//读取相关的属性值
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String url = properties.getProperty("url");
String driver = properties.getProperty("driver");

//3. 给数据源 comboPooledDataSource 设置相关的参数
//注意:连接管理是由 comboPooledDataSource 来管理
comboPooledDataSource.setDriverClass(driver);
comboPooledDataSource.setJdbcUrl(url);
comboPooledDataSource.setUser(user);
comboPooledDataSource.setPassword(password);
//设置初始化连接数
comboPooledDataSource.setInitialPoolSize(10);
//最大连接数
comboPooledDataSource.setMaxPoolSize(50);

//4. 测试连接池的效率, 测试对 mysql 5000 次操作
long start = System.currentTimeMillis();
for (int i = 0; i < 5000; i++) {
Connection connection = comboPooledDataSource.getConnection(); //这个方法就是从 DataSource 接口实现的
System.out.println("连接 OK");
connection.close();
}
long end = System.currentTimeMillis();
//c3p0 5000 连接 mysql 耗时=318
System.out.println("c3p0 5000 连接 mysql 耗时=" + (end - start));
}
  • 方式二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//第二种方式 使用配置文件模板来完成
//1. 将 c3p0 提供的 c3p0.config.xml 拷贝到 src 目录下
//2. 该文件指定了连接数据库和连接池的相关参数
@Test
public void testC3P0_02() throws SQLException {
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource("durango");

//测试 5000 次连接 mysql
long start = System.currentTimeMillis();
System.out.println("开始执行....");
for (int i = 0; i < 500000; i++) {
Connection connection = comboPooledDataSource.getConnection();
System.out.println("连接 OK~");
connection.close();
}
long end = System.currentTimeMillis();
//c3p0 的第二种方式 耗时=413
System.out.println("c3p0 的第二种方式(500000) 耗时=" + (end - start));//3371
}
  • 配置文件 c3p0.config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<c3p0-config>
<!-- 数据源名称 代表连接池 -->
<named-config name="durango">
<!-- 驱动类 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<!-- url-->
<property name="jdbcUrl">jdbc:mysql://127.0.0.1:3306/db01</property>
<!-- 用户名 -->
<property name="user">root</property>
<!-- 密码 -->
<property name="password">123456</property>
<!-- 每次增长的连接数-->
<property name="acquireIncrement">5</property>
<!-- 初始的连接数 -->
<property name="initialPoolSize">10</property>
<!-- 最小连接数 -->
<property name="minPoolSize">5</property>
<!-- 最大连接数 -->
<property name="maxPoolSize">10</property>
<!-- 可连接的最多的命令对象数 -->
<property name="maxStatements">5</property>
<!-- 每个连接对象可连接的最多的命令对象数 -->
<property name="maxStatementsPerConnection">2</property>
</named-config>
</c3p0-config>

25.11.5 Druid(德鲁伊) 应用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Durid_ {
@Test
public void testDruid() throws Exception {
//1. 加入 Druid jar 包
//2. 加入 配置文件 druid.properties , 将该文件拷贝项目的 src 目录
//3. 创建 Properties 对象, 读取配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("src\\druid.properties"));
//4. 创建一个指定参数的数据库连接池, Druid 连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
Connection connection = dataSource.getConnection();
System.out.println(connection.getClass());
//System.out.println("连接成功!");
connection.close();
}
long end = System.currentTimeMillis();
//druid 连接池 操作 5000 耗时=2333
System.out.println("druid 连接池 操作 500000 耗时=" + (end - start));//539
}
}

25.11.6 将 JDBCUtils 工具类改成 Druid(德鲁伊)实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class JDBCUtilsByDruid {
private static DataSource ds;

//在静态代码块完成 ds 初始化
static {
Properties properties = new Properties();
try {
properties.load(new FileInputStream("src\\druid.properties"));
ds = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}

//编写 getConnection 方法
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}

//关闭连接, 在数据库连接池技术中,close 不是真的断掉连接
//而是把使用的 Connection 对象放回连接池
public static void close(ResultSet resultSet, Statement statement, Connection connection) {
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

25.12 Apache—DBUtils

25.12.1 先分析一个问题

image-20240201101600286

25.12.2 用自己的土方法来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//使用老师的土方法来解决 ResultSet =封装=> Arraylist
@Test
public ArrayList<Actor> testSelectToArrayList() {
System.out.println("使用 druid 方式完成");
//1. 得到连接
Connection connection = null;
//2. 组织一个 sql
String sql = "select * from actor where id >= ?";
PreparedStatement preparedStatement = null;
ResultSet set = null;
ArrayList<Actor> list = new ArrayList<>();//创建 ArrayList 对象,存放 actor 对象
//3. 创建 PreparedStatement 对象
try {
connection = JDBCUtilsByDruid.getConnection();
System.out.println(connection.getClass());//运行类com.alibaba.druid.pool.DruidPooledConnection
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 1);//给?号赋值
//执行, 得到结果集
set = preparedStatement.executeQuery();
//遍历该结果集
while (set.next()) {
int id = set.getInt("id");
String name = set.getString("name");//getName()
String sex = set.getString("sex");//getSex()
Date borndate = set.getDate("borndate");
String phone = set.getString("phone");
//把得到的 resultset 的记录,封装到 Actor 对象,放入到 list 集合
list.add(new Actor(id, name, sex, borndate, phone));
}
System.out.println("list 集合数据=" + list);
for(Actor actor : list) {
System.out.println("id=" + actor.getId() + "\t" + actor.getName());
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
//关闭资源
JDBCUtilsByDruid.close(set, preparedStatement, connection);
}
//因为 ArrayList 和 connection 没有任何关联,所以该集合可以复用.
return list;
}

25.12.3 基本介绍

  1. commons-dbutils 是 Apache 组织提供的-一个开源JDBC工具类库,它是对JDBC的封装使用dbutils能极大简化jdbc编码的工作量。

  2. DbUtils

  3. **QueryRunner**类: 该类封装了SQL的执行,是线程安全的。可以实现增、删、改、查、批处理

  4. ResultSetHandler接口: 该接口用于处理 java.sql.ResultSet,将数据按要求转换为另一种形式,有如下几种:

    image-20240201102308514

25.12.4 应用实例

query()方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public <T> T query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException {
PreparedStatement stmt = null; //定义 PreparedStatement
ResultSet rs = null; //接收返回的 ResultSet
T result = null; //返回 ArrayList

try {
stmt = this.prepareStatement(conn, sql); //创建 PreparedStatement
this.fillStatement(stmt, params); ;//对 sql 进行 ? 赋值
rs = this.wrap(stmt.executeQuery()); ;//执行 sql,返回 resultset
result = rsh.handle(rs); //返回的 resultset --> arrayList[result] [使用到反射,对传入 class 对象
} catch (SQLException var33) {
this.rethrow(var33, sql, params);
} finally {
try {
this.close(rs); ;//关闭 resultset
} finally {
this.close((Statement)stmt); //关闭 preparedstatement 对象
}
}

return result;
}
  • apache-DBUtils 工具类 + druid 完成对表的查询操作

    • 返回结果是多行数据的情况
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Test
    public void testQueryMany() throws SQLException {
    // 1.用druid方式得到连接
    Connection connection = JDBCUtilsByDruid.getConnection();
    // 2.使用 DBUtilsl 类和接口, 先引入DBUtils相关的jar
    // 3.创建 QueryRunner
    QueryRunner queryRunner = new QueryRunner();
    // 4. 执行相关的方法,返回ArrayList结果集
    String sql = "select * from actor where id >= ?";
    // (1) query 方法就是执行 sql 语句,得到 resultset ---封装到 --> ArrayList 集合中
    // (2) list: 返回集合
    // (3) connection: 连接
    // (4) sql : 执行的 sql 语句
    // (5) new BeanListHandler<>(Actor.class): 在将 resultset -> Actor 对象 -> 封装到 ArrayList
    // 底层使用反射机制 去获取 Actor 类的属性,然后进行封装
    // (6) 1 就是给 sql 语句中的 ? 赋值,可以有多个值,因为是可变参数 Object... params
    // (7) 底层得到的 resultset,会在query中关闭, 还会关闭 PreparedStatment
    List<Actor> list = queryRunner.query(connection, sql, new BeanListHandler<>(Actor.class), 1);
    System.out.println("输出集合信息");
    for (Actor actor : list) {
    System.out.println(actor);
    }
    //释放资源
    JDBCUtilsByDruid.close(null, null, connection);
    }
    • 返回结果是单行数据的情况
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Test
    public void testQuerySingle() throws SQLException {
    // 1.用druid方式得到连接
    Connection connection = JDBCUtilsByDruid.getConnection();
    // 2.使用 DBUtilsl 类和接口, 先引入DBUtils相关的jar
    // 3.创建 QueryRunner
    QueryRunner queryRunner = new QueryRunner();
    // 4. 执行相关的方法,返回单个对象
    String sql = "select * from actor where id = ?";
    Actor actor = queryRunner.query(connection, sql, new BeanHandler<>(Actor.class), 4);
    System.out.println(actor);
    // 释放资源
    JDBCUtilsByDruid.close(null, null, connection);
    }
    • 查询结果是单行单列-返回的就是 object
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Test
    public void testScalar() throws SQLException {
    // 1.用druid方式得到连接
    Connection connection = JDBCUtilsByDruid.getConnection();
    // 2.使用 DBUtilsl 类和接口, 先引入DBUtils相关的jar
    // 3.创建 QueryRunner
    QueryRunner queryRunner = new QueryRunner();
    // 4. 就可以执行相关的方法,返回单行单列 , 返回的就是 Object
    String sql = "select name from actor where id = ?";
    //因为返回的是一个对象, 使用的 handler 就是 ScalarHandler
    Object obj = queryRunner.query(connection, sql, new ScalarHandler(), 4);
    System.out.println(obj);
    // 释放资源
    JDBCUtilsByDruid.close(null, null, connection);
    }
  • 使用apache-dbutils + druid 完成 dml (update, insert ,delete)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Test
    public void testDML() throws SQLException {
    // 1.用druid方式得到连接
    Connection connection = JDBCUtilsByDruid.getConnection();
    // 2.使用 DBUtilsl 类和接口, 先引入DBUtils相关的jar
    // 3.创建 QueryRunner
    QueryRunner queryRunner = new QueryRunner();
    //4. 这里组织 sql 完成 update, insert delete
    //String sql = "update actor set name = ? where id = ?";
    //String sql = "insert into actor values(null, ?, ?, ?, ?)";
    String sql = "delete from actor where id = ?";
    // (1) 执行 dml 操作是 queryRunner.update()
    // (2) 返回的值是受影响的行数 (affected: 受影响)
    //int affectedRow = queryRunner.update(connection, sql, "林青霞", "女", "1966-10-10", "116");
    int affectedRow = queryRunner.update(connection, sql, 1000 );
    System.out.println(affectedRow > 0 ? "执行成功" : "执行没有影响到表");
    // 释放资源
    JDBCUtilsByDruid.close(null, null, connection);
    }

25.12.5 表和 JavaBean 的类型映射关系

image-20240201111944056

25.13 DAO 和增删改查通用方法-BasicDao

25.13.1 先分析一个问题

image-20240203215133282

image-20240203215636053

25.13.2 基本说明

image-20240203220023888

25.13.3 BasicDAO 应用实例

image-20240203220057648

image-20240203223232503
  • JDBCUtilsByDruid.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class JDBCUtilsByDruid {
    private static DataSource ds;

    //在静态代码块完成 ds 初始化
    static {
    Properties properties = new Properties();
    try {
    properties.load(new FileInputStream("src\\druid.properties"));
    ds = DruidDataSourceFactory.createDataSource(properties);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    //编写 getConnection 方法
    public static Connection getConnection() throws SQLException {
    return ds.getConnection();
    }

    //关闭连接, 在数据库连接池技术中,close 不是真的断掉连接
    //而是把使用的 Connection 对象放回连接池
    public static void close(ResultSet resultSet, Statement statement, Connection connection) {
    try {
    if (resultSet != null) {
    resultSet.close();
    }
    if (statement != null) {
    statement.close();
    }
    if (connection != null) {
    connection.close();
    }
    } catch (SQLException e) {
    throw new RuntimeException(e);
    }
    }
    }
  • Actor.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    public class Actor { //Javabean, POJO, Domain对象

    private Integer id;
    private String name;
    private String sex;
    private Date borndate;
    private String phone;

    public Actor() { //一定要给一个无参构造器[反射需要]
    }

    public Actor(Integer id, String name, String sex, Date borndate, String phone) {
    this.id = id;
    this.name = name;
    this.sex = sex;
    this.borndate = borndate;
    this.phone = phone;
    }

    public Integer getId() {
    return id;
    }

    public void setId(Integer id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public String getSex() {
    return sex;
    }

    public void setSex(String sex) {
    this.sex = sex;
    }

    public Date getBorndate() {
    return borndate;
    }

    public void setBorndate(Date borndate) {
    this.borndate = borndate;
    }

    public String getPhone() {
    return phone;
    }

    public void setPhone(String phone) {
    this.phone = phone;
    }

    @Override
    public String toString() {
    return "\nActor{" +
    "id=" + id +
    ", name='" + name + '\'' +
    ", sex='" + sex + '\'' +
    ", borndate=" + borndate +
    ", phone='" + phone + '\'' +
    '}';
    }
    }
  • BasicDAO.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    public class BasicDAO<T> { //泛型指定具体类型
    private QueryRunner qr = new QueryRunner();

    //开发通用的 dml 方法, 针对任意的表
    public int update(String sql, Object... parameters) {
    Connection connection = null;
    try {
    connection = JDBCUtilsByDruid.getConnection();
    int update = qr.update(connection, sql, parameters);
    return update;
    } catch (SQLException e) {
    throw new RuntimeException(e); //将编译异常->运行异常 ,抛出
    } finally {
    JDBCUtilsByDruid.close(null, null, connection);
    }
    }

    //返回多个对象(即查询的结果是多行), 针对任意表

    /**
    * @param sql sql 语句,可以有 ?
    * @param clazz 传入一个类的 Class 对象 比如 Actor.class
    * @param parameters 传入 ? 的具体的值,可以是多个
    * @return 根据 Actor.class 返回对应的 ArrayList 集合
    */
    public List<T> queryMulti(String sql, Class<T> clazz, Object... parameters) {
    Connection connection = null;
    try {
    connection = JDBCUtilsByDruid.getConnection();
    return qr.query(connection, sql, new BeanListHandler<T>(clazz), parameters);
    } catch (SQLException e) {
    throw new RuntimeException(e); //将编译异常->运行异常 ,抛出
    } finally {
    JDBCUtilsByDruid.close(null, null, connection);
    }
    }

    //查询单行结果 的通用方法
    public T querySingle(String sql, Class<T> clazz, Object... parameters) {
    Connection connection = null;
    try {
    connection = JDBCUtilsByDruid.getConnection();
    return qr.query(connection, sql, new BeanHandler<T>(clazz), parameters);
    } catch (SQLException e) {
    throw new RuntimeException(e); //将编译异常->运行异常 ,抛出
    } finally {
    JDBCUtilsByDruid.close(null, null, connection);
    }
    }

    //查询单行单列的方法,即返回单值的方法
    public Object queryScalar(String sql, Object... parameters) {
    Connection connection = null;
    try {
    connection = JDBCUtilsByDruid.getConnection();
    return qr.query(connection, sql, new ScalarHandler(), parameters);
    } catch (SQLException e) {
    throw new RuntimeException(e); //将编译异常->运行异常 ,抛出
    } finally {
    JDBCUtilsByDruid.close(null, null, connection);
    }
    }

    }
  • ActorDAO.java

    1
    2
    3
    4
    public class ActorDAO extends BasicDAO<Actor>{
    //1. 包含 BasicDAO 的方法
    //2. 根据业务需求,可以编写特有的方法
    }
  • TestDAO.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class TestDAO {
    //测试 ActorDAO 对 actor 表 crud 操作
    @Test
    public void testActorDAO() {
    ActorDAO actorDAO = new ActorDAO();

    //1. 查询多行记录
    List<Actor> actors = actorDAO.queryMulti("select * from actor where id >= ?", Actor.class, 1);
    System.out.println("===查询结果===");
    for (Actor actor : actors) {
    System.out.println(actor);
    }

    //2. 查询单行记录
    Actor actor = actorDAO.querySingle("select * from actor where id = ?", Actor.class, 6);
    System.out.println("====查询单行结果====");
    System.out.println(actor);

    //3. 查询单行单列
    Object o = actorDAO.queryScalar("select name from actor where id = ?", 6);
    System.out.println("====查询单行单列值===");
    System.out.println(o);

    //4. dml 操作 insert ,update, delete
    int update = actorDAO.update("insert into actor values(null, ?, ?, ?, ?)", "张无忌", "男", "2000-11-11", "999");
    System.out.println(update > 0 ? "执行成功" : "执行没有影响表");
    }
    }

第二十六章 满汉楼

(略)

第二十七章 正则表达式

27.1 快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Regexp_ {
public static void main(String[] args) {
String content = "";
//1. 先创建一个 Pattern 对象, 模式对象, 可以理解成就是一个正则表达式对象
Pattern pattern = Pattern.compile("[a-zA-Z]+"); //单词
//Pattern pattern = Pattern.compile("[0-9]+"); //数字
//Pattern pattern = Pattern.compile("([0-9]+)|([a-zA-Z]+)"); //数字和单词
//Pattern pattern = Pattern.compile("<a target=\"_blank\" title=\"(\\S*)\""); //百度热搜的标题

//2. 创建一个匹配器对象
Matcher matcher = pattern.matcher(content);

//3. 循环匹配
while (matcher.find()) {
//匹配内容,放到 m.group(0)
System.out.println("找到:" + matcher.group(0));
}
}
}

27.2 正则表达式基本介绍

  1. 一个正则表达式,就是用某种模式去匹配字符串的一个公式.很多人因
    为它们看上去比较古怪而且复杂所以不敢去使用,不过,经过练习后,
    就觉得这些复杂的表达式写起来还是相当简单的,而且,一旦你弄懂它
    们,你就能把数小时辛苦而且易错的文本处理工作缩短在几分钟(甚至
    几秒钟)内完成

27.3 正则表达式底层实现(重要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class RegTheory {
public static void main(String[] args) {
String content = "1998 年 12 月 8 日,第二代 Java 平台的企业版 J2EE 发布。1999 年 6 月,Sun 公司发布了" +
"第二代 Java 平台(简称为 Java2)的 3 个版本:J2ME(Java2 Micro Edition,Java2 平台的微型" +
"版),应用于移动、无线及有限资源的环境;J2SE(Java 2 Standard Edition,Java 2 平台的" +
"标准版),应用于桌面环境;J2EE(Java 2Enterprise Edition,Java 2 平台的企业版),应" +
"用 3443 于基于 Java 的应用服务器。Java 2 平台的发布,是 Java 发展过程中最重要的一个" +
"里程碑,标志着 Java 的应用开始普及 9889 ";

//目标:匹配所有四个数字
//说明
// 1. \\d 表示一个任意的数字
String regStr = "(\\d\\d)(\\d\\d)";
// 2. 创建模式对象[即正则表达式对象]
Pattern pattern = Pattern.compile(regStr);
//3. 创建匹配器
//说明:创建匹配器 matcher, 按照 正则表达式的规则 去匹配 content 字符串
Matcher matcher = pattern.matcher(content);
//4.开始匹配
/**
* matcher.find() 完成的任务 (考虑分组)
* 什么是分组,比如 (\d\d)(\d\d) ,正则表达式中有() 表示分组, 第 1 个()表示第 1 组,第 2 个()表示第 2 组...
* 1. 根据指定的规则 ,定位满足规则的子字符串(比如(19)(98))
* 2. 找到后,
* 2.1 将 子字符串的开始的索引 记录到 matcher对象的属性 int[] groups; groups[0] = 0 ,
* 把 该子字符串的结束的索引+1 记录到 groups[1] = 4
* 2.2 记录 1 组()匹配到的字符串 groups[2] = 0 groups[3] = 2
* 2.3 记录 2 组()匹配到的字符串 groups[4] = 2 groups[5] = 4
* 2.4.如果有更多的分组.....
* 3. 同时记录 oldLast 的值为 子字符串的结束的索引+1, 即 4, 即下次执行 find 时,就从 4 开始匹配
*
* matcher.group(0) 分析
* 源码
* return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
* 1. 根据 groups[0]=0 和 groups[1]=4 的记录的位置,从 content 开始截取子字符串返回
* 是 [0,4) 包含 0 但是不包含索引为 4 的位置
*
* 如果再次指向 find 方法.仍然安上面分析来执行
**/

while (matcher.find()) {
//小结
//1. 如果正则表达式有() 即分组
//2. 取出匹配的字符串规则如下
//3. group(0) 表示匹配到的子字符串
//4. group(1) 表示匹配到的子字符串的第一组字串
//5. group(2) 表示匹配到的子字符串的第 2 组字串
//6. ... 但是分组的数不能越界.
System.out.println("找到: " + matcher.group(0));
System.out.println("第 1 组()匹配到的值=" + matcher.group(1));
System.out.println("第 2 组()匹配到的值=" + matcher.group(2));
}
}
}

27.4 正则表达式语法

27.4.1 基本介绍

元字符按照功能大致分为:

  • 限定符
  • 选择匹配符
  • 特殊字符
  • 字符匹配符
  • 定位符
  • 分组组合和反向引用符

27.4.2 元字符 - 转义号 \\

image-20240223123953145

image-20240223124028575

27.6.3 元字符 - 字符匹配符

image-20240226102714593

image-20240226102725620

符号 含义 示例 说明 匹配输入
[ ] 可接收的字符列表 [efgh] e、f、g、h中的任意1个字符
[ ^ ] 不可接收的字符列表 [^abc] 除a、b、c、之外的任意1个字符,包含数字和特殊符号
- 连字符 A-Z 任意单个大写字母
. 匹配除了\n以外的任何字符 a..b 以a开头,b结尾,中间包含2个任意字符的长度为4的字符串 aaab, aefb, a35b
\\d 匹配单个数字字符,相当于[0-9] \\d{3}(\\d)? 包含3个或4个数字的字符串 123, 9876
\\D 匹配单个非数字字符,相当于[^ 0-9] \\D(\\d)* 以单个非数字字符开头,后接任意个数字符串 a, A342
\\w 匹配单个数字,大小写字母字符,相当于[0-9a-zA-Z] \\d{3}\\w{4} 以3个数字开头的长度为7的数字字母字符串 234abcd, 12345pe
\\W 匹配单个非数字、大小写字母字符,相当于[^0-9a-zA-Z] \\W+\\d{2} 以至少1个非数字字母字符开头,2个数字字符结尾的字符串 #29, #?@10
\\s 匹配任何空白字符(空格,制表符等)
\\S 匹配任何非空白字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
String content = "a11c8abc _ABCy @";
//String regStr = "[a-z]";//匹配 a-z 之间任意一个字符
//String regStr = "[A-Z]";//匹配 A-Z 之间任意一个字符
//String regStr = "abc";//匹配 abc 字符串[默认区分大小写]
//String regStr = "(?i)abc";//匹配 abc 字符串[不区分大小写]
//String regStr = "[0-9]";//匹配 0-9 之间任意一个字符
//String regStr = "[^a-z]";//匹配 不在 a-z 之间任意一个字符
//String regStr = "[^0-9]";//匹配 不在 0-9 之间任意一个字符
//String regStr = "[abcd]";//匹配 在 abcd 中任意一个字符
//String regStr = "\\D";//匹配 不在 0-9 的任意一个字符
//String regStr = "\\w";//匹配 大小写英文字母, 数字,下划线
//String regStr = "\\W";//匹配 等价于 [^a-zA-Z0-9_]
//\\s 匹配任何空白字符(空格,制表符等)
//String regStr = "\\s";
//\\S 匹配任何非空白字符 ,和\\s 刚好相反
//String regStr = "\\S";
//. 匹配出 \n 之外的所有字符,如果要匹配.本身则需要使用 \\.
//String regStr = ".";
//说明
//1. 当创建 Pattern 对象时,指定 Pattern.CASE_INSENSITIVE, 表示匹配是不区分字母大小写.
Pattern pattern = Pattern.compile(regStr, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到 " + matcher.group(0));
}

27.6.4 元字符 - 选择匹配符

image-20240223133731633

27.6.5 元字符 - 限定符

用于指定其前面的字符和组合项连续出现多少次

image-20240223133756832

27.6.6 元字符 - 定位符

image-20240223134820741

27.6.7 分组

  • 捕获分组

image-20240223150323017

1
2
3
4
5
6
7
8
9
10
11
12
13
 //非命名分组
String regStr = "(\\d\\d)(\\d\\d)";//匹配 4 个数字的字符串
//命名分组: 即可以给分组取名
String regStr1 = "(?<g1>\\d\\d)(?<g2>\\d\\d)";//匹配 4 个数字的字符串
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到=" + matcher.group(0));
System.out.println("第 1 个分组内容=" + matcher.group(1));
System.out.println("第 1 个分组内容[通过组名]=" + matcher.group("g1"));
System.out.println("第 2 个分组内容=" + matcher.group(2));
System.out.println("第 2 个分组内容[通过组名]=" + matcher.group("g2"));
}
  • 非捕获分组

image-20240223151121978

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
String content = "hello 韩顺平教育 jack 韩顺平老师 韩顺平同学 hello 韩顺平学生";
// 找到 韩顺平教育 、韩顺平老师、韩顺平同学 子字符串
//String regStr = "韩顺平教育|韩顺平老师|韩顺平同学";
//上面的写法可以等价非捕获分组, 注意:不能 matcher.group(1)
String regStr = "韩顺平(?:教育|老师|同学)";


//找到 韩顺平 这个关键字,但是要求只是查找韩顺平教育和 韩顺平老师 中包含有的韩顺平
//下面也是非捕获分组,不能使用 matcher.group(1)
String regStr1 = "韩顺平(?=教育|老师)";

//找到 韩顺平 这个关键字,但是要求只是查找 不是 (韩顺平教育 和 韩顺平老师) 中包含有的韩顺平
//下面也是非捕获分组,不能使用 matcher.group(1)
String regStr2 = "韩顺平(?!教育|老师)";

Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到: " + matcher.group(0));
}

27.5 应用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String content = "13588889999";
// 汉字
//String regStr = "^[\u0391-\uffe5]+$";

// 邮政编码
// 要求:1.是 1-9 开头的一个六位数. 比如:123890
//String regStr = "^[1-9]\\d{5}$";

// QQ 号码
// 要求: 是 1-9 开头的一个(5 位数-10 位数) 比如: 12389 , 1345687 , 187698765
//String regStr = "^[1-9]\\d{4,9}$";

// 手机号码
// 要求: 必须以 13,14,15,18 开头的 11 位数 , 比如 13588889999
String regStr = "^1[3|4|5|8]\\d{9}$";

Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if(matcher.find()) {
System.out.println("满足格式");
} else {
System.out.println("不满足格式");
}
1
2
3
4
5
6
7
8
9
//网址
//String content = "https://www.bilibili.com/video/BV1fh411y7R8?from=search&seid=1831060912083761326";
/**
* 思路
* 1. 先确定 url 的开始部分 https:// | http://
* 2.然后通过 ([\w-]+\.)+[\w-]+ 匹配 www.bilibili.com
* 3. (\/[\w-?=&/%.#]*)? 匹配/video/BV1fh411y7R8?from=sear
*/
String regStr1 = "^((http|https)://)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-?=&/%.#]*)?$";//注意:[. ? *]表示匹配就是.本身

27.6 正则表达式三个常用类

image-20240223154309577

  • 常用方法
    • boolean Pattern.maches(regStr, content) 整体匹配
    • boolean Macher.maches() 整体匹配
    • int Macher.start() int Macher.end() 返回匹配的索引
    • String Macher.replaceAll(String s) 替换匹配的子串

27.8 分组、捕获、反向引用

27.8.1 提出需求

image-20240226100609246

27.8.2 介绍

image-20240226100742475

27.8.3 看几个小案例

image-20240226101541332

27.8.4 经典的结巴程序

把 类似 : “我….我要….学学学学….编程 java!”; 通过正则表达式 修改成 “我要学编程 java”

image-20240226103540076

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class RegExp13 {
public static void main(String[] args) {
String content = "我....我要....学学学学....编程 java!";

//1. 去掉所有的.
Pattern pattern = Pattern.compile("\\.");
Matcher matcher = pattern.matcher(content);
//注意:返回的字符串才是替换后的字符串 原来的 content 不变化
content = matcher.replaceAll("");
System.out.println("newContent=" + content);

//2. 去掉重复的字 我我要学学学学编程 java
// 思路
//(1) 使用 (.)\\1+ 匹配到 (我)我 (学)学学学
//(2) 使用 反向引用$1 来替换匹配到的内容
// 注意:因为正则表达式变化,所以需要重置 matcher
pattern = Pattern.compile("(.)\\1+");//分组的捕获内容记录到$1
matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到=" + matcher.group(0));
}

//3. 使用 反向引用$1 来替换匹配到的内容
content = matcher.replaceAll("$1"); //用(我)替换(我)我;(学)替换(学)学学学
System.out.println("content=" + content);

//4. 使用一条语句 去掉重复的字 我我要学学学学编程 java!
content = Pattern.compile("(.)\\1+").matcher(content).replaceAll("$1");
System.out.println("content=" + content);
}
}

27.9 String 类中使用正则表达式

27.9.1 替换功能

1
2
3
4
5
6
7
String content = "2000 年 5 月,JDK1.3、JDK1.4 和 J2SE1.3 相继发布,几周后其" +
"获得了 Apple 公司 Mac OS X 的工业标准的支持。2001 年 9 月 24 日,J2EE1.3 发 布。2002 年 2 月 26 日,J2SE1.4 发布。自此 Java 的计算能力有了大幅提升";

//使用正则表达式方式,将 JDK1.3 和 JDK1.4 替换成 JDK
//content = content.replaceAll("JDK1\\.3|JDK1\\.4", "JDK");
content = content.replaceAll("JDK1(\\.3|\\.4)", "JDK");
System.out.println(content);

27.9.2 判断功能

1
2
3
4
5
6
7
//要求 验证一个 手机号, 要求必须是以 138 139 开头的
content = "13888889999";
if (content.matches("1(38|39)\\d{8}")) {
System.out.println("验证成功");
} else {
System.out.println("验证失败");
}

27.9.3 分割功能

1
2
3
4
5
6
7
//要求按照 # 或者 - 或者 ~ 或者 数字 来分割字符串
System.out.println("===================");
content = "hello#abc-jack12smith~北京";
String[] split = content.split("#|-|~|\\d+");
for (String s : split) {
System.out.println(s);
}

27.10 Homeworks

  • Homework01

    image-20240226113849814

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    String content = "1530268781@qq.com";
    String regStr = "^[\\w-]+@([a-zA-Z]+\\.)+[a-zA-Z]+$";

    Pattern pattern = Pattern.compile(regStr);
    Matcher matcher = pattern.matcher(content);
    boolean matches = matcher.matches();

    if (content.matches(regStr)) {
    System.out.println("输入合法");
    } else {
    System.out.println("输入不合法");
    }
  • Homework02

    image-20240226113936314

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 要求验证是不是整数或者小数
    * 要求考虑整数和负数 如123 -123 3.14 -3.14
    * 思路:
    * 先写出简单的正则表达式,再逐步完善
    * 考虑 0012.3 不是规范的,0.3是规范的,将
    * \\d+ 改成 [1-9]\\d*|0
    */
    String content = "12.3";
    String regStr = "^[-+]?([1-9]\\d*|0)(\\.\\d+)?$";

    if (content.matches(regStr)) {
    System.out.println("匹配成功");
    } else {
    System.out.println("匹配失败");
    }
    }
  • Homework03

    image-20240226114121201

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 对一个url进行解析
    * 协议 如 http: https:
    * 域名 如 www.suhu.com
    * 端口 如 8080
    * 文件名 如 index.htm
    */
    String content = "http://www.sohu.com:8080/abc/xxx/index.htm";
    String regStr = "^([a-zA-Z]+)://([a-zA-Z.]+):(\\d+)([\\w-/]*)/([\\w.]+)$";

    Pattern pattern = Pattern.compile(regStr);
    Matcher matcher = pattern.matcher(content);

    if (matcher.matches()) {
    System.out.println("整体匹配=" + matcher.group(0));
    System.out.println("协议=" + matcher.group(1));
    System.out.println("域名=" + matcher.group(2));
    System.out.println("端口=" + matcher.group(3));
    System.out.println("路径=" + matcher.group(4));
    System.out.println("文件名=" + matcher.group(5));
    } else {
    System.out.println("匹配失败");
    }